來玩Android執行緒

Rick

關於

三竹資訊 編源人

Rick Wu (吳仁弘)

大家好 我是Rick Wu

今天我要說一個關於

玩執行緒卻被執行緒玩

的故事。

朋友說想寫一個APP,  

將自己放在網路上的照片下載下來

 醒醒吧!你沒有朋友

所以他就買了一本Android的書來參考

過沒多久他就跑來問說

為什麼我把圖片讀出來要等很久?

而且沒辦法進行其他操作

所以看了一下他的程式碼

Image img = loadImg(url);//讀取圖片
refresh();//刷新畫面

原因就出在你把所有程式都寫在UI Thread

Thread是Process中一系列有序的執行流程

一般情況下Android App由一個Process組成

Process可以有多個Thread

每個Thread會被分配一小段的執行時間

每執行完一個Thread就跳到下一個Thread

由於轉換的速度很快

這使得一個程式可以像是同時間處理多個事務

什麼是Thread

Android分成兩種Thread

  • UI Thread                       控制畫面

  • Non-UI Thread           適合處理冗長任務

長任務都應該往後面站

寫在Background Thread

哪些可能是長任務呢?

長任務包含

  • 檔案、資料庫處理

  • 非同步處理(檔案上傳、下載)

  • 運算處理(影像處理、大量數學運算)

  • 排程(時間任務)

很快的他把程式改寫成這樣

new Thread(new Runnable(){
    public void run(){
        Image img = loadImg(url);//讀取圖片
        refresh();//刷新畫面
    }
}).start();

然後程式就死掉了

因為你不能在Non-UI Thread更新UI

進行了一個更新畫面的動作

透過Handler可以讓不同的Thread進行溝通

什麼是Handler?

Handler是Android特有的機制

透過Handler可以跟UI Thread進行溝通

Handler一次送出一個訊息UI Thread處理

Runnable

Runnable

Handler

Handler

Message Queue

Looper

Message

Message

UI Thread

Message

就像一台投籃機一次處理一顆球

要注意的點是Handler送出的訊息是循序的,

因此有可能造成訊息處理時間過長導致畫面延遲

前面的球卡住後面就全部塞住惹

這樣的App容易得到一顆星

爛透惹

givemepass

2016/10/29

於是學會Handler以後

朋友又繼續開發他的App了

朋友: 我覺得程式碼很醜!

我: 蛤?

new Thread(new Runnable(){
    @Override
    public void run(){
        //Task 1
        runOnUiThread(new Runnable(){
            @Override
            public void run(){
                //update ui
                new Thread(new Runnable(){
                    @Override
                    public void run(){
                        //Task 2
                        runOnUiThread(new Runnable(){
                            @Override
                            public void run(){
                                //update ui
                            }
                        });
                    }
                }).start();
            }
        });
    }
}).start();

這不就是傳說中的

Callback Hell

科科

HandlerThread

可以幫你解決這個問題

HandlerThread是一個Thread

內部維護

MessageQueueLooper

就跟UI Thread一樣

new Thread(new Runnable() {
    
    public void run() {
        
        Log.e(TAG, "A");
        
        Looper.prepare();
        
        new Handler().post(new Runnable(){

            
            @Override
            
            public void run() {
                
                Log.e(TAG, "B1");
            
            }
        
        });
        
        new Handler().post(new Runnable(){

            
            @Override
            
            public void run() {
                
                Log.e(TAG, "B2");
                
                Looper.myLooper().quit();
            
            }
        
        });


        Looper.loop();

        
        Log.e(TAG, "C");
        
        ((Activity) mContext).runOnUiThread(new Runnable() {
            
            public void run() {
                
                Log.e(TAG, "D");
            
        }
        
    });
    
}
}).start();

輸出為:

  • A
  • B1
  • B2
  • C
  • D

改用HandlerThread來做

HandlerThread mHandlerThread = new HandlerThread("my handler thread");
mHandlerThread.start();
Handler mHandler = new Handler(mHandlerThread.getLooper());
mHandler.post(task1);
mHandler.post(task2);
// ... n-2 task
mHandler.post(taskN);

//...
mHandlerThread.quit();

