参考知乎@李板溪
问题背景,假设你要下载一张美图显示出来。 使用这个问题就可以说明主要的问题了。
好了 上代码,下载图片,然后显示在 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<Bitmap> bitmapFutureTask = new FutureTask<>(new Callable<Bitmap>() {
@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<String,Void,Bitmap>(){
@Override
protected Bitmap doInBackground(String... params) {
return downloadImage(params[0]);
}
@Override
protected void onPostExecute(Bitmap bitmap) {
mBeautyImageView.setImageBitmap(bitmap);
}
}.execute(beautyUrl);
相比之前的代码简洁不少。
看一下 AsyncTask 的源代码,正是集合我们之前考虑的这些东西。
- 一个全局的线程池
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
- 一个绑定主线程的 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 可以让我们非常方便的在各线程中安排可执行的任务。