android中handle和線程的關係是什麼?

Android 中 Handler 和線程的關係,Thread,非同步任務,有點亂


我來詳細解釋下吧,

問題背景,假設你要下載一張美女圖顯示出來。 使用這個問題就可以說明主要的問題了。

好了 上代碼,下載美女圖片,然後顯示在 ImageView 中。 代碼如下:

public class MainActivity extends AppCompatActivity {

public static final String beautyUrl = "http://ww3.sinaimg.cn/large/6e1fdf79gw1etbqbu4256j20c80idmy4.jpg";
ImageView mBeautyImageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBeautyImageView = (ImageView)findViewById(R.id.beauty);
mBeautyImageView.setImageBitmap(downloadImage(beautyUrl));
}

@Nullable
public Bitmap downloadImage(String urlString){
try {
final URL url = new URL(urlString);
try(InputStream is = url.openStream()){
return BitmapFactory.decodeStream(is);
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
}

}

然後這樣的一段看似沒有問題的代碼,在 Android 3 以上是會直接報錯的。 主要錯誤原因在

Caused by: android.os.NetworkOnMainThreadException at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1147)

為了保證用戶體驗, Android 在 3.0 之後,就不允許在 主線程(MainThread)即 UI線程 中執行網路請求了。 那怎麼辦呢?

好吧,我們暫不考試 Android 提供的一系統組件及工具類, 用純 Java 的方式來解決這個問題。

在新的線程中下載顯示圖片

在新創建的線程中下載顯示圖片如下:

new Thread(new Runnable() {
@Override
public void run() {
mBeautyImageView.setImageBitmap(downloadImage(beautyUrl));
}
}).start();

看起來來錯的樣子,跑起來看看。 啊,又報錯了。

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

說只能在創建View 層級結構的線程中修改它。 創建它的線程就是主線程。 在別的線程不能修改。 那怎麼辦?

那現在我們遇到的問題是: 下載不能在主線程下載。 更新ImageView 一定要在 主線程中進行。 在這樣的限制下,我們自然而然想去一個解決辦法: 就是在新創建的線程中下載。 下載完成在主線程中更新 ImageView。

但是,怎麼在下載完成之後,將圖片傳遞給主線程呢?這就是我們問題的關鍵了。

線程間通信

我們的通信要求,當下載線程中下載完成時,通知主線程下載已經完成,請在主線程中設置圖片。 純 Java 的實現上面的線程間通信的辦法我暫沒有找到,於是我想到使用 FutureTask 來實現在主線程中等待圖片下載完成,然後再設置。

FutureTask& bitmapFutureTask = new FutureTask&<&>(new Callable&() {
@Override
public Bitmap call() throws Exception {
return downloadImage(beautyUrl);
}
});
new Thread(bitmapFutureTask).start();
try {
Bitmap bitmap = bitmapFutureTask.get();
mBeautyImageView.setImageBitmap(bitmap);
} catch (InterruptedException |ExecutionException e) {
e.printStackTrace();
}

不過這雖然騙過了 Android 系統,但是雖然系統阻塞的現象沒有解決。 例如我在 get() 方法前後設置了輸出語句:

Log.i(TAG,"Waiting Bitmap");
Bitmap bitmap = bitmapFutureTask.get();
Log.i(TAG,"Finished download Bitmap");

設置了下載時間至少 5 秒鐘之後,輸出如下:

06-27 23:30:18.058 21298-21298/com.banxi1988.androiditc I/MainActivity﹕ Waiting Bitmap 06-27 23:30:23.393 21298-21298/com.banxi1988.androiditc I/MainActivity﹕ Finished download Bitmap

讓主線程什麼事做不做,就在那傻等了半天。然後由於 onCreate沒有返回。用戶也就還沒有看到界面。 導致說應用半天啟動不了。。卡死了。。

查閱了一些資料,沒有不用 Android 的框架層的東西而實現在其他進程執行指定代碼的。

而 Android 中線程間通信就得用到 android.os.Handler 類了。 每一個 Handler 都與一個線程相關聯。 而 Handler 的主要功能之一便是: 在另一個線程上安插一個需要執行的任務。

這樣我的問題就通過一個 Handler 來解決了。

於是 onCreate 中的相關代碼變成如下了:

final Handler mainThreadHandler = new Handler();

new Thread(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = downloadImage(beautyUrl);
mainThreadHandler.post(new Runnable() {
@Override
public void run() {
mBeautyImageView.setImageBitmap(bitmap);
}
});
}
}).start();

看起來很酷的樣子嘛,一層套一層的 Runnable. mainThreadHandler 因為是在 主線程中創建的, 而 Handler創建時,綁定到當前線程。 所以 mainThreadHandler 綁定到主線程中了。

當然 Android 為了方便你在向主線程中安排進操作,在 Activity類中提供了 runOnUiThread 方法。 於是上面的代碼簡化為:

new Thread(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = downloadImage(beautyUrl);
runOnUiThread(new Runnable() {
@Override
public void run() {
mBeautyImageView.setImageBitmap(bitmap);
}
});
}
}).start();

你不用自己創建一個 Handler了。

而 runOnUiThread 的具體實現,也跟我們做得差不多。UI線程即主線程。

public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}

Handler 與 Loop 簡介

