Android 源碼解析之AysncTask

来源:http://www.cnblogs.com/guanmanman/archive/2016/11/18/6076418.html
-Advertisement-
Play Games

AysncTask相信大家都不陌生,它是為了簡化非同步請求、更新UI操作而誕生的。使用它不僅可以完成我們的網路耗時操作,而且還可以在完成耗時操作後直接的更新我們所需要的UI組件。這使得它在android開發中成為炙手可熱的網路請求工具類。 ...


AysncTask相信大家都不陌生,它是為了簡化非同步請求、更新UI操作而誕生的。使用它不僅可以完成我們的網路耗時操作,而且還可以在完成耗時操作後直接的更新我們所需要的UI組件。這使得它在android開發中成為炙手可熱的網路請求工具類。

而今天我們就以源碼分析的形式來徹底的學習下它的實現過程。

首先,我們先看看AysncTask的定義形式:

public abstract class AsyncTask<Params, Progress, Result> {
}

首先AysncTask它是一個抽象類,包括三種泛型類型,具體含義如下:

  • Params:它表示請求參數的類型
  • Progress:執行任務的進度類型
  • Result:返回結果的類型

但是以上三個參數並不是一定必須,在不需要時可以設置為Void,沒有返回類型。

然後我們看看它的執行過程,包括以下幾個方法:

  • execute(Params... params),我們在執行非同步操作時會調用該方法,表示開始執行任務。

  • protected void onPreExecute() {},在調用execute方法後,該方法就會得到執行,它執行在UI線程中,用來初始化一些UI空間等

  • protected abstract Result doInBackground(Params... params);在onPreExecute執行完後將會執行該方法,它執行在後臺,並接受一個Params類型的數組參數,用於請求網路,並且它返回一個Result 類型的結果。該方法中可以在執行網路請求的同時更新請求進度,調用publishProgress(Progress... values) 。

  • protected void onProgressUpdate(Progress... values) ,假如在doInBackground方法中調用了publishProgress方法,那麼該方法就會得到執行,它是執行在UI線程的,根據values的值不停的更改進度,以達到想要的效果。

  • protected void onPostExecute(Result result),該方法是在doInBackground方法執行完畢後得到執行,可根據doInBackground返回的結果進行後續的UI操作,由此可見它是工作在UI線程中的。

經過上面一系列的方法運轉,一個完整的AysncTask請求就正式的完成了任務。不僅完成了耗時操作還更新的UI組件,這就是它的魅力所在。但是這時候你該有疑問了,上面的方法都是你說執行哪個就執行哪個,哪到底是怎麼執行的呢?

那麼接下來就正式的揭開它的廬山正面目。

在正式介紹它的源碼之前,你必須知道new 一個類它所執行的過程:

  • 在new的過程中,它會首先一層一層的載入它所繼承的父類的成員變數及構造方法

  • 然後在載入自己的成員變數和構造方法。

順序是不可變得。

那麼看看在我們執行 new AysncTask()中,它到底載入了哪些成員呢?


    private static final int CPU_COUNT =          Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE = 1;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(128);

    public static final Executor THREAD_POOL_EXECUTOR
            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                    TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

    private static final int MESSAGE_POST_RESULT = 0x1;
    private static final int MESSAGE_POST_PROGRESS = 0x2;

    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
    private static InternalHandler sHandler;

    private final WorkerRunnable<Params, Result> mWorker;
    private final FutureTask<Result> mFuture;

    private volatile Status mStatus = Status.PENDING;
    
    private final AtomicBoolean mCancelled = new AtomicBoolean();
    private final AtomicBoolean mTaskInvoked = new AtomicBoolean();

    private static class SerialExecutor implements Executor{...}
    
    public enum Status {
        PENDING,
        RUNNING,
        FINISHED,
    }

看到這麼一大堆是不是很麻頭皮,其實仔細拆分下來,你主要看幾個變數即可。

  • THREAD_POOL_EXECUTOR :這個成員變數從它THREAD_POOL_EXECUTOR = new ThreadPoolExecutor中可以看出,它是一個線程池,而ThreadPoolExecutor線程池中需要幾個參數,如corePoolSize(核心線程數)、maximumPoolSize(最大線程數)、workQueue(任務隊列)、threadFactory(線程工程)等等,所以像CORE_POOL_SIZE,sPoolWorkQueue ,sThreadFactory 等成員變數,只是為了配置這個線程池而已。

  • sDefaultExecutor 這個成員變數是預設的線程調度任務,從上面可看出SERIAL_EXECUTOR則是一個序列化的任務調度,從sDefaultExecutor = SERIAL_EXECUTOR;中可以明確的知道sDefaultExecutor任務調度中是按先後順序執行的。

  • sHandler顧名思義是一個handler,mWorker是一個工作線程,mFuture則是一個FutureTask,FutureTask是專門用於管理Runnable線程的,mStatus 則是一個枚舉,裡面有三種狀態,分別是未執行,執行中,以及執行完成狀態,預設狀態是未執行狀態。

