当前位置: 代码迷 >> Android >> android 不能在子线程中更新ui的议论和分析
  详细解决方案

android 不能在子线程中更新ui的议论和分析

热度:139   发布时间:2016-04-24 11:46:33.0
android 不能在子线程中更新ui的讨论和分析

问题描述

  做过android开发基本都遇见过ViewRootImpl$CalledFromWrongThreadException,上网一查,得到结果基本都是只能在主线程中更改ui,子线程要修改ui只能post到主线程或者使用handler之类。但是仔细看看exception的描述并不是这样的,“Only the original thread that created a view hierarchy can touch its views”,只有创建该 view 布局层次的原始线程才能够修改其所属view的布局属性,所以“只能在主线程中更改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:6498)at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:954)at android.view.ViewGroup.invalidateChild(ViewGroup.java:4643)at android.view.View.invalidateInternal(View.java:11775)at android.view.View.invalidate(View.java:11739)at android.view.View.invalidate(View.java:11723)at android.widget.TextView.checkForRelayout(TextView.java:7002)at android.widget.TextView.setText(TextView.java:4073)at android.widget.TextView.setText(TextView.java:3931)at android.widget.TextView.setText(TextView.java:3906)at com.android.sample.HomeTestActivity$1.run(HomeTestActivity.java:114)at java.lang.Thread.run(Thread.java:818)

问题分析

  我们根据 exception 的StackTrace信息,了解一下源码,以setText为例,如果 textview 已经被绘制出来了,调用setText函数,会调用到View的invalidate函数,其中又会调用到invalidateInternal函数,接着调用到parent.invalidateChildInParent函数,其中parent对象就是父控件ViewGroup,最后会调用到ViewRootImpl的invalidateChildInParent函数,为什么最后会调用到ViewRootImpl类中呢,这里就需要说到布局的创建过程了:

Activity的启动和布局创建过程

  先分析一下Activity启动过程,startActivity和startActivityForResult函数用来启动一个activity,最后他们最终都会调用到一个函数
public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options)
中,接着函数中会调用Instrumentation的execStartActivity方法,该函数中会调用ActivityManagerNative.getDefault().startActivity方法,ActivityManagerNative类的定义
public abstract class ActivityManagerNative extends Binder implements IActivityManager,该类继承自Binder并实现了IActivityManager这个接口,IActivityManager继承自IInterface接口,用过AIDL的应该知道,基本和这个结构相似,所以肯定是用来跨进程通信的,ActivityManagerService 类也是继承自 ActivityManagerNative接口,因此ActivityManagerService也是一个Binder,他是IActivityManager接口的具体实现类,getDefault函数是通过一个Singleton对象对外提供,他最后返回的是ActivityManagerService的Binder对象,所以startActivity方法最终实现是在ActivityManagerService类中,接着进行完一系列的操作之后会会调到IApplicationThread中,这个类也是一个继承自IInterface的Binder类型接口,ApplicationThreadNative虚类继承自该接口,在该类中的onTransact函数中,根据code不同会进行不同的操作,最后ActivityThread类的内部类ApplicationThread继承自ApplicationThreadNative类,最终的实现者就是ApplicationThread类,在ApplicationThreadNative中根据code进行不同操作的实现代码都在ApplicationThread类中,最后会回调到ApplicationThread类中的scheduleLaunchActivity方法:

@Overridepublic final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,                                         ActivityInfo info, Configuration curConfig, Configuration overrideConfig,                                         CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,                                         int procState, Bundle state, PersistableBundle persistentState,                                         List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,                                         boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {    updateProcessState(procState, false);    ActivityClientRecord r = new ActivityClientRecord();    ....    sendMessage(H.LAUNCH_ACTIVITY, r);}

最终给H这个Handler类发送了一个message,其中调用了的handleLaunchActivity方法,这个方法通过performLaunchActivity方法获取到一个Activity对象,在performLaunchActivity函数中会调用该activity的attach方法,这个方法把一个ContextImpl对象attach到了Activity中,非常典型的装饰者模式:

final void attach(Context context, ActivityThread aThread,                  Instrumentation instr, IBinder token, int ident,                  Application application, Intent intent, ActivityInfo info,                  CharSequence title, Activity parent, String id,                  NonConfigurationInstances lastNonConfigurationInstances,                  Configuration config, String referrer, IVoiceInteractor voiceInteractor) {    attachBaseContext(context);    mFragments.attachHost(null /*parent*/);    mWindow = new PhoneWindow(this);    mWindow.setCallback(this);    mWindow.setOnWindowDismissedCallback(this);    mWindow.getLayoutInflater().setPrivateFactory(this);    if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {        mWindow.setSoftInputMode(info.softInputMode);    }    if (info.uiOptions != 0) {        mWindow.setUiOptions(info.uiOptions);    }    mUiThread = Thread.currentThread();    ....    mLastNonConfigurationInstances = lastNonConfigurationInstances;    if (voiceInteractor != null) {        if (lastNonConfigurationInstances != null) {            mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;        } else {            mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,                    Looper.myLooper());        }    }    mWindow.setWindowManager(            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),            mToken, mComponent.flattenToString(),            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);    if (mParent != null) {        mWindow.setContainer(mParent.getWindow());    }    mWindowManager = mWindow.getWindowManager();    mCurrentConfig = config;}

