Android中子线程真的不能更新UI吗?-程序员宅基地

技术标签: 子线程更新UI线程  UI线程  子线程  子线程不能更新UI线程  Android  

Android的UI访问是没有加锁的,这样在多个线程访问UI是不安全的。所以Android中规定只能在UI线程中访问UI。

但是有没有极端的情况?使得我们在子线程中访问UI也可以使程序跑起来呢?接下来我们用一个例子去证实一下。

新建一个工程,activity_main.xml布局如下所示:

复制代码
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <TextView
        android:id="@+id/main_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:layout_centerInParent="true"
        />

</RelativeLayout>
复制代码

很简单,只是添加了一个居中的TextView

MainActivity代码如下所示:

复制代码
public class MainActivity extends AppCompatActivity {

    private TextView main_tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        main_tv = (TextView) findViewById(R.id.main_tv);

        new Thread(new Runnable() {

            @Override
            public void run() {
                main_tv.setText("子线程中访问");
            }
        }).start();

    }

}
复制代码

也是很简单的几行,在onCreate方法中创建了一个子线程,并进行UI访问操作。

点击运行。你会发现即使在子线程中访问UI,程序一样能跑起来。结果如下所示: 

咦,那为嘛以前在子线程中更新UI会报错呢?难道真的可以在子线程中访问UI?

先不急,这是一个极端的情况,修改MainActivity如下:

复制代码
public class MainActivity extends AppCompatActivity {

    private TextView main_tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        main_tv = (TextView) findViewById(R.id.main_tv);

        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                main_tv.setText("子线程中访问");
            }
        }).start();

    }

}
复制代码

让子线程睡眠200毫秒,醒来后再进行UI访问。

结果你会发现,程序崩了。这才是正常的现象嘛。抛出了如下很熟悉的异常:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. 
at android.view.ViewRootImpl.checkThread(ViewRootImpl.Java:6581) 
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)

……

作为一名开发者,我们应该认真阅读一下这些异常信息,是可以根据这些异常信息来找到为什么一开始的那种情况可以访问UI的。那我们分析一下异常信息:

首先,从以下异常信息可以知道

at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6581)

这个异常是从android.view.ViewRootImpl的checkThread方法抛出的。

这里顺便铺垫一个知识点:ViewRootImpl是ViewRoot的实现类。

那现在跟进ViewRootImpl的checkThread方法瞧瞧,源码如下:

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

只有那么几行代码而已的,而mThread是主线程,在应用程序启动的时候,就已经被初始化了。

由此我们可以得出结论: 
在访问UI的时候,ViewRoot会去检查当前是哪个线程访问的UI,如果不是主线程,那就会抛出如下异常:

Only the original thread that created a view hierarchy can touch its views

这好像并不能解释什么?继续看到异常信息

at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)

 

那现在就看看requestLayout方法,

复制代码
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}
复制代码

这里也是调用了checkThread()方法来检查当前线程,咦?除了检查线程好像没有什么信息。那再点进scheduleTraversals()方法看看

复制代码
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
复制代码

注意到postCallback方法的的第二个参数传入了很像是一个后台任务。那再点进去

复制代码
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
复制代码

找到了,那么继续跟进doTraversal()方法。

复制代码
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}
复制代码

可以看到里面调用了一个performTraversals()方法,View的绘制过程就是从这个performTraversals方法开始的。PerformTraversals方法的代码有点长就不贴出来了,如果继续跟进去就是学习View的绘制了。而我们现在知道了,每一次访问了UI,Android都会重新绘制View。这个是很好理解的。

分析到了这里,其实异常信息对我们帮助也不大了,它只告诉了我们子线程中访问UI在哪里抛出异常。 
而我们会思考:当访问UI时,ViewRoot会调用checkThread方法去检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常,这是没问题的。但是为什么一开始在MainActivity的onCreate方法中创建一个子线程访问UI,程序还是正常能跑起来呢?? 
唯一的解释就是执行onCreate方法的那个时候ViewRootImpl还没创建,无法去检查当前线程。

那么就可以这样深入进去。寻找ViewRootImpl是在哪里,是什么时候创建的。好,继续前进

在ActivityThread中,我们找到handleResumeActivity方法,如下:

复制代码
final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume) {
    // If we are getting ready to gc after going to the background, well
    // we are back active so skip it.
    unscheduleGcIdler();
    mSomeActivitiesChanged = true;

    // TODO Push resumeArgs into the activity for consideration
    ActivityClientRecord r = performResumeActivity(token, clearHide);

    if (r != null) {
        final Activity a = r.activity;

        //代码省略

            r.activity.mVisibleFromServer = true;
            mNumVisibleActivities++;
            if (r.activity.mVisibleFromClient) {
                r.activity.makeVisible();
            }
        }

      //代码省略    
}
复制代码