所以我們只要理解好上面幾個變數可以不用害怕它一堆的初始化成員。

然後我們在看看AysncTask的構造方法中具體做了那些事:

public AsyncTask() {
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                //noinspection unchecked
                Result result = doInBackground(mParams);
                Binder.flushPendingCommands();
                return postResult(result);
            }
        };

        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    postResultIfNotInvoked(get());
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occurred while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };
    }

簡單來說,AsyncTask的成員變數中就只是初始化了兩個變數,mWorker 和 mFuture 。這兩個變數是非常重要的,後續的所有執行過程都是由這兩個變數構成或引導的。

首先mWorker 是一個抽象內部類實例,是一個任務線程,它實現Callable

private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
        Params[] mParams;
    }

然後mFuture 則是一個針對任務線程的管理類。專門用於管理任務線程的,可以使我們的任務得到更好的控制,來看看它的構造方法吧:

 public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

就是接受了我們的mWorker 對象以及把自身的狀態設置為NEW。

以上就是在new一個AysncTask所進行的所有操作,無非就是初始化了一些數據和變數。

下麵來看看AysncTask的正式執行。

我們所知道開啟一個AysncTask任務所調用的方法是execute方法,該方法必須在主線程中調用。

public final AsyncTask<Params, Progress, Result> execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }

在調用execute方法後,該方法什麼也沒做,只是把已初始化好的預設序列任務線程sDefaultExecutor和傳遞進來的數據params傳遞給了executeOnExecutor(),那麼我們在看看這個方法做了哪些事情:

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
            Params... params) {
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
                case FINISHED:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task has already been executed "
                            + "(a task can be executed only once)");
            }
        }

        mStatus = Status.RUNNING;

        onPreExecute();

        mWorker.mParams = params;
        exec.execute(mFuture);

        return this;
    }
  • executeOnExecutor()方法中首先判斷了AysncTask的執行狀態,如果是正在執行或是已經結束執行了,它就會報出一個IllegalStateException的異常,告訴你線程或是在執行中或是已經執行完畢了。

  • 只有在未執行的狀態下,才可以進行AysncTask請求任務,接下來它直接把AysncTask的執行狀態更改為Status.RUNNING,告訴其他任務該AysncTask正在執行中,保持執行結果的一致性。然後就執行了onPreExecute();由於execute方法是必須在主線程中執行的,所以到目前為止還是在主線程中運行,也就證明瞭onPreExecute()方法是在主線程中運行的。

  protected void onPreExecute() {
  }

onPreExecute源碼中並沒有做什麼事情,這對於我們來說,只需要重寫該方法就可以在主線程中進行一些UI組件的初始化等操作。

  • 接下來則是將我們所傳遞的數據賦值給mWorker的mParams變數,然後調用exec.execute(mFuture)方法,我們通過execute方法中知道exec其實就是一個sDefaultExecutor,sDefaultExecutor實則是一個SerialExecutor 序列線程,而mFuture我們在構造方法中也很清楚的知道,它是一個封裝了mWorker線程的一個可管理的任務線程,那麼在調用sDefaultExecutor的execute方法並傳遞進了mFuture任務線程,那到底做了什麼事情呢,我們來看下它的源碼:
private static class SerialExecutor implements Executor {
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        Runnable mActive;