window是通过下面方法获取的mWindow = new PhoneWindow(this),创建完Window之后,Activity会为该Window设置回调,Window接收到外界状态改变时就会回调到Activity中。在activity中会调用setContentView()函数,它是调用 window.setContentView()完成的,最终的具体操作是在PhoneWindow中,PhoneWindow的setContentView方法第一步会检测DecorView是否存在,如果不存在,就会调用generateDecor函数直接创建一个DecorView;第二步就是将Activity的视图添加到DecorView的mContentParent中;第三步是回调Activity中的onContentChanged方法通知Activity视图已经发生改变。

public void setContentView(View view, ViewGroup.LayoutParams params) {    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window    // decor, when theme attributes and the like are crystalized. Do not check the feature    // before this happens.    if (mContentParent == null) {        installDecor();    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {        mContentParent.removeAllViews();    }    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {        view.setLayoutParams(params);        final Scene newScene = new Scene(mContentParent, view);        transitionTo(newScene);    } else {        mContentParent.addView(view, params);    }    mContentParent.requestApplyInsets();    final Window.Callback cb = getCallback();    if (cb != null && !isDestroyed()) {        cb.onContentChanged();    }}

这些步骤完成之后,DecorView还没有被WindowManager正式添加到Window中,接着会调用到ActivityThread类的handleResumeActivity方法将顶层视图DecorView添加到PhoneWindow窗口,Activity的视图才能被用户看到:

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {    .....    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);    }    .....}

DecorView和Window的关系代码中已经很清楚了,接下来分析一下addView方法,WindowManager接口继承自ViewManager接口,最终实现类是WindowManagerImpl类,该类并没有直接实现Window的三大操作,而是全部交给了WindowManagerGlobal来处理,WindowManagerGlobal以工厂的形式向外提供自己的实例,在WindowManagerImpl中有如下一段代码:private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getinstance()。WindowManagerImpl这种工作模式是典型的桥接模式,将所有的操作全部委托给WindowManagerGlobal来实现,WindowManagerGlobal的addView函数中创建了一个ViewRootImpl对象root,然后调用ViewRootImpl类中的setView成员方法:

ViewRootImpl root;View panelParentView = null;synchronized (mLock) {    .....    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 thingstry {    root.setView(view, wparams, panelParentView);} catch (RuntimeException e) {    ....}

setView方法完成了三件事情,将外部参数DecorView赋值给mView成员变量、标记DecorView已添加到ViewRootImpl、调用requestLayout方法请求布局,那么继续跟踪代码到 requestLayout()方法:

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

scheduleTraversals函数实际是View绘制的入口,该方法会通过WindowSession使用IPC方式调用WindowManagerService中的相关方法去添加窗口,scheduleTraversals函数最后会调用到doTraversal方法,doTraversal方法又调用performTraversals函数,performTraversals函数就非常熟悉了,他会去调用performMeasure,performLayout和performDraw函数去进行view的计算和绘制,接下来的操作我就不说了,推荐一篇非常好的博客:http://blog.csdn.net/jacklam200/article/details/50039189,讲的真的很详细,或者可以看看这个英文资料Android Graphics Architecture。
  回到“为什么最后会调用到ViewRootImpl类中”这个问题,从上面可以理解到,每个Window都对应着一个View和一个ViewRootImpl,Window和View是通过ViewRootImpl来建立关联的,所以invalidateChildInParent会一直while循环直到调用到ViewRootImpl的invalidateChildInParent函数中:

do {    View view = null;    if (parent instanceof View) {        view = (View) parent;    }    if (drawAnimation) {        if (view != null) {            view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;        } else if (parent instanceof ViewRootImpl) {            ((ViewRootImpl) parent).mIsAnimating = true;        }    }    ....    parent = parent.invalidateChildInParent(location, dirty);    ....} while (parent != null);

  这个问题就差不多清楚了,其他的可以再看看老罗的博客:http://blog.csdn.net/luoshengyang/article/details/8223770。

主线程与子线程ui讨论

  上面分析了activity的启动和布局创建过程,其中知道activity的创建需要新建一个ViewRootImpl对象,看看ViewRootImpl的构造函数:

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

在初始化一个ViewRootImpl函数的时候,会调用native方法,获取到该线程对象mThread,接着setText函数会调用到requestLayout方法(TextView绘制出来之后,调用setText才会去调用requestLayout方法,没有绘制出来之前,在子线程中调用setText是不会抛出Exception):

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

所以现在“不能在子线程中更新ui”的问题已经很清楚了,不管startActivity函数调用在什么线程,ActivityThread是运行在主线程中的:

/** * This manages the execution of the main thread in an * application process, scheduling and executing activities, * broadcasts, and other operations on it as the activity * manager requests. */public final class ActivityThread {....}

所以ViewRootImpl对象的创建也是在主线程中,所以一个Activity的对应ViewRootImpl对象中的mThread一定是代表主线程,这也就是“为什么不能在子线程中操作UI的”答案的解释,问题解决!!!
  但是不是说这个答案不严谨么?是的,可不可以在子线程中添加Window,并且创建ViewRootImpl呢?当然可以,在子线程中创建一个Window就可以,思路是在子线程中调用WindowManager添加一个view,类似于

windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);WindowManager.LayoutParams params = new WindowManager.LayoutParams();params.width = WindowManager.LayoutParams.MATCH_PARENT;params.height = WindowManager.LayoutParams.MATCH_PARENT;params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;params.format = PixelFormat.TRANSPARENT;params.gravity = Gravity.CENTER;params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;....windowManager.addView(v, params);

