UI更新为什么一定要在UI线程里?幕后真相究竟如何?

Posted 骨灵冷

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UI更新为什么一定要在UI线程里?幕后真相究竟如何?相关的知识,希望对你有一定的参考价值。

相信很多人看到这个标题之后轻笑一声,UI更新为啥要在UI线程里?逗呢,这是谷歌专门制定的规则,当然要遵循规则来玩儿啦。但是细致的人看到后半截标题之后就不会那么轻易的下结论了,二八定律告诉我们,大部分人都知道的事儿往往有着猫腻。

我当然相信且明确知道谷歌制定了那么一套规则:UI更新一定要在主线程中进行。所以依旧取这个标题绝对不是哗众取宠。本文需要讨论的内容主要有以下几点:
1.谷歌为什么要制定这套规则?
2.这套规则背后隐藏的机制是什么?
3.这套规则是如何形成使用闭环的?
4.该机制的细节有哪些?
5.这套规则的优势和劣势有哪些?
6.这套规则未来可能的前景有哪些?

带着这些问题展开我的理解进行讨论。
[TOC]

谷歌为什么要制定这套规则

讨论这个命题之前,先抛出我认为的结论:

    谷歌提出“UI更新一定要在UI线程里实现”这一规则的根本原因在于提高移动端UI的使用效率和体验。

在移动设备中,UI的使用是绝对设备对于用户体验的重要因素之一。而谷歌android系统当中的控件都不是线程安全的,这导致当使用多线程模式的时候在多个线程共同使用同一个UI控件时容易发生不可控的错误,而这是致命的。

    那么谷歌为什么不把UI控件设计成线程安全的呢?我认为这也是移动端的特点所致,由于移动端的应用十分讲究UI,不单单数量多,UI的交互方式也很多,而且UI还会涉及到动画的体现等等,诸多因素叠加,如果考虑使用多线程的话,线程间存在挂起和堵塞,会十分影响UI的使用效率,这对于移动端app来说是不可饶恕的。另一方面,移动端app所属的移动设备本身具有一定的不足:如系统推出之初内存小、磁盘容量小、CPU数量和性能等等,这些先天性的移动设备特征也为谷歌在最初设计UI线程这一方案奠定了基础。

    因此,规则的产生基于产品的特点和使用场景,在这样的移动端应用大背景下,在移动端的设备特征下,多线程与移动端所期许的高效快速的使用体验下是存在冲突矛盾的,所以至少目前的多线程模型是不合适的。由此,谷歌推出单线程模型,所有涉及UI的操作都在一个线程内完成,这个线程由于主要用于UI的效率提升,为UI服务,就称为了UI线程。既然是单线程模型,且讲究UI的使用效率,那么就催生出了另一个概念:

    在这个单线程UI线程内(ActivityThread)中不可以执行耗时操作,如果有耗时操作,就有了熟悉的"ANR"错误了。

这套规则背后隐藏的机制是什么

前面了解了下谷歌推出UI线程的原因,但是UI线程基于的模型是单线程模型,而为了单线程模型下UI效率,禁止UI线程内进行耗时任务的执行,推出了ANR。但是,有时候确实需要执行耗时任务该如何呢?在单线程模型下,UI线程负责UI更新,子线程负责耗时任务。当子线程中的耗时任务执行完成之后需要更新UI的时候,由于分属不同线程,因此推出了Handler消息机制。

    Handler机制是谷歌单线程模型的伴生产物,就是为了解决“子线程需要执行耗时任务”和“主线程不允许耗时且必须负责UI更新”这两者之间线程的矛盾问题而出现的。

Handler机制的核心点就是Looper、MessageQueue、Handler三者的铁三角组成。

这套规则是如何形成闭环的

前面两节已经详细描述了UI线程推出的原因以及伴随UI线程引出的新的机制:Handler消息机制,那么就来用如下图所示来描述整个规则闭环的构成:

    在如上的闭环图描述中,给出了单线程模型和Handler机制推行的原因。同时也提出了为什么会引出"ANR"错误和“子线程更新UI即报错”。

    Handler机制是用于线程切换任务执行的;单线程模型是为了有效提高UI体验和效率;两者最终都是从UI角度和移动设备角度出发来考虑的。

