zoukankan      html  css  js  c++  java
  • Android中子线程真的不能更新UI吗?

    今天讲一个老生常谈的问题,"Android中子线程真的不能更新UI吗?

    AndroidUI访问是没有加锁的,这样在多个线程访问UI是不安全的。

    所以Android中规定只能在UI线程中访问UI。子线程更新是不被允许的。

    那么子线程访问UI会报错吗?

     首先,我们在布局文件随意定义一个textview:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <TextView
            android:id="@+id/tv_test"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:paddingTop="10dp"
            android:paddingBottom="10dp"
            android:textColor="@color/yellow"
            android:background="#000000"
            android:text="test"
            android:textSize="16sp"
            android:gravity="center"/>
    </LinearLayout>

    接着我们在activity中开了个子线程修改UI

    class TestActivity :AppCompatActivity(R.layout.activity_test) {
        
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            Thread(object :Runnable{
                override fun run() {
                    tv_test.text = "这是修改后的text"
                }
    
            }).start()
        }
    }

    展示,并没有报错:

     

    紧接着,我们试试延时试试:

    class TestActivity :AppCompatActivity(R.layout.activity_test) {
        
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            Thread(object :Runnable{
                override fun run() {
                    Thread.sleep(3000)
                    tv_test.text = "这是修改后的text"
                }
    
            }).start()
        }
    }

    运行试了下,3s后居然奔溃了,这是为啥呢?

    查看错误日志:

    经典问题出现了,查看源码可以看到,是在ViewRootImpl.checkThread中抛出的这个异常.

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

    当访问 UI 时,ViewRootImpl 会调用 checkThread方法去检查当前访问 UI 的线程是否为创建 UI 的那个线程,如果不是。则会抛出异常。

    那为啥要一定需要checkThread?根据handler的相关知识:

     

    因为UI控件不是线程安全的。那为啥不加锁呢?

    一是加锁会让UI访问变得复杂;

    二是加锁会降低UI访问效率,会阻塞一些线程访问UI

    所以干脆使用单线程模型处理UI操作,使用时用Handler切换即可。

     

     

    疑问点:为什么一开始在 MainActivity onCreate 方法中创建一个子线程访问 UI,程序还是正常能跑起来呢???

     

    我们可以看看tv_text.setText触发的调用流程:

     

    TextView#setText
        -->View#requestLayout()
        满足条件:
           --> ViewParent # requestLayout
           --> ViewRootImpl # requestLayout
           
        -->View#invalidate
        -->  View#invalidate(boolean)
        -->  View#invalidateInternal //如果 if mAttachInfo 以及 mParent 都不为空
            -->ViewParent#invalidateChild
            -->ViewRootImpl#invalidateChild
            -->ViewRootImpl#invalidateChildInParent
                  
    ---------------------
    View#invalidateInternal
    // Propagate the damage rectangle to the parent view.
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }
    
    ViewRootImpl#invalidateChildInParent
            @Override
            public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
                checkThread();
                if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
    
                if (dirty == null) {
                    invalidate();
                    return null;
                } else if (dirty.isEmpty() && !mIsAnimating) {
                    return null;
                }

     

    我们仔细看一下mThread,他这个错误信息并不是:

    Only the UI Thread ... 而是 Only the original thread

    这个mThread是什么?

    ViewRootImpl的成员变量,我们重点应该关注它什么时候赋值的:

    public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mThread = Thread.currentThread();
    
    }

    ViewRootImpl构造的时候赋值的,赋值的就是当前的Thread对象。

    也就是说,ViewRootImpl在哪个线程创建的,你后续的UI更新就需要在哪个线程执行,跟是不是UI线程毫无关系。

    那么,VIewRootImpl是什么时候创建的呢?

    我们启动Activity绘制UI的方法在onResume方法里,所以我们找到Activity的线程ActivityThread类。在ActivityThread中,我们找到handleResumeActivity方法,内部调用了performResumeActivity方法,逐层跟进会发现调用了activity.onResume()方法,所以performResumeActivity确实是resume的入口。

     

    ActivityThread.java#handleResumeActivity

    final void handleResumeActivity(IBinder token,
                boolean clearHide, boolean isForward, boolean reallyResume) {
                ...
    if (r.window == null && !a.mFinished && willBeVisible) {
                    r.window = r.activity.getWindow();
                    View decor = r.window.getDecorView();
                    decor.setVisibility(View.INVISIBLE);
                    ViewManager wm = a.getWindowManager();
                    WindowManager.LayoutParams l = r.window.getAttributes();
                    a.mDecor = decor;
                    l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                    l.softInputMode |= forwardBit;
                    if (a.mVisibleFromClient) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l);
                    }
    
                // If the window has already been added, but during resume
                // we started another activity, then don't yet make the
                // window visible.
                } else if (!willBeVisible) {
                    if (localLOGV) Slog.v(
                        TAG, "Launch " + r + " mStartedActivity set");
                    r.hideForNow = true;
                }
                ...
    }

    wm.addView(decor, l);是他进行的View的加载,我们去看看他的实现方法,在WindowManager的实现类WindowManagerImpl里:

     

    @Override
        public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
            applyDefaultToken(params);
            mGlobal.addView(view, params, mDisplay, mParentWindow);
        }

     

    发现他是调用WindowManagerGlobal的方法实现的,最后我们找到了最终实现addView的方法:

    WindowManagerGlobal.java#addView

    public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
            ...
    
            ViewRootImpl root;
            View panelParentView = null;
    
            synchronized (mLock) {
                // Start watching for system property changes.
                ...
                root = new ViewRootImpl(view.getContext(), display);
    
                view.setLayoutParams(wparams);
    
                mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
            }
    
            // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                synchronized (mLock) {
                    final int index = findViewLocked(view, false);
                    if (index >= 0) {
                        removeViewLocked(index, true);
                    }
                }
                throw e;
            }
        }

    果然在这里,View的加载最后就是在这里实现的,而ViewRootImpl的实例化也在这里。

    所以如果我们在子线程中调用WindowManageraddView方法,是不是就可以成功更新UI呢?

     

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Thread(object :Runnable{
            override fun run() {
                    Thread.sleep(3000)
                    // tv_test.text = "这是修改后的text"
                    val tx = TextView(this@TestActivity)
                    tx.text = "这是修改后的text"
                    tx.setBackgroundColor(Color.WHITE)
                    val layoutParams = WindowManager.LayoutParams(
                        200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                        WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE
                    )
                    windowManager.addView(tx, layoutParams)
            }
    
        }).start()
    }

     

     错误原因是没有启动Looper。原来是因为在ViewRootImpl类里新建了ViewRootHandler的实例mHandler在子线程中加上Looper.prepare()Looper.loop(),然后成功了~

     

    Thread(object :Runnable{
                override fun run() {
    //                tv_test.text = "这是修改后的text"
                    Thread.sleep(3000)
                    Looper.prepare()
                    val tx = TextView(this@TestActivity)
                    tx.text = "这是修改后的text"
                    tx.setBackgroundColor(Color.BLACK)
                    tx.setTextColor(Color.YELLOW)
                    val layoutParams = WindowManager.LayoutParams(
                        WindowManager.LayoutParams.WRAP_CONTENT, 100, 0, 0, WindowManager.LayoutParams.TYPE_DRAWN_APPLICATION,
                        WindowManager.LayoutParams.TYPE_STATUS_BAR, PixelFormat.OPAQUE
                    )
                    windowManager.addView(tx, layoutParams)
                    Looper.loop()
                }
    
            }).start()

     

    运行成功!

     

    扩展点:为什么子线程需要加Looper.loop(),主线程不用?

     

    总结:

    ViewRootImpl 的创建在 onResume 方法回调之后,而我们一开篇是在 onCreate 方法中创建了子线程并访问 UI,在那个时刻,ViewRootImpl 还没有创建,我们在因此 ViewRootImpl#checkThread 没有被调用到,也就是说,检测当前线程是否是创建的 UI 那个线程 的逻辑没有执行到,所以程序没有崩溃一样能跑起来。而之后修改了程序,让线程休眠了 3000 毫秒后,程序就崩了。很明显 3000 毫秒后 ViewRootImpl 已经创建了,可以执行 checkThread 方法检查当前线程。

    等等,还没完?

    在测试的时候,偶然发现这么写不会报错??怀疑人生!!?

     

    Thread(object :Runnable{
        override fun run() {
            Thread.sleep(3000)
            tv_test.text = tv_test.text.toString()+"abc"
        }
    
    }).start()

     

     仔细想想,想明白了,这块就涉及到View的绘制流程了,设置值时,执行到View.requestLayout->ViewRootImpl#requestLayout 方法,

    ViewRootperformTraversals()方法会在measure结束后继续执行,并调用Viewlayout()方法来执行此过程,如下所示:

     

    host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);

     

    在layout()方法中,首先会判断视图的宽高是否发生过变化,以确定有没有必要对当前的视图进行重绘,这块显然没有变化,自然也就不会继续往下执行了。

    下次如果有人问你 Android 中子线程真的不能更新 UI 吗? 你可以这么回答:

    任何线程都可以更新自己创建的 UI。只要保证满足下面几个条件就好了

    1.  在 ViewRootImpl 还没创建出来之前。
      1. UI 修改的操作没有线程限制。因为 checkThread 方法不会被执行到。
    2. 在 ViewRootImpl 创建完成之后
      •  保证「创建 ViewRootImpl 的操作」和「执行修改 UI 的操作」在同一个线程即可。也就是说,要在同一个线程调用 ViewManager#addView 和 ViewManager#updateViewLayout 的方法。
        •  注:ViewManager 是一个接口,WindowManger 接口继承了这个接口,我们通常都是通过 WindowManger(具体实现为 WindowMangerImpl) 进行 view 的 add remove update 操作的。
      • 对应的线程需要创建 Looper 并且调用 Looper#loop 方法,开启消息循环。

    有同学可能会问,保证上述条件 1 成立,不就可以避免 checkThread 时候抛出异常了吗?为什么还需要开启消息循坏?

     条件 1 可以避免检查异常,但是无法保证 UI 可以被绘制出来。

     条件 2 可以让更新的 UI 效果呈现出来

    •  WindowManger#addView 最终会调用 WindowManageGlobal#addView 方法,进而触发ViewRootImpl#setView 方法,该方法内部会调用ViewRootImpl#requestLayout 方法。
    •  根据 UI 绘制原理,下一步就是 scheduleTraversals 了,该方法会往消息队列中插入一条消息屏障,然后调用 Choreographer#postCallback 方法,往 looper 中插入一条异步的 MSG_DO_SCHEDULE_CALLBACK 消息。等待垂直同步信号回来之后执行。

     注:ViewRootImpl 有一个 Choreographer 成员变量,ViewRootImpl 的构造函数中会调用 Choreographer#getInstance(); 方法,获取一个当前线程的 Choreographer 局部实例。

     

    使用子线程更新 UI 有实际应用场景吗?

    Android 中的 SurfaceView 通常会通过一个子线程来进行页面的刷新。如果我们的自定义 View 需要频繁刷新,或者刷新时数据处理量比较大,那么可以考虑使用 SurfaceView 来取代 View

     

    扩展知识-使用Looper实现日志捕获:

    讲这个之前,先说下Looper的流程吧~

     

    前面说到,需要在子线程中调用Looper.loop开启循环。那么我们的主线程为什么没有调用呢?

    这里又涉及到handler的事件分发机制了,查看源码得知,在ActivityThread中,系统已经帮我们调用了Looper.prepareMainLooper()

     

    public static void main(String[] args) {
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
    
        // Install selective syscall interception
        AndroidOs.install();
    
        // 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();
    
        // 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();
    
        // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
        // It will be in the format "seq=114"
        long startSeq = 0;
        if (args != null) {
            for (int i = args.length - 1; i >= 0; --i) {
                if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
                    startSeq = Long.parseLong(
                            args[i].substring(PROC_START_SEQ_IDENT.length()));
                }
            }
        }
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);
    
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
    
        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }
    
        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();
    
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

     

    然后执行Looper.loop(),不断从队列中抽取message

     

    public static void loop() {
            final Looper me = myLooper();
            if (me == null) {
                throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
            }
            //1、获取到消息队列
            final MessageQueue queue = me.mQueue;
    
           //..................
    
            //开启死循环
            for (;;) {
                //2、拿到队列中的消息
                Message msg = queue.next(); // might block
                if (msg == null) {
                    // No message indicates that the message queue is quitting.
                    return;
                }
    
            //省略部分不相关的代码
            //..................
                try {
                    //3、执行队列中的消息
                    msg.target.dispatchMessage(msg);
                    dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
                } finally {
                    if (traceTag != 0) {
                        Trace.traceEnd(traceTag);
                    }
                }
            //..................
                msg.recycleUnchecked();
            }

     

    loop()方法中,代码非常简单,分三步走

     1、获取到looper中的 MessageQueue

     2、开启一个死循环,从MessageQueue 中不断的取出消息

     3、执行取出来的消息  msg.target.dispatchMessage(msg);(顺便说一下,HandlerhandleMessage()方法就是在这一步执行的)

    在第二步里面,会发生阻塞,如果消息队列里面没有消息了,会无限制的阻塞下去,主线程休眠,释放CPU资源,直到有消息进入消息队列,唤醒线程。从这里就可以看出来,loop死循环本身大部分时间都处于休眠状态,并不会占用太多的资源,真正会造成线程阻塞的反而是在第三步里的  msg.target.dispatchMessage(msg)方法,因此如果在生命周期或者handlerHandlerhandleMessage执行耗时操作的话,才会真正的阻塞UI线程。

     由此我们可以自定义一个handler+Looper截全局崩溃(主线程),避免 APP 退出。

    相关代码如下:

     

    class MyApp : Application() {
    
        override fun onCreate() {
            super.onCreate()
    
            var handler = Handler(Looper.getMainLooper())
            handler.post {
                while (true){
                    try {
                     Looper.loop()
                    }catch (e:Throwable){
                        e.printStackTrace()
                        if (e.message != null && e.message!!.startsWith("Unable to start activity")) {
                            System.gc();
                            _restart();
                            android.os.Process.killProcess(android.os.Process.myPid());
                            break
                        }
                    }
                }
            }
            Thread.setDefaultUncaughtExceptionHandler { t, e ->
                e.printStackTrace()
            }
        }
    
        private fun _restart() {
            val intent = getPackageManager().getLaunchIntentForPackage(getPackageName())
            intent?.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
            startActivity(intent)
        }
    
    
    }

     

    testDemo:

     

    var data:ArrayList<String> ?= null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        tv_test.setOnClickListener {
            data!!.add("1")
        }
    }

    不添加上述代码时,点击文本:

    页面闪退,报错:

     

    添加后:

    异常捕获,页面不受影响

     

     

    通过上面的代码就可以就可以实现拦截UI线程的崩溃。但是也并不能够拦截所有的奔溃,如果在ActivityonCreate出现崩溃,导致Activity创建失败,那么就会杀死该app并重启。

     

    参考链接:

     

  • 相关阅读:
    gulp-css-spriter 将css代码中的切片图片合并成雪碧图
    通过JS模拟select表单,达到美化效果[demo]
    jQuery拖拽 & 弹出层
    sublime text 快速编码技巧 GIT图
    原生JS不到30行,实现类似javascript MVC的功能-minTemplate
    javascript拖拽原理与简单实现方法[demo]
    滚动焦点图实现原理和实践[原创视频]
    谈一谈值类型与引用类型和装箱与拆箱
    【原创】asp.net内部原理(三) 第三个版本 (最详细的版本)
    由JS函数返回值引发的一场”血案"
  • 原文地址:https://www.cnblogs.com/fangg/p/12917235.html
Copyright © 2011-2022 走看看