        public synchronized void execute(final Runnable r) {
            mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        scheduleNext();
                    }
                }
            });
            if (mActive == null) {
                scheduleNext();
            }
        }

        protected synchronized void scheduleNext() {
            if ((mActive = mTasks.poll()) != null) {
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }

源碼我們很清晰的知道在execute方法中最終的目的就是把mFuture任務線程賦值給一個Runnable 線程並放到了THREAD_POOL_EXECUTOR線程池中,由THREAD_POOL_EXECUTOR線程池來執行mFuture線程任務。

那麼接著我們看看在THREAD_POOL_EXECUTOR線程池中execute的方法中主要做了什麼事情:

 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

THREAD_POOL_EXECUTOR線程池中主要是判斷了傳遞的線程是否為空,是否小於當前線程池中保存的核心線程數,如果小於則直接執行addWorker(command, true)方法,下麵看看addWorker方法中的實現內容:

 private boolean addWorker(Runnable firstTask, boolean core) {
        ...(前面代碼省略)
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) 
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

我們只看主要的邏輯,首先是將我們的mFuture任務線程存放到了一個Worker的對象中,然後又從Worker對象中獲取到mFuture線程並賦值給了Thread ,接著把Worker對象放到workers的HashSet數據集合對象中,經過獲取HashSet的大小併進行一些判斷,把workerAdded 設置為true,最後開啟t.start();線程,由此進入了子線程中

那麼接下來在開啟的子線程中又做了什麼事情呢?

我們從上面的分析指導t.start()開啟就是一個mFuture的非同步任務線程,那麼它在哪執行呢?

細心的朋友可以發現,原來是在SerialExecutor 中的execute方法中我們的mFuture的run()早已在等待了線程的啟動,那麼,我現在去看看mFuture的run()方法中做了什麼工作吧

public void run() {
        if (state != NEW ||
            !U.compareAndSwapObject(this, RUNNER, null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

這段代碼很簡單,一眼就可以看得出來就是利用我們在為mFuture初始化時傳遞的mWorker 對象實例並調用它的call()方法,我們先不管call怎麼實現的,先來看看這個方法中的後續是什麼。

接著它得到一個執行結果,並把一個boolean類型的ran設置為true,最後根據ran調用set(result);方法,並把結果傳遞進去,下麵看看set的源碼:

protected void set(V v) {
        if (U.compareAndSwapInt(this, STATE, NEW, COMPLETING)) {
            outcome = v;
            U.putOrderedInt(this, STATE, NORMAL); // final state
            finishCompletion();
        }
    }

它主要調用了finishCompletion();在來看看finishCompletion的源碼:

 private void finishCompletion() {
        // assert state > COMPLETING;
        for (WaitNode q; (q = waiters) != null;) {
            if (U.compareAndSwapObject(this, WAITERS, q, null)) {
                for (;;) {
                    Thread t = q.thread;
                    if (t != null) {
                        q.thread = null;
                        LockSupport.unpark(t);
                    }
                    WaitNode next = q.next;
                    if (next == null)
                        break;
                    q.next = null; // unlink to help gc
                    q = next;
                }
                break;
            }
        }

        done();

        callable = null;        // to reduce footprint
    }

在執行完call中,把一些對象進行還原,還調用了 done(),該方法就是在AysncTask構造方法中我們有看到它的實現:

mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    postResultIfNotInvoked(get());
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occurred while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };

我們也說過FutureTask主要是用來管理非同步線程任務的,那麼在done方法中就有很好的體現,在該方法中,它會判斷執行的結果是否成功,成功後有沒有被髮送,如果有發送它就不再發送消息,如果結果執行成功,但沒有被髮送它就會發送最終的執行結果:

  private void postResultIfNotInvoked(Result result) {
        final boolean wasTaskInvoked = mTaskInvoked.get();
        if (!wasTaskInvoked) {
            postResult(result);
        }
    }

postResult方法的內容我們推後一點講,那麼現在我們來看看mWorker 中call()是怎麼實現的:

mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                //noinspection unchecked
                Result result = doInBackground(mParams);
                Binder.flushPendingCommands();
                return postResult(result);
            }
        };

在這裡我們終於見到了我們所熟悉的一個方法doInBackground(),由此也可以知道其確實是在子線程運行的,而doInBackground()方法在AysncTask類中是一個抽象方法:

 protected abstract Result doInBackground(Params... params);

那麼我們在重寫doInBackground()時就可以直接的在其中進行一些耗時的網路和IO操作了。

這裡插上一句,假如在doInBackground()調用了publishProgress方法來更新進度的話,那來看看它是怎麼做的:

  protected final void publishProgress(Progress... values) {
        if (!isCancelled()) {
            getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
        }
    }

publishProgress方法中主要是通過Hangler發送一條更新進度的標誌用來更新進度。這裡的Hangler接受消息在下麵和執行結果一起講。

最後doInBackground()執行獲取的Result 結果也將會傳遞到postResult(result);方法中,那麼現在我們來看看它的源碼實現:

 private Result postResult(Result result) {
        @SuppressWarnings("unchecked")
        Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskResult<Result>(this, result));
        message.sendToTarget();
        return result;
    }
    
  private static Handler getHandler() {
        synchronized (AsyncTask.class) {
            if (sHandler == null) {
                sHandler = new InternalHandler();
            }
            return sHandler;
        }
    }

  private static class AsyncTaskResult<Data> {
        final AsyncTask mTask;
        final Data[] mData;

        AsyncTaskResult(AsyncTask task, Data... data) {
            mTask = task;
            mData = data;
        }
    }