该机制的细节有哪些

论述完前面的内容,接下来就来详细拆分一下这套规则、这套handler机制背后的一些细节布置,将描述以下几个细节:

(1)Handler机制相关细节

既然是单线程模型,那就必须详细了解一下Handler机制。

    handler机制的核心三要素:Handler、Looper、MessageQueue

Handler:

           这玩意存在的目的就在于实现线程间的工作切换,将一个任务从一个线程中切换到另一个线程去执行,我在读《Android开发艺术探索》一书中认为作者使用“切换”一词的用意很到位。当我们在子线程中执行完耗时任务时,通过使用handler,来让代码逻辑切换到主线程中,完成UI的更新。看一下Handler源码:
  public Handler() 
        this(null, false);
      

上面的构造方法是我们最常使用的构造方法了(啥也不用填总是挺舒服的),在该方法中调用了另一个构造方法:

public Handler(Callback callback, boolean async) 
        if (FIND_POTENTIAL_LEAKS) 
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) 
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            
        

        mLooper = Looper.myLooper();
        if (mLooper == null) 
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    

这才是真正实现Handler的地方之一,其实里面的逻辑可以拆分成三个部分:
a.Handler类的构建声明
b.获取Looper
c.Handler赋值

看第一部分:

        if (FIND_POTENTIAL_LEAKS) 
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) 
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            
        

这一部分逻辑其实是控制Handler类的构建。我们一般在使用Handler的时候都会创建一个自定义继承自Handler的本地类,这个时候需声明此类为static,且不说匿名类、不是内部类,具有static修饰符。否则,将会提示出会有内存泄漏的风险存在。

看第二部分:

mLooper = Looper.myLooper();

从Looper中拿到looper对象。我们知道线程是代码逻辑运行的最小环境,而每一个Looper都会绑定到一个线程中去。恰好,咱们的单线程模型依托主线程,所以可以需要一个Looper去绑定,具体的绑定后续介绍。那么Handler服务于线程切换,所依托的消息机制必然需要一个Looper,如果此线程都没事先实现Looper的绑定,那么在这个线程里创建Handler也会导致失败的。如下:

if (mLooper == null) 
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");

看第三部分:

mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;

当获取了所在线程中的looper之后,为Handler的一系列成员变量赋值了就。最主要的就是拿到Looper内部维护的消息队列mQueue了。这样才可以把自己手里的消息从外部传递给另一个线程中,然后执行对应操作。

callback是什么呢?
关于callback会在Looper当中去介绍更合适。先预留一下这个玩意。

Looper:

Looper是在Handler机制中与Thread密不可分的关键元素(抛开Handler机制就不管它了)

首先了解一下在线程中创建Looper的逻辑,这里使用HandlerThread的代码为例:

@Override
    public void run() 
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) 
            mLooper = Looper.myLooper();
            notifyAll();
        
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    

在Thread的回调方法run中创建Looper,其核心点有两处,分别是Looper.prepare()和Looper.loop()。

完成以上两步之后就创建好了Looper,并使得Thread开始执行时启动Looper的消息循环。

看一下Looper的prepare方法:

private static void prepare(boolean quitAllowed) 
        if (sThreadLocal.get() != null) 
            throw new RuntimeException("Only one Looper may be created per thread");
        
        sThreadLocal.set(new Looper(quitAllowed));
    

首先从sThreadLocal里获取looper对象,如果获取到了就会报出异常,不然的话就可以执行set方法了,将新建的一个looper方法塞给了sThreadLocal。这里为什么要这样的代码设计呢?

这样设计的好处就在于此静态方法只会调用一次,因为set方法必然会执行(如过prepare方法执行完的话),但是一个线程只能有一个Looper,所以这样的设计其实也算是“单例”实现的一种吧。

这里还要提一下关于sThreadLocal,这个玩意儿是一个ThreadLocal类型,这个类型是以线程为作用域的,实现以线程为单位各自数据的存储和获取,线程间不会影响:

// sThreadLocal.get() will return null unless you've called prepare().
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