android WindowManager解析与骗取QQ密码案例分析博客中介绍到activity和dialog不是系统层级的window,可以使用WindowManager添加自定义的系统window,那么问题又来了,系统级别window是怎么添加的呢,老罗的另一篇博客http://blog.csdn.net/luoshengyang/article/details/8498908中介绍到: “对于非输入法窗口、非壁纸窗口以及非Activity窗口来说,它们所对应的WindowToken对象是在它们增加到WindowManagerService服务的时候创建的……如果参数attrs所描述的一个WindowManager.LayoutParams对象的成员变量token所指向的一个IBinder接口在WindowManagerService类的成员变量mTokenMap所描述的一个HashMap中没有一个对应的WindowToken对象,并且该WindowManager.LayoutParams对象的成员变量type的值不等于TYPE_INPUT_METHOD、TYPE_WALLPAPER,以及不在FIRST_APPLICATION_WINDOW和LAST_APPLICATION_WINDOW,那么就意味着这时候要增加的窗口就既不是输入法窗口,也不是壁纸窗口和Activity窗口,因此,就需要以参数attrs所描述的一个WindowManager.LayoutParams对象的成员变量token所指向的一个IBinder接口为参数来创建一个WindowToken对象,并且将该WindowToken对象保存在WindowManagerService类的成员变量mTokenMap和mTokenList中。”。
  了解上面之后,换一种思路,就可以在子线程中创建view并且添加到windowManager中。

实现

  有了思路之后,既可以来实现相关代码了:

new Thread(new Runnable() {    @Override    public void run() {        showWindow();    }}).start();......private void showWindow(){    windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);    WindowManager.LayoutParams params = new WindowManager.LayoutParams();    params.width = WindowManager.LayoutParams.MATCH_PARENT;    params.height = WindowManager.LayoutParams.MATCH_PARENT;    params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;    params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;    params.format = PixelFormat.TRANSPARENT;    params.gravity = Gravity.CENTER;    params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;    LayoutInflater inflater = LayoutInflater.from(this);    v = (RelativeLayoutWithKeyDetect) inflater.inflate(R.layout.window, null);    .....    windowManager.addView(v, params);}

  运行一下,报错:

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()at android.os.Handler.<init>(Handler.java:200)at android.os.Handler.<init>(Handler.java:114)at android.view.ViewRootImpl$ViewRootHandler.<init>(ViewRootImpl.java:3185)at android.view.ViewRootImpl.<init>(ViewRootImpl.java:3483)at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:261)at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)at com.android.grabqqpwd.BackgroundDetectService.showWindow(BackgroundDetectService.java:208)at com.android.grabqqpwd.BackgroundDetectService.access$100(BackgroundDetectService.java:39)at com.android.grabqqpwd.BackgroundDetectService$1.run(BackgroundDetectService.java:67)at java.lang.Thread.run(Thread.java:818)

这是因为ViewRootImpl类内部会新建一个ViewRootHandler类型的mHandler用来处理相关信息,所以如果线程没有Looper是会报错的,所以添加Looper,修改代码:

new Thread(new Runnable() {    @Override    public void run() {        Looper.prepare();        showWindow();        handler = new Handler(){            @Override            public void dispatchMessage(Message msg) {                Looper.myLooper().quit();                L.e("quit");            }        };        Looper.loop();    }}).start();

创建Looper之后,需要在必要时候调用quit函数将其退出。这样就成功显示了
这里写图片描述
而且创建之后的view只能在子线程中修改,不能在主线程中修改,要不然会抛出最开始的ViewRootImpl$CalledFromWrongThreadException,OK,就解释到这了,有什么问题,随时联系小弟~~

1楼wen_wxpk3天前 19:16
牛逼,但感觉是鸡肋,有点像脱裤子放屁。。
Re: zhao_zepeng3天前 19:27
回复wen_wxpkn哈哈哈哈,知识讨论可行性而已,不是讨论的实用性
Re: wen_wxpk3天前 08:30
回复zhao_zepeng嗯,写得不错,顶一个!
Re: zhao_zepeng前天 17:27
回复wen_wxpkn谢谢
  相关解决方案