如果1張圖1個Thread 

100張圖就要開100個Thread?

Thread Pool

可以解決這個問題

什麼是Thread Pool?

一組任務佇列以及工作執行緒的結合

ThreadPoolExecutor自行定義

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    
    int corePoolSize,
    
    int maxPoolSize,
    
    long keepAliveTime,
    
    TimeUnit unit,
    
    BlockingQueue<Runnable> workQueue
);

透過內建ThreadPool操作

固定尺寸執行器 : 固定大小

Executors.newFixedThreadPool(2);

動態尺寸執行器 : 動態調整, 超過60秒沒做事就被刪掉。

Executors.newCachedThreadPool();

單一執行緒執行器 : 單一Thread, 後來的任務要排隊。

Executors.newSingleThreadExecutor();

ThreadPool的好處

Java提供了ThreadPoolExecutor讓我們使用, 有幾個優點:

  • Thread能保持存活, 等待新任務, 不會隨著任務建立再銷毀。

  • Thread Pool限制最大Thread數量, 避免系統浪費。

  • Thread的生命週期被Thread Pool控制。

不僅如此,

透過內建ExecutorService會回傳一個Future物件

你聽過Future Pattern嗎?

什麼是Future Pattern

Future Pattern是一種非同步處理的程式習慣, 一開始會先讓你取得目標物件,

但是內容尚未取得,此時可以先去處理其他事情, 等待結果處理完畢後,

再透過目標物件內的方法將結果取出

舉例來說

到電影院看電影,

你會先拿到一張票(目標物件),

此時電影尚未開始,

因此先去樓下逛街或者買東西(做其他事情), 等到時間到了,

憑著這張票就可以進場看電影(實際的資料)。

等待時間

這是一段非常耗時的程式

其他工作

其他工作

Call

Other Call

other call return

call return

Client

Service

Data

這是一段非常耗時的程式

其他工作

其他工作

Call

Other Call

other call return

立即返回

Client

Service

Data

Request

Client

Server

Future

Data

Thread

Real

Data

Result

Future Object

程式內怎麼表示呢?

Java提供內建的Future

Interface Future<V>
All Known Implementing Classes:
ForkJoinTask, FutureTask, RecursiveAction, 
RecursiveTask, SwingWorker


FutureTask(Callable<V> callable)

FutureTask(Runnable runnable, V result)

Future Pattern的優缺點

  • 非同步處理Task

  • 可讀性高

  • 方法簡單容易操作

  • 不易管理

  • 不好重組再利用

Future搭配Thread使用

FutureTask<Void> futureTask = new FutureTask<>(()->{
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("hi");
}, null);
new Thread(futureTask).start();
try {
    futureTask.get();
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}

Future搭配ThreadPool使用

ExecutorService executor = Executors.newSingleThreadExecutor();

Future<Object> future = executor.submit(new Callable<Object>(){
    
    public Object call() throws Exception{
        
        Object obj = doLongTask();
        
        return obj;
    
    }

});

Object result = future.get();

在操作上是否可以更彈性?

  • 如果多個任務只要一個完成就處理怎麼做?

  • 如果想要全部結果完成才處理怎麼做?

  • 如果想要回來一個就處理一個怎麼做?

InvokeAll與InvokeAny的操作

Executor提供同時多個Thread並行的操作。

InvokeAll : 同時並行多個Thread, 並且透過blocking來取回每一個Task的結果

InvokeAny: 同時並行多個Thread, 只要有一個回傳成功, 則終止剩下的Task

單執行緒, 那就確保送進去的任務按照順序回來

如果是用多執行緒, 就無法確保回來的順序

ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorCompletionService<String> completionService = 
    new ExecutorCompletionService<>(executorService);

for(int i = 0; i < 100; i++) {
    completionService.submit(getTask(i));
}
//...

String s = completionService.take().get();

如果你想要回來一個Task處理一個Task,

可以使用ExecutorCompletionService

Executor的確是一個很方便的工具

但是全部都還是背景執行緒

如果我要更新UI還是要透過Handler來處理

另外我可能還需要進度條的顯示下載百分比

難道沒有更方便的工具嗎?