具体ThreadLocal是如何实现以线程为作用域的数据存储而线程相互之间做到不影响的呢?后续其他的文章会专门描述,可以提一下,其实每个线程类Thread里也有一个全局变量:

ThreadLocal.Values  localValues

这个localValues绑定了ThreadLocal的set方法和get方法,所以实际上set和get方法结合了Thread的接应,完成了以线程为单位的数据获取与存储。

讲完了prepare,再来看一下Looper.loop方法:

/**
     * Run the message queue in this thread. Be sure to call
     * @link #quit() to end the loop.
     */
    public static void loop() 
        final Looper me = myLooper();
        if (me == null) 
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) 
            Message msg = queue.next(); // might block
            if (msg == null) 
                // No message indicates that the message queue is quitting.
                return;
            

            // This must be in a local variable, in case a UI event sets the logger
            Printer logging = me.mLogging;
            if (logging != null) 
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            

            msg.target.dispatchMessage(msg);

            if (logging != null) 
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) 
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            

            msg.recycleUnchecked();
        
    

loop方法很长,但是实际上需要关注的点就是for(;;)里面的逻辑,在执行这样一个死循环之后,每次循环都会搜索MessageQueue里面的内容,查看是否会有新的message,如果有,就拿出来,执行以下方法:

msg.target.dispatchMessage(msg);

msg.target属性就是handler,毕竟消息都是handler传递过来的嘛。

看一下handler是如何传递消息的:

public final boolean sendMessage(Message msg)
    
        return sendMessageDelayed(msg, 0);
    

往下继续调用,调用到sendMessageAtTime之后进入enqueueMessage的如下方法:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) 
        msg.target = this;
        if (mAsynchronous) 
            msg.setAsynchronous(true);
        
        return queue.enqueueMessage(msg, uptimeMillis);
    

在这个方法里对target属性赋值,就是handler自己。
为什么要用target呢? 我认为这样的设计是为了解耦,降低耦合性。

那么looper在loop方法里重新让hander执行dispatchMessage方法是意欲何为呢?看一下dispatchMessage方法:

public void dispatchMessage(Message msg) 
        if (msg.callback != null) 
            handleCallback(msg);
         else 
            if (mCallback != null) 
                if (mCallback.handleMessage(msg)) 
                    return;
                
            
            handleMessage(msg);
        
    

在上面的代码逻辑里,首先判断callback是否为空,若不为空就执行handleCallback方法;若为空,则执行handleMessage方法,这个方法就是我们熟悉handler回调方法了,至此将后续逻辑切换到了Handler所在线程当中去执行了。

我们来回顾一下之前提出的问题,handler的构造方法里为啥会有个callback。其实这要归宿到handler派发消息除了sendMessage之外的另一种姿势:post

public final boolean post(Runnable r)
    
       return  sendMessageDelayed(getPostMessage(r), 0);
    

我们可以new一个Runnable作为post方法的参数,然后handler会自动将这个Runnable对象包装成一个Message:

private static Message getPostMessage(Runnable r) 
        Message m = Message.obtain();
        m.callback = r;
        return m;
    

这样的话,loop里的回调方法就不是handleMessage而是我们自己创建Runnable时覆写的run方法了。
这就是msg及handler要有callback的原因。

我们之前也提到过,“切换”是handler存在的意义,如果有两个线程,Thread1和Thread2,我们完全可以在Thread1中创建一个Handler,但是使用的构造方法是带参Looper的:

public Handler(Looper looper) 
        this(looper, null, false);
    

这个looper完全可以显式的使用Thread2中的looper,这样在Thread1中的操作就可以切换到Thread2中去了。具体可以参看HandlerThread的使用姿势。

MesssageQueue:

讲完了Handler和Looper,MessageQueue就显而易见了。只是一个消息队列而已,内部采用单链表的数据结构来实现和维护。

(2)主线程入口

都说UI线程、主线程,问问谁谁谁这个主线程叫啥,好像初入android的都不知道。
其实这个主线程叫ActivityThread。而app的入口如果除去launcher这个进程和AMS的话,以app为单位,入口就是ActivityThread的main方法:

