Android 子线程更新 UI

01.Android中子线程可以更新UI吗

  • 出自《Android艺术探索》
  • 这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那么为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:
    • ①首先加上锁机制会让UI访问的逻辑变得复杂
    • ②锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。
    • 所以最简单且高效的方法就是采用单线程模型来处理UI操作。
  • 为什么说子线程不能更新UI?
    • 子线程是不能直接更新UI的。Android实现View更新有两组方法,分别是invalidate和postInvalidate。前者在UI线程中使用,后者在非UI线程即子线程中使用。换句话说,在子线程调用 invalidate 方法会导致线程不安全。熟悉View工作原理的人都知道,invalidate 方法会通知 view 立即重绘,刷新界面。作一个假设,现在用 invalidate 在子线程中刷新界面,同时UI线程也在用 invalidate 刷新界面,这样会不会导致界面的刷新不能同步?这就是invalidate不能在子线程中使用的原因。

02.子线程更新UI有哪些方式

  • 主要有这些方法
    • 主线程中定义Handler,子线程通过mHandler发送消息,主线程Handler的handleMessage更新UI
    • 用Activity对象的runOnUiThread方法
    • 创建Handler,传入getMainLooper
    • View.post(Runnable r)

03.runOnUiThread如何实现子线程更新UI

  • 如何使用代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    new Thread(new Runnable() {
    @Override
    public void run() {
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    tv_0.setText("滚犊子++++");
    }
    });
    }
    }).start();
  • 看看源码,如下所示

    • 如果msg.callback为空的话,会直接调用我们的mCallback.handleMessage(msg),即handler的handlerMessage方法。由于Handler对象是在主线程中创建的,所以handler的handlerMessage方法的执行也会在主线程中。
    • 在runOnUiThread程序首先会判断当前线程是否是UI线程,如果是就直接运行,如果不是则post,这时其实质还是使用的Handler机制来处理线程与UI通讯。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
    handleCallback(msg);
    } else {
    if (mCallback != null) {
    if (mCallback.handleMessage(msg)) {
    return;
    }
    }
    handleMessage(msg);
    }
    }

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

04.创建Handler,传入getMainLooper

  • Looper在哪个线程创建,就跟哪个线程绑定,并且Handler是在他关联的Looper对应的线程中处理消息的。

    • 在子线程中,是否也可以创建一个Handler,并获取MainLooper,从而在子线程中更新UI呢?首先我们看到,在Looper类中有静态对象sMainLooper,并且这个sMainLooper就是在ActivityThread中创建的MainLooper。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    //源码部分
    private static Looper sMainLooper;
    public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
    if (sMainLooper != null) {
    throw new IllegalStateException("The main Looper has already been prepared.");
    }
    sMainLooper = myLooper();
    }
    }


    //在子线程中通过这个sMainLooper来进行更新UI操作
    new Thread(new Runnable() {
    @Override
    public void run() {
    Log.e("yc", "yc 4"+Thread.currentThread().getName());
    //注意这里创建handler时,需要传入looper
    Handler handler = new Handler(getMainLooper());
    handler.post(new Runnable() {
    @Override
    public void run() {
    //Do Ui method
    tv_0.setText("滚犊子————————————---");
    Log.e("yc", "yc 5"+Thread.currentThread().getName());
    }
    });
    }
    }).start();

05.View.post(Runnable r)更新UI

  • 代码如下所示

    1
    2
    3
    4
    5
    6
    tv_0.post(new Runnable() {
    @Override
    public void run() {
    tv_0.setText("滚犊子");
    }
    });
  • 源码原理如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
    return attachInfo.mHandler.post(action);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
    }

    private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
    mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
    }
  • View.post(Runnable r)使用注意事项

    • 看源码的注释可知:如果view已经attached,则调用ViewRootImpl中的ViewRootHandler,放入主线程Lopper等待执行。如果detach,则将其暂存在RunQueue当中,等待其它线程取出执行。
    • View.post(Runnable r)很多时候在子线程调用,用于进行子线程无法完成的操作,或者在该方法中通过getMeasuredWidth()获取view的宽高。需要注意的是,在子线程调用该函数,可能不会被执行,原因是该view不是attached状态。

06.子线程更新UI总结概括

  • handler.post(Runnable r)、 view.post(Runnable r)、activity.runOnUIThread(Runnable r)等方法。跟进去看源码,发现其实它们的实现原理都还是一样,最终都是通过Handler发送消息来实现的。

07.子线程中Toast,showDialog

  • 先来看看问题代码

    1
    2
    3
    4
    5
    6
    new Thread(new Runnable() {
    @Override
    public void run() {
    Toast.makeText(MainActivity.this, "run on thread",Toast.LENGTH_SHORT).show();//崩溃无疑
    }
    }).start();
  • 如何解决上面的问题呢?

    1
    2
    3
    4
    5
    6
    7
    8
    new Thread(new Runnable() {
    @Override
    public void run() {
    Looper.prepare();
    Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();
    Looper.loop();
    }
    }).start();
  • 在子线程中Toast原理分析

    • 在show方法中,我们看到Toast的show方法和普通UI控件不太一样,并且也是通过Binder进程间通讯方法执行Toast绘制。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public void show() {
    //首先判断吐司内容不能为空
    if (mNextView == null) {
    throw new RuntimeException("setView must have been called");
    }
    //从SMgr中获取名为notification的服务
    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;

    try {
    service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
    // Empty
    }
    }
    • 把目光放在TN 这个类上,通过TN类,可以了解到它是Binder的本地类。在Toast的show方法中,将这个TN对象传给NotificationManagerService就是为了通讯!并且我们也在TN中发现了它的show方法。
    • 使用Handler,所以需要创建Looper对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    private static class TN extends ITransientNotification.Stub {
    final Runnable mHide = new Runnable() {
    @Override
    public void run() {
    handleHide();
    }
    };
    final Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
    IBinder token = (IBinder) msg.obj;
    handleShow(token);
    }
    };
    }
    • Toast本质是通过window显示和绘制的(操作的是window),而主线程不能更新UI是因为ViewRootImpl的checkThread方法在Activity维护的View树的行为。Toast中TN类使用Handler是为了用队列和时间控制排队显示Toast,所以为了防止在创建TN时抛出异常,需要在子线程中使用Looper.prepare();和Looper.loop();(但是不建议这么做,因为它会使线程无法执行结束,导致内存泄露)