抛砖引玉,浅讲Handler和线程的关系

参考知乎@李板溪

问题背景,假设你要下载一张美图显示出来。 使用这个问题就可以说明主要的问题了。

好了 上代码,下载图片,然后显示在 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 的源代码,正是集合我们之前考虑的这些东西。

  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 可以让我们非常方便的在各线程中安排可执行的任务。

posted @ 2016-08-17 17:35  虎必烈  阅读(270)  评论(0编辑  收藏  举报