public static void main(String[] args) 
        SamplingProfilerIntegration.start();

        // CloseGuard defaults to true and can be quite spammy.  We
        // disable it here, but selectively enable it later (via
        // StrictMode) on debug builds, but using DropBox, not logs.
        CloseGuard.setEnabled(false);

        Environment.initForCurrentUser();

        // Set the reporter for event logging in libcore
        EventLogger.setReporter(new EventLoggingReporter());

        Security.addProvider(new AndroidKeyStoreProvider());

        // Make sure TrustedCertificateStore looks in the right place for CA certificates
        final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
        TrustedCertificateStore.setDefaultUserDirectory(configDir);

        Process.setArgV0("<pre-initialized>");

        Looper.prepareMainLooper();

        ActivityThread thread = new ActivityThread();
        thread.attach(false);

        if (sMainThreadHandler == null) 
            sMainThreadHandler = thread.getHandler();
        

        if (false) 
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        

        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    

这个main方法是一个静态方法,任务就是初始化主线程ActivityThread,并且启动looper.loop:

ActivityThread thread = new ActivityThread();
Looper.loop();

在初始化ActivityThread的时候就初始化好mainLooper了。

(3)子线程更新UI报错

我们知道在进行子线程更新的时候会报错。
实际上各自UI控件都是最终派生自View,而UI的绘制离不开ViewRootImpl这个类,在这个类中实现了线程检查:

void checkThread() 
        if (mThread != Thread.currentThread()) 
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        
    

但是ViewRootImpl的初始化完成在handleResumeActivity方法中:

final void handleResumeActivity(...)
//省略
if (r.activity.mVisibleFromClient) 
                    r.activity.makeVisible();
                
//省略

进入到makeVisible方法:


    void makeVisible() 
        if (!mWindowAdded) 
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        
        mDecor.setVisibility(View.VISIBLE);
    

在makeVisible方法里,创建了一个WindowManager,而WindowManager的具体实现类是WindowManagerGlobal,所以进入到该类中可以找到addView的具体实现,在其中,我们可以找到ViewRootImpl的初始化:

root = new ViewRootImpl(view.getContext(), display);

所以,当Resume方法执行完成之后就不能在子线程中更新UI了,但是可以在onCreate方法中(onResume方法执行完成之前),使用子线程更新UI。

这套规则的优势和劣势有哪些

在上一节里,详细再详细的描述了整套UI规则背后承载的机制细节,那么谷歌使用这套规则和机制,到底有什么优势和劣势呢?

这里本人也是简单想了下,首先优势我认为有以下几个点:
1.UI使用效率得到提高;
2.UI体验得到提高;
3.针对当前的设备特点和产品特征,具有一定的友好性和使用可行性;
4.轻量级;

那么缺点呢?
1.在一个线程内充斥着各种逻辑难免会过于复杂,可维护行和扩展性有待商榷;
2.如果解决了多线程的一些弊端,可能多线程带来的效率提升会高一些;
3.硬件在提升,技术在提升,新的系统以及在开发了,so?

所以,任何一个机制、规则的诞生都离不开一系列的需求背景和当前条件的限制,那么如果一个个瓶颈被突破之后,技术也会持续改革和更新吧。

这套规则未来可能的前景有哪些

相信你们的意见会更好的。
反正我觉得谷歌在研究新系统已经想表达他们对android一些已经成型的机制的无奈了。如果推到重来,还不如投入更多的精力研发一个更好的系统呢。

以上就是对UI线程这个大众所知的概念进行的一些研究和理解,如果有什么兴趣,可以联系:
QQ:526315041
微信:526315041
博客:木有
电话:木有
地点:上海市长宁区

以上是关于UI更新为什么一定要在UI线程里?幕后真相究竟如何?的主要内容,如果未能解决你的问题,请参考以下文章

如何在android一条单独线程,更新ui ?

iOS 子线程下载 主线程刷新UI

为何invalidate()不可以直接在UI线程中调用

为啥loop之后就可以子线程更新ui

试图了解 iOS 中后台线程行为的幕后情况

多线程学习之--真的不能在子线程里更新UI吗?