上面說到 每一個 Handler 都與一個線程相綁定。 實際上是,通過 Handler 與 Loop 綁定,而每一個 Loop 都與一個線程想綁定的。 比如 Handler 中兩個主要構造函數大概如下:

public Handler(...){
mLooper = Looper.myLooper();
// ...
}

public Handler(Looper looper,...){
mLooper = looper;
// ...
}

public static Looper myLooper() {
return sThreadLocal.get();
}

然後 Looper 的構造函數如下:

private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

綁定了當前的線程和生成了一個消息隊列。

值得提起的一點是, Looper 類保持了對 應用的主線程的 Looper 對象的靜態應用。

private static Looper sMainLooper; // guarded by Looper.class
public static Looper getMainLooper() {
synchronized (Looper.class) {
return sMainLooper;
}
}

這樣就可以方便你在其他線程中,使用一個綁定到主線程的 Handler,從而方便向主線程安插任務。 例如一般的圖片處理庫即是如此。這樣你只要指定一個 圖片的 url,及要更新的ImageView 即可。 如 Picasso 庫可以用如下代碼的讀取並更新圖片。 Picasso.with(context).load(url).into(imageView);

然後 Handler 可以做的事情還有很多。 因為它後面有 Looper 有 MessageQueue。可以深入了解下。

談一下線程池與 AsyncTask 類

Android 早期便有這個便利的類來讓我們方便的處理 工作線程及主線程的交互及通信。 但是現在先思考一下,我們上面的代碼可能遇到的問題。 比如,我們現在要顯示一個圖片列表。 一百多張圖片。 如果每下載一張就開一個線程的話,那一百多個線程,那系統資源估計支持不住。特別是低端的手機。

正確的做法是使用一個線程池。

final ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = downloadImage(beautyUrl);
runOnUiThread(new Runnable() {
@Override
public void run() {
mBeautyImageView.setImageBitmap(bitmap);
}
});
executor.shutdown();
}
});

由於我們是在在一個局部方法中使用了一個線程池。所以處理完了之後應該將線程停止掉。 而我們上面只有一個線程,所以直接在下載完成之後,調用 executor停掉線程池。 那如果執行了多個圖片的下載請求。需要怎麼做呢? 那就要等他們都完成時,再停止掉線程池。 不過這樣用一次就停一次還是挺浪費資源的。不過我們可以自己保持一個應用級的線程池。 不過這就麻煩不少。

然後 Android 早已經幫我們想法了這一點了。 我們直接使用 AsyncTask 類即可。

於是我們下載圖片並顯示圖片的代碼如下:

new AsyncTask&(){
@Override
protected Bitmap doInBackground(String... params) {
return downloadImage(params[0]);
}

@Override
protected void onPostExecute(Bitmap bitmap) {
mBeautyImageView.setImageBitmap(bitmap);
}
}.execute(beautyUrl);

相比之前的代碼簡潔不少。

看一下 AsyncTask 的源代碼,正是集合我們之前考慮的這些東西。

  1. 一個全局的線程池

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

  1. 一個綁定主線程的 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:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
}
}
}

小結

Android 提供的 Looper 和 Handler 可以讓我們非常方便的在各線程中安排可執行的任務。


@李板溪 的回答通俗易懂,寫的很好。

我補充下純java實現線程通信(應該算是吧),使用回調函數,還是非同步的,一直想不通為什麼Future搞成同步的.

public class TestThreadCallBack {

public static void main(String[] args) {
System.out.println("Main ThreadId: " + Thread.currentThread().getId());
final String [] str = new String[1];
runThreadCallBack(new CallbackListener() {
@Override
public void onFinish(String s) {
System.out.println("onFinish ThreadId: " + Thread.currentThread().getId());
System.out.println(s);
str[0] = s;
}
@Override
public void onError(Exception e) {

}
});
System.out.println(str[0]);//null
}

private static void runThreadCallBack(final CallbackListener callbackListener) {
System.out.println("runThreadCallBack ThreadId: " + Thread.currentThread().getId());
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5*1000);
} catch (InterruptedException e) {
e.printStackTrace();
callbackListener.onError(e);
}
callbackListener.onFinish("CallbackListener");
System.out.println("run ThreadId: " + Thread.currentThread().getId());
}
}).start();

}

private interface CallbackListener{

void onFinish(String s);

void onError(Exception e);
}
}


你還漏了一個looper,這幾個傢伙一共構成ui更新 。


使用Handle不一定會新起一個進程(Thread),這一點是肯定的


乾的事情基本是一樣的。區別在於。

Thread,自己處理線程安全。

Handler,幫你處理好了線程安全,內部實現用Thread。

AsyncTask, 只能從UIThread發出,幫你處理好了線程安全,內部實現用Thread。

大部分的時候我會使用Thread或者Handler,因為他們比較容易寫,Handler的postDelayed方法比較方便。在需要不斷更新進度的時候可以考慮使用AsyncTask,例如下載更新包等等。


推薦閱讀:

helio x20和高通650哪個更強?
為何安卓沒有整合Chrome?
Android 下什麼中文字體最好?
不能拆卸電池的的安卓手機死機了怎麼辦?
有沒有兼具iPhone和安卓優勢的安卓手機?

TAG:Android開發 | Java | Android |