Android早就預料你會有這樣的需求

AsyncTask已經準備好等著你來用

跟UI Thread溝通的AsyncTask

想執行長時間運行的任務,

最後再跟UI Thread進行溝通,

Android將這些特性全部封裝在AsyncTask

如何使用AsyncTask ?

class MyTask extends AsyncTask<Params, Progress, Result>{
    
    protected void onPreExecute(){
       
        // in main thread

    
    }
   
    protected Result doInBackground(Params... params){
       
        // in background thread

    
    }
  
    protected void onProgressUpdate(Progress... progress){
       
        // in main thread

    
    }
        
    protected void onPostExecute(Result result){
       
        // in main thread

    
    }
    
    protected void onCancelled(Result result){
       
        // in main thread

    
    }


}
  • PENDING : 實體化但未執行execute。

    RUNNING : execute已經呼叫。

    FINISHED : onPostExecute或onCancelled已經被呼叫。

    以上三個狀態都是不可逆的, 一旦進入了RUNNING狀態, 就無法再啟動新的執行,必須重新產生新的實體

    可透過AsyncTask.getStatus()取得狀態資訊。

AsyncTask的狀態

AsyncTask自訂Executor(範例)

AsyncTask允許你自訂執行器

//循序

executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, Object... objs);

//並行

executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, Object... objs);
//自訂

executeOnExecutor(CustomExecutor, Object... objs);

AsyncTask<Void, Void, Void>

背景執行緒無法跟UI執行緒溝通, 沒有任何回傳結果。

又或者只是實作doInBackground

這樣就只是個Background Thread

不建議的使用方式

初音只是個軟體

有這些工具就可以開發出

良好使用者體驗的APP了

太棒惹

AsyncTask也使用Future Pattern

public AsyncTask() {
    mWorker = new WorkerRunnable<Params, Result>() {
        public Result call() throws Exception {
            mTaskInvoked.set(true);

            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            //noinspection unchecked
            return postResult(doInBackground(mParams));
        }
    };

    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 occured while executing doInBackground()",
                        e.getCause());
            } catch (CancellationException e) {
                postResultIfNotInvoked(null);
            }
        }
    };
}

抓到!

Q & A

Executor還不夠好用

  • 任務與任務之間的彼此不相關, 造成前一任務的結果必須透過變數來進行連結, 造成複雜度變高以及過於耦合難以重用。
  • 如果任務之間必須傳入類似的非同步物件, 則會造成Callback hell
ExecutorService singleThread = Executors.newSingleThreadExecutor();
singleThread.submit(new Runnable() {
    @Override
    public void run() {
        //處理Task
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //處理Ui
            }
        });
    }
});
singleThread.submit(new Runnable() {
    @Override
    public void run() {
        //處理Task
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //處理Ui
            }
        });
    }
});

Callback Hell

readFileAsync(args[0],
    content -> processContentAsync(content,
                 processedContent -> handleData(content,
                         c-> out.println(c) ,
                         ex -> ex.printStackTrace(), service) ,
                 ex -> ex.printStackTrace(), service),
    ex -> ex.printStackTrace(), service);

CompletableFuture的專業處理

CompletableFuture
.runAsync(() -> task1)
.thenRunAsync(() -> task2)
.thenRunAsync(() -> task3)
.whenComplete((r, ex) -> System.out.println("done"));

CompletableFuture

只能運行在Java 8

Android N 才能使用Java 8

RxJava

List<Task> taskList = new ArrayList<>();
taskList.add(new Task("task 1"));
taskList.add(new Task("task 2"));
taskList.add(new Task("task 3"));
Observable.from(taskList)
.subscribe(new Observer<Task>() {
    private boolean isTaskSuccess = true;
    @Override
    public void onCompleted() {
        System.out.println("completed");
    }

    @Override
    public void onError(Throwable e) {
        System.out.println("error");
    }

    @Override
    public void onNext(Task task) {
        try {
            if(!task.call() || !isTaskSuccess){
                isTaskSuccess = false;
                System.out.println(task.getName() + " failed.");
            } else {
                System.out.println(task.getName() + " success.");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});

Thank you.

Made with Slides.com