postResult中首先封裝了doInBackground非同步執行結果的AsyncTaskResult對象,然後獲取到一個Handler ,通過消息處理機制發送一條信息來切換到主線程中進行UI界面的更換,消息處理機制不屬於本次博文的內容所以不再細說,那來看看這個Handler是怎麼處理這個消息內容的。

private static class InternalHandler extends Handler {
        public InternalHandler() {
            super(Looper.getMainLooper());
        }

        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }

Handler中主要是根據消息標誌進行區分是更新進度還是執行結果:

如果是更新進度則調用AysncTask的onProgressUpdate方法來更新內容,由於通過Handler已轉變為主線程中,所以我們在重寫該方法時可以直接更新UI組件。

如果是執行結果則AysncTask的finish(result.mData[0]);並把結果數據傳遞過去,來看看finish()中是怎麼實現的:

  private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            onPostExecute(result);
        }
        mStatus = Status.FINISHED;
    }

finish()方法中也非常的簡單,首先判斷是否為取消線程,否則的話則執行onPostExecute(result)方法,由此我們在重寫了onPostExecute方法後可以直接的更新我們的UI組件。

最後把AysncTask的狀態改為完成狀態,至此整個AysncTask生命周期就執行完畢了。

好了,至此AysncTask整個執行過程就完全講完了,相信大家也學到了不少東西,建議大家有空自己對著源碼在梳理一遍,畢竟自己總結出來的印象就更深刻。

今天就到這裡吧,祝大家生活愉快。

更多資訊請關註微信平臺,有博客更新會及時通知。愛學習愛技術。
這裡寫圖片描述


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這一節詳細的總結jQuery選擇器。 一、基礎選擇器 二、基本過濾器 基本選擇器獲取的元素集合,通過過濾器的篩選,使選擇更加精確。 三、內容過濾器 jQery準備了內容過濾器用於對選擇的元素集合內容進行過濾。 統一名稱: 空元素:不包含任何後代元素或文本內容的元素,如<div></div> 非空元素 ...
  • 捂臉,辛酸淚ing...... 本文主要涉及部分在移動設備上特有的問題。 相對來說,Jquery側重DOM操作,AngularJS是以視圖模型和雙向綁定為核心的。 ...
  • position position--設置定位方式,設置參照物 top,right,bottom,left,z-index--設置位置,必須配合position使用,如果一個元素不是一個定位元素,設置了這些屬性是不起效果的。 上面這兩項結合就能定 一個元素在瀏覽器中的位置 1. top/right/... ...
  • 在建設網站中用到地圖是很常見的,在國內大部分都是用百度地圖,但是有時候可能會用到國外地址,這時候就只能使用谷歌地圖了。 方法一、使用框架引入谷歌地圖 用框架引入谷歌地址是最簡單的方法,不是專業開發人員也可以操作。登陸ditu.google.cn地圖,輸入地址信息,如:"上海東方明珠",把地圖移動到合... ...
  • <html> <head> <title> new document </title> <meta name="generator" content="editplus" /> <meta name="author" content="" /> <meta name="keywords" conte ...
  • 定義:正則用於規定在文本中檢索的內容,它是對字元串執行模式匹配的強大工具 RegExp(正則表達式) 對象的語法: new RegExp(pattern, attributes); pattern為一個字元串或匹配規則 attributes為可選字元串,包含屬性g、i 和 m g:代表全局匹配 (繼 ...
  • 關於Turtle Online Turtle online 是Turtle框架的PC前端架構,包括組件和API兩大部分,可以快速的搭建PC前端開發環境。組件包括日曆、分頁、圖片輪播/圖片瀏覽、各類提示彈框/自定義彈層、氣泡提示、圖標icon、表單等。API包括常用JS方法封裝(cookie操作、aj ...
  • 1、tortoisegit Git下載地址: https://tortoisegit.org/download/ SVN下載地址: https://tortoisesvn.net/downloads.html 2、sourcetree https://www.sourcetreeapp.com/ ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...