• 抛砖引玉,浅讲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 可以让我们非常方便的在各线程中安排可执行的任务。

  • 相关阅读:
    新浪微博OAuth2.0 VS OAuth1.0 主要区别总结
    NSCondition的用法
    iOS $99 刀 开发者证书的申请步骤
    three20 报出 文件 no such file or directory的原因以及解决方案
    three20.bundle以及 sharekit.bundle多语言无法生效的解决方案
    NSThread的使用
    iOS中switchcase的优化用法
    iOS开发中生成随机数方法的选择
    面向对象软件设计原则(五) —— 应用示例
    普通软件项目开发过程规范(四)—— 控制和结束阶段
  • 原文地址:https://www.cnblogs.com/wzdwork/p/5780928.html
Copyright © 2020-2023  润新知