专栏首页面试系列(java&Android)面试官问我:Andriod为什么不能在子线程更新UI?
原创

面试官问我:Andriod为什么不能在子线程更新UI?

记得看文章三部曲,点赞,评论,转发。 微信搜索【程序员小安】关注还在移动开发领域苟活的大龄程序员,“面试系列”文章将在公众号同步发布。

1.前言

看完《你为什么在现在的公司不离职?》,很多同学踏上了面试之路,作为颜值担当的天才少年_也开始了面试之路。

2.正文

天才少年_来到一家公司等待面试中。。。 一个眼睛又大又亮的小姐姐,萌萌的站在我去 的面前。 你像一片轻柔的云在我眼前飘来飘去,你清丽秀雅的脸上荡漾着春天般美丽的笑容,我连我们孩子的名字都起好了。等等,我tm不是来面试的吗?

小伙子,听说你是来面试的,我是今天的面试官,你先介绍一下你自己吧。

我叫【天才少年_】,男,30未婚,家里有车有房,我的优点是英俊潇洒,我的座右铭是:既往不纠结,纵情向前看,继续努力。

额,你这介绍,怎么感觉是来相亲的。

果然面试官已经被我英俊的外表深深吸引,不能自拔,嗯,萌萌的外表都是不太聪明的样子,今天面试有希望啦,我心中一阵暗喜。

Android消息处理机制(Handler、Looper、MessageQueue与Message)已经被问烂了,那我们今天来谈谈为什么需要主线程更新UI,子线程不能更新UI?

卧槽,不按套路出牌啊,果然漂亮的女人都难搞定。

1)首先,并非在子线程里面更新UI就一定有问题,如下所示的代码,则可以完美更新UI。

 @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        init();
        new Thread(new Runnable() {
            @Override
            public void run() {
                tv_sport_mile.setText("测试界面更新");
            }
        }).start();
    }

但是,如果我们让线程等待2秒后再更新UI,则会发生报错,代码如下所示:

 @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        init();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tv_sport_mile.setText("测试界面更新");
            }
        }).start();
    }

异常报错日志如下图所示:

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

为什么在onActivityCreated方法里面可以实现子线程更新UI,但是线程等待两秒后就异常呢?

你要是不傻,你就知道,肯定是刷新线程判断时机的原因,当时这是我的心理想法,脑子里说不要,嘴上还是很真诚的。

从at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7021)的报错可以看到是在ViewRootIml类的checkThread方法中出现异常,多说无益,开启撸源码:

我们首先看ViewRootImpl源码中的requestLayout()和checkThread()方法:

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

view的绘制流程是从scheduleTraversals()方法开始的,包括很多面试官喜欢问的onMeasure、onLayout、onDraw都是由该方法发起的。而在调用scheduleTraversals()方法前,调用了checkThread()方法,该方法会检查当前线程是否为主线程。

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

如果当前线程不是主线程,则抛出异常Only the original thread that created a view hierarchy can touch its views,跟我们的异常一直吻合。总结一下就是在刷新页面前会判断当前是否在主线程,如果不在主线程则抛异常,所以我们开始学Android的时候,别人就告诉我们:更新UI一定要在主线程。

那为什么上面第一次没有线程等待的时候没有报错呢?可以讲讲吗?

我想...大概,可能是ViewRootImp还没有创建出来吧,所以没有走到checkThread()方法。

ViewRootImp什么时候创建的,在onActivityCreated方法后面吗?

我想起了那个风黑夜高的晚上,我跟小韩(我们部门的程序媛)干着羞羞的事情,嘿嘿~~

不对,是一起加班看源码的经历,我努力回忆着ViewRootImp的创建过程。

从ActivityThread源码开始,找到handleResumeActivity()方法:

 final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
       ...
        mSomeActivitiesChanged = true;

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

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

            if (localLOGV) Slog.v(
                TAG, "Resume " + r + " started activity: " +
                a.mStartedActivity + ", hideForNow: " + r.hideForNow
                + ", finished: " + a.mFinished);

            final int forwardBit = isForward ?
                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;

            // If the window hasn't yet been added to the window manager,
            // and this guy didn't finish itself or start another activity,
            // then go ahead and add the window.
            boolean willBeVisible = !a.mStartedActivity;
            if (!willBeVisible) {
                try {
                    willBeVisible = ActivityManager.getService().willActivityBeVisible(
                            a.getActivityToken());
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            ...
                r.activity.mVisibleFromServer = true;
                mNumVisibleActivities++;
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }
            ...
        }
    }

从上面的代码可以看到,调用r.activity.makeVisible();我们看下Activity的makeVisible()的处理逻辑

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

通过上面的方法可以看到,makeVisible调用了WindowManager的addView方法,WindowManager是个接口,他的具体实现类是WindowManagerImp,直接看WindowManagerImp的addView()方法:

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

mGlobal是WindowManagerGlobal对象,即调用了WindowManagerGlobal的addView方法,继续深入,快乐继续。

 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
       ...
            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.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

这边可以看到创建ViewRootImpl对象,后面View的刷新正是通过ViewRootImpl实现的,由于你面试官没有问,这边不展开讨论,不然把我留到天黑,面试官可能有危险,嘿嘿。

赠送一个知识点:真正把mDecor加到WindowManager上是并显示出来在makeVisible()方法中实现的,Activity的Window才能正在被使用。