可以看到内部调用了performResumeActivity方法,这个方法看名字肯定是回调onResume方法的入口的,那么我们还是跟进去瞧瞧。

复制代码
public final ActivityClientRecord performResumeActivity(IBinder token,
        boolean clearHide) {
    ActivityClientRecord r = mActivities.get(token);
    if (localLOGV) Slog.v(TAG, "Performing resume of " + r
            + " finished=" + r.activity.mFinished);
    if (r != null && !r.activity.mFinished) {
    //代码省略
            r.activity.performResume();

    //代码省略

    return r;
}
复制代码

可以看到r.activity.performResume()这行代码,跟进 performResume方法,如下:

复制代码
final void performResume() {
    performRestart();

    mFragments.execPendingActions();

    mLastNonConfigurationInstances = null;

    mCalled = false;
    // mResumed is set by the instrumentation
    mInstrumentation.callActivityOnResume(this);

    //代码省略

}
复制代码

Instrumentation调用了callActivityOnResume方法,callActivityOnResume源码如下:

复制代码
public void callActivityOnResume(Activity activity) {
    activity.mResumed = true;
    activity.onResume();

    if (mActivityMonitors != null) {
        synchronized (mSync) {
            final int N = mActivityMonitors.size();
            for (int i=0; i<N; i++) {
                final ActivityMonitor am = mActivityMonitors.get(i);
                am.match(activity, activity, activity.getIntent());
            }
        }
    }
}
复制代码

找到了,activity.onResume()。这也证实了,performResumeActivity方法确实是回调onResume方法的入口。

那么现在我们看回来handleResumeActivity方法,执行完performResumeActivity方法回调了onResume方法后, 
会来到这一块代码:

r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
    r.activity.makeVisible();
}

activity调用了makeVisible方法,这应该是让什么显示的吧,跟进去探探。

复制代码
void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}
复制代码

往WindowManager中添加DecorView,那现在应该关注的就是WindowManager的addView方法了。而WindowManager是一个接口来的,我们应该找到WindowManager的实现类才行,而WindowManager的实现类是WindowManagerImpl。这个和ViewRoot是一样,就是名字多了个impl。

找到了WindowManagerImpl的addView方法,如下:

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

里面调用了WindowManagerGlobal的addView方法,那现在就锁定 
WindowManagerGlobal的addView方法:

复制代码
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {

    //代码省略  


    ViewRootImpl root;
    View panelParentView = null;

    //代码省略

        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;
    }
}
复制代码

终于击破,ViewRootImpl是在WindowManagerGlobal的addView方法中创建的。

回顾前面的分析,总结一下: 
ViewRootImpl的创建在onResume方法回调之后,而我们一开篇是在onCreate方法中创建了子线程并访问UI,在那个时刻,ViewRootImpl是没有创建的,无法检测当前线程是否是UI线程,所以程序没有崩溃一样能跑起来,而之后修改了程序,让线程休眠了200毫秒后,程序就崩了。很明显200毫秒后ViewRootImpl已经创建了,可以执行checkThread方法检查当前线程。

这篇博客的分析如题目一样,Android中子线程真的不能更新UI吗?在onCreate方法中创建的子线程访问UI是一种极端的情况,这个不仔细分析源码是不知道的。我是最近看了一个面试题,才发现这个。

从中我也学习到了从异常信息中跟进源码寻找答案,你呢?

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Bill_xiao/article/details/64441487

智能推荐

如何查看网页端所占的内存大小_f12怎么查看内存_Jason先生的博客-程序员宅基地

文章浏览阅读6k次,点赞2次,收藏9次。在做测试的过程中,有时需要查看某个网页的内存占用情况,有以下两个方式可以查看内存大小:1、查看js代码运行时所产生的内存直接在谷歌浏览器中打开F12调试模式,在Memory页签下点击“Take heap snapshot”来查看当前网页的js代码所占内存其中, Shallow Size是对象本身占据的内存的大小,不包含其引用的对象。对于常规对象(非数组)的Shallow Size由其成员变量的数量和类型来定,而数组的ShallowSize由数组类型和数组长度来决定,它为数组元素大小的总和;Reta_f12怎么查看内存

kylin:构建cube第一步报错 求解救(刚开始学习)_java.io.ioexception:os command error exit with ret_...半個好人!的博客-程序员宅基地

文章浏览阅读2.2k次。java.io.IOException: OS command error exit with return code: 1, error message: SLF4J: Class path contains multiple SLF4J bindings.SLF4J: Found binding in [jar:file:/opt/apps/hbase/lib/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]SLF._java.io.ioexception:os command error exit with return code :1,error message:

Java -- 你掌握的线程拿得出手吗?_拿得出手的java项目-程序员宅基地

文章浏览阅读384次。【导入】【目录】一、进程1、概念2、特点二、线程1、概念2、进程和线程的关系3、多线程特性3.1 随机性3.2 线程状态三、多线程创建1、继承Thread1.1 概述1.2 创建对象1.3 常用方法1.4 测试案例2、实现Runnable接口2.1 概述2.2 常用方法2.2 测试案例四、售票案例方案1:继承Thr..._拿得出手的java项目

Nike Lebron 10 Peru Primera Division Wrap- Real Garcilaso seal top spot-程序员宅基地

文章浏览阅读131次。Peru Primera Division Wrap: Real Garcilaso seal top spot Real Garcilaso will enter the Peru Primera Divi...

DataCamp “Data Scientist with Python track” 第六章 Importing Data in Python (Part 2) 学习笔记-程序员宅基地

文章浏览阅读349次。Performing HTTP requests in Python using urllib终于到了从网页上爬数据的部分,与前面一样,这里我们先与HTTP建立连接并发送请求(前往website)*注意最后关闭连接# Import packagesfrom urllib.request import urlopen, Request# Specify the urlurl...

module ‘yaml‘ has no attribute ‘FullLoader‘_module 'yaml' has no attribute 'fullloader-程序员宅基地

文章浏览阅读5.2k次。module 'yaml' has no attribute 'FullLoader'_module 'yaml' has no attribute 'fullloader

随便推点

[LCT] BZOJ2002: [Hnoi2010]Bounce 弹飞绵羊-程序员宅基地

文章浏览阅读493次。题意一条直线摆上n个装置,每个装置有个弹力系数ki,当绵羊达到第i个装置时,它会往后弹到第i+ki个装置,若不存在第i+ki个装置,则绵羊被弹飞。 绵羊想知道当它从第i个装置起步时,被弹几次后会被弹飞。 你需要执行m次操作,有两种类型: 1.修改某装置的弹力系数(ki始终为正整数) 2.询问从i起步被弹几次后会被弹飞 n,m<=200000题解算是LCT模板题吧。 添加一个点表示“弹飞”_bzoj2002

国家计算机技术与软件专业技术资格(水平)考试与职称有何对应关系?_软考和计算机等级考试职称-程序员宅基地

文章浏览阅读4.2k次。14、国家计算机技术与软件专业技术资格(水平)考试与职称有何对应关系?国家对计算机技术与软件专业人员实行统一考试的办法。国家计算机与软件资格(水平)考试的专业类别、资格名称和对应的级别见下表: 计算机软件计算机网络_软考和计算机等级考试职称

小程序、H5登录授权、分享、支付流程_微信小程序与h5 如何做权限设计-程序员宅基地

文章浏览阅读2.9k次。微信登录、分享、支付流程[TOC]前言对于前端来说,微信的支付、分享、登录是一定要掌握的,今天这篇文章,主要对这三方面的流程进行详细的介绍。主要内容如下:域名相关知识介绍业务域名:在微信浏览器中点击文本框,会弹出提示该网站不安全,请不要输入密码的提示,通过配置业务域名可以解决这个问题。JS接口安全域名:分享功能(js-sdk)时需要试用这个域名。网页授权域名:用于获取用户..._微信小程序与h5 如何做权限设计

word字体大小与公式编辑器字体对照表-程序员宅基地

文章浏览阅读1.2k次。字体大小对照表如下初号44pt小初36pt一号26pt小一24pt二号22pt小二18pt三号16pt小三15pt四号14pt小四12pt五号10.5pt小五9pt六号7.5pt小六6.5pt七号5.5pt八号5pt修改方法:选中公式双击进到公式编辑器里,点尺寸------其他----------输入换算后的多少pt就ok了。...

众为兴机械手与上位机交互调试-程序员宅基地

文章浏览阅读2.8k次,点赞3次,收藏13次。示教器的使用界面及按钮含义点击手动界面上方倍率,改变速度倍率导入程序 可以插入U盘可以在我的电脑输入ftp://192.168.0.123,将文件拖入PROJECT 点击左上方小黄人点击CPU#1导入程序 示教器点击刷新即可LUA编程软件使用新建文件:点击文件,新建,输入function main()endCtrl+s 保存文件,用数字或者字母命名文件名,将后缀名改为.AR,机械手才可识别文件程序编写:1.local 定义局部变量定义输入信号,输出信号,报警信号,点位

记录vscode常用插件_vscode console.log插件_阿仁_清风徐来的博客-程序员宅基地

文章浏览阅读1.7k次。vscode的常用插件_vscode console.log插件