小伙子理解讲得还不错哦 那ViewRootImp是在onActivityCreated方法后面创建的吗?

看来面试官小姐姐还是没有忘记这个问题,我们回过头来看handleResumeActivity()

 final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
       ...
        mSomeActivitiesChanged = true;

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

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

            if (localLOGV) Slog.v(
                TAG, "Resume " + r + " started activity: " +
                a.mStartedActivity + ", hideForNow: " + r.hideForNow
                + ", finished: " + a.mFinished);

            final int forwardBit = isForward ?
                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;

            // If the window hasn't yet been added to the window manager,
            // and this guy didn't finish itself or start another activity,
            // then go ahead and add the window.
            boolean willBeVisible = !a.mStartedActivity;
            if (!willBeVisible) {
                try {
                    willBeVisible = ActivityManager.getService().willActivityBeVisible(
                            a.getActivityToken());
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            ...
                r.activity.mVisibleFromServer = true;
                mNumVisibleActivities++;
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }
            ...
        }
    }

可以看到里面调用了performResumeActivity()方法,继续跟到performResumeActivity()方法体:

 public final ActivityClientRecord performResumeActivity(IBinder token,
            boolean clearHide, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        if (localLOGV) Slog.v(TAG, "Performing resume of " + r
                + " finished=" + r.activity.mFinished);
        ...
                r.activity.performResume();

                synchronized (mResourcesManager) {
                    // If there is a pending local relaunch that was requested when the activity was
                    // paused, it will put the activity into paused state when it finally happens.
                    // Since the activity resumed before being relaunched, we don't want that to
                    // happen, so we need to clear the request to relaunch paused.
                    for (int i = mRelaunchingActivities.size() - 1; i >= 0; i--) {
                        final ActivityClientRecord relaunching = mRelaunchingActivities.get(i);
                        if (relaunching.token == r.token
                                && relaunching.onlyLocalRequest && relaunching.startsNotResumed) {
                            relaunching.startsNotResumed = false;
                        }
                    }
                }

               ...
            }
        }
        return r;
    }

performResumeActivity()方法调用了r.activity.performResume(),我们继续看Activity的performResume()的源码,再次深入,再次快乐。

final void performResume() {
       ...

        mCalled = false;
        // mResumed is set by the instrumentation
        mInstrumentation.callActivityOnResume(this);
        if (!mCalled) {
            throw new SuperNotCalledException(
                "Activity " + mComponent.toShortString() +
                " did not call through to super.onResume()");
        }

       ...
    }

然后又调用了Instrumentation的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());
                }
            }
        }
    }

可以看到callActivityOnResume()方法调用了activity.onResume(),即回调到Activity的onResume()方法,综合上面的分析可以得出:ViewRootImpl是在Activity的OnResume()方法后面创建出来的。

到这里可以事后一支烟了,不是,是总结一下了:

1)ViewRootImpl是在Activity的onResume()方法后面创建出来的,所以在onResume之前的UI更新可以在子线程操作而不报错,因为这个时候ViewRootImpl还没有创建,没有执行checkThread()方法。

2)安卓系统中,操作viwe对象没有加锁,所以如果在子线程中更新UI,会出现多线程并发的问题,导致页面展示异常。

小伙子分析得很不错,把我打动了,回去等offer吧。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 大厂面试必备之设计模式:漫画单例模式

    饿汉模式中的类实例是当类被加载时就被初始化出来的,所以在应用初始化时,会占用不必要的内存。同时,由于该实例在类被加载的时候就创建出来了,所以他是线程安全的。因为...

    天才少年
  • 大厂面试必备之设计模式:漫画策略模式

    官方定义不太好理解,我翻译一下,在策略模式中,会针对一个行为(比如支付),定义多个实现类,每个类都封装具体的实现算法,并且为了保证他们是同一行为,通常这些实现类...

    天才少年
  • SpringMVC 解毒4

    ViewResolver 视图解析器能够通过viewName视图名和locale用户区域获取View对象,从而渲染视图。 如果你想使用自定义的一个模板引擎,就得...

    zhangheng
  • log4go源码分析(一)

    一直想研究分享几个开源项目提高提高,但由于工作忙一直没时间。今天把业余时间总结的log4go源码分析的第一篇记录下来。

    特立独行的猫a
  • 数据插入失败引发的主键auto_increment问题

    Marser
  • Shiro 修改权限,刷新权限

    结合缓存框架,第一次查询权限信息后缓存,在缓存生存期内,访问鉴权使用缓存中的权限信息。

    WindWant
  • C语言入门系列之8.指针的概念与应用

    指针是C语言中的一个重要的概念,也是C语言的一个重要特色。 正确而灵活地运用它,可以有效地表示复杂的数据结构;能动态分配内存;能方便地使用字符串;有效而方便地...

    cutercorley
  • Leetcode 20. Valid Parentheses

    版权声明:博客文章都是作者辛苦整理的,转载请注明出处,谢谢! https://blog.csdn....

    Tyan
  • 给Angular应用增添搜索Search功能

    (1) dashboard Component里增添搜索Component的selector:

    Jerry Wang
  • *Android面试实战总结2

     接上一篇 往下写 http://blog.csdn.net/u011733020/article/details/45998861  ,   非常感谢在上...

    bear_fish

扫码关注云+社区

领取腾讯云代金券