精华内容
下载资源
问答
  • 那么我在想,为什么不能,会产生什么问题; 是否真的就一定不能在子线程更新UI; 2、能否在子线程中更新UI 答案是可以的,比如以下代码: @Override protected void onCreate(Bundle savedInstanceState) { ...

    1、前言

    • 众所周知在Android中,子线程是不能更新UI的;
    • 那么我在想,为什么不能,会产生什么问题;
    • 是否真的就一定不能在子线程更新UI;

    2、能否在子线程中更新UI

    答案是可以的,比如以下代码:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            tv = findViewById(R.id.tv);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    tv.setText("测试是否报出异常");
                }
            }).start();
        }
    

    运行结果并无异常,可以正常的在子线程中更新了TextView控件;假如让线程休眠1000ms,就会发生错误:

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

    这句话的意思是只有创建视图层次结构的原始线程才能更新这个视图,也就是说只有主线程才有权力去更新UI,其他线程会直接抛异常的;
    at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7905)的异常路径可以看到抛出异常的最终在ViewRootImlcheckThread方法里,ViewRootImlView的根类,其控制着View的测量、绘制等操作,那么现在我们转到ViewRootImpl.java源码观察:

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

    scheduleTraversals()里是对View进行绘制操作,而在绘制之前都会检查当前线程是否为主线程mThread,如果不是主线程,就抛出异常;这样做法就限制了开发者在子线程中更新UI的操作;
    但是为什么最开始的在onCreate()里子线程对UI的操作没有报错呢,可以设想一下是因为ViewRootImp此时还没有创建,还未进行当前线程的判断;
    现在,我们寻找ViewRootImp在何时创建,从Activity启动过程中寻找源码,通过分析可以查看ActivityThread.java源码,并找到handleResumeActivity方法:

    final void handleResumeActivity(IBinder token,boolean clearHide, boolean isForward, boolean reallyResume) {
            ···
            // TODO Push resumeArgs into the activity for consideration
            ActivityClientRecord r = performResumeActivity(token, clearHide);
            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);
                    }
    
                } else if (!willBeVisible) {
                    if (localLOGV) Slog.v(
                        TAG, "Launch " + r + " mStartedActivity set");
                    r.hideForNow = true;
                }
            ···
    }
    

    内部调用了performResumeActivity方法:
    ···
    public final ActivityClientRecord performResumeActivity(IBinder token,
    boolean clearHide) {
    if (r != null && !r.activity.mFinished) { r.activity.performResume(); } catch (Exception e) { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( "Unable to resume activity " + r.intent.getComponent().toShortString() + ": " + e.toString(), e); } } } return r; } ··· 在内部调用了ActivityperformResume方法,可以肯定应该是要回调生命周期的onResume```方法了:

    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()");
            }
            ···
        }
    

    ,然后又调用了InstrumentationcallActivityOnResume方法:

    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()方法,也就是回调了Activity生命周期的onResume方法;现在让我们回头看看handleResumeActivity方法,会执行这段代码:

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

    在内部调用了ActivitymakeVisible方法:

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

    内部调用了WindowManageraddView方法,而WindowManager方法的实现类是WindowManagerImp类,直接找WindowManagerImpaddView方法:

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

    然后又调用了WindowManagerGlobaladdView方法:

    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);
            }
            ···
        }
    

    在该方法中,终于看到了ViewRootImpl的创建;

    结论:从以上的源码分析可得知,ViewRootImpl对象是在onResume方法回调之后才创建,那么就说明了为什么在生命周期的onCreate方法里,甚至是onResume方法里都可以实现子线程更新UI,因为此时还没有创建ViewRootImpl对象,并不会进行是否为主线程的判断;


    3、更新UI一定要在主线程实现

    谷歌提出:“一定要在主线程更新UI”,实际是为了提高界面的效率和安全性,带来更好的流畅性;反推一下,假如允许多线程更新UI,但是访问UI是没有加锁的,一旦多线程抢占了资源,那么界面将会乱套更新了,体验效果就不言而喻了;所以在Android中规定必须在主线程更新UI


    4、总结

    • 子线程可以在ViewRootImpl还没有被创建之前更新UI
    • 访问UI是没有加对象锁的,在子线程环境下更新UI,会造成不可预期的风险;
    • 开发者更新UI一定要在主线程进行操作;
    展开全文
  • 问题描述做过android开发基本都遇见过ViewRootImpl$CalledFromWrongThreadException,上网一查,得到结果基本都是只能在主线程中更改ui,子线程要修改ui只能post到主线程或者使用handler之类。但是仔细看看exception...

    问题描述

    做过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方法:

    @Override

    public 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 pendingResults, List 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 things

    try {

    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是怎么添加的呢,老罗的另一篇博客中介绍到: “对于非输入法窗口、非壁纸窗口以及非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.(Handler.java:200)

    at android.os.Handler.(Handler.java:114)

    at android.view.ViewRootImpl$ViewRootHandler.(ViewRootImpl.java:3185)

    at android.view.ViewRootImpl.(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函数将其退出。这样就成功显示了

    215148_0.png

    而且创建之后的view只能在子线程中修改,不能在主线程中修改,要不然会抛出最开始的ViewRootImpl$CalledFromWrongThreadException,OK,就解释到这了,有什么问题,随时联系小弟~~

    展开全文
  • 大家都知道,在子线程中直接更新UI就会奔溃,报错如下: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. 报错提示:...

    大家都知道,在子线程中直接更新UI就会奔溃,报错如下:
    android.view.ViewRootImpl$CalledFromWrongThreadException:
    Only the original thread that created a view hierarchy can touch its views.
    报错提示:只有创建了视图层次结构的原始线程才能访问它的视图。

    什么意思呢?就是说,ui在哪个线程创建的,就应该在哪个线程中更新。而我们的UI是在主线程中创建的,所以就应该在主线程中更新UI。这是奔溃的信息提示。

    首先,我们应该明白UI更新的原理,就是通过不断的测量、布局和绘制的一个过程。最终都会请求view的绘制操作,即requestLayout()这个方法。我们直接来看View.java的源码:
    @CallSuper
    public void requestLayout() {
    //…
    //当父视图不为空,且有请求布局,则执行父视图的requestLayout()方法
    if (mParent!=null && !mParent.isLayoutRequested()){
    mParent.requestLayout();
    }
    //…
    }

    protected ViewParent mParent; //当前视图的父视图
    //父视图的赋值方法
    void assignParent(ViewParent parent) {
    if (mParentnull) {
    mParent = parent;
    } else if (parent
    null) {
    mParent = null;
    } else {
    throw new RuntimeException(“view " + this + " being added, but”
    + " it already has a parent");
    }
    }
    从上面的源码可以看出,当请求布局时,会调用父视图mParent的requestLayout()方法,mParent是一个接口对象,这个方法是接口方法。我们得找到实现这个接口的那个类。
    我们发现,在assignParent(ViewParent parent)方法中对mParent进行了赋值,那么assignParent(ViewParent parent)在哪里被调用的呢?

    通过阅读Activity的启动过程源码,可以发现:
    1、在Activity的onCreate()方法中调用了我们熟悉的setContentView()方法,其实最终调用了顶层视图PhoneWindow的setContentView()方法,然后调用其内部的installDecor()初始化mDecor;
    2、紧跟着在handleLaunchActivity()中调用了handleResumeActivity();
    3、然后在handleResumeActivity()中可以发现调用了WindowMangerImpl的addView方法,wm.addView(decor, l),
    4、然后又调用了WindowManagerGlobal类的addView()方法,在里面可以发现又调用了root.setView(view, wparams, panelParentView),这个root是ViewRootImpl一个实例。
    然后我们直接看ViewRootImpl的setView()方法:
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
    if (mView == null) {
    //…
    view.assignParent(this);
    //…
    }
    }
    }
    由此可见,最终在ViewRootImpl的setView()方法中对前面提到的mParent进行了赋值。同时,也指明了mParent是ViewRootImpl的一个实例,实现了ViewParent接口,所以很显然了,前面提到的,调用了mParent 的requestLayout()方法,即是ViewRootImpl的requestLayout():
    ViewRootImpl.java代码如下:
    @Override
    public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) { // 1.请求更新布局
    checkThread(); // 2.首先检查当前所在线程
    //…
    }
    }

    void checkThread() {
    // 3.直接判断view所属的线程是否为当前线程,否则抛出一坨异常
    if (mThread != Thread.currentThread()) {
    throw new CalledFromWrongThreadException(
    “Only the original thread that created a view hierarchy can touch its views.”);
    }
    }
    final Thread mThread; //存放view所属的线程
    所以,在进行UI更新时,都会进行线程的检测判断。以上就是为什么不能在子线程中直接更新UI的原因原理。

    展开全文
  • 子线程不能更新UI线程总结子线程整的不能更新UI线程吗android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.有时候大家做项目的...

    子线程不能更新UI线程总结

    子线程整的不能更新UI线程吗

    android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

    有时候大家做项目的时候偶尔会碰到这个毛病。不用说大家都知道是子线程更新主线程(UI)线程的问题,一样大家也会给出相对应的解法:使用handle+Thread方法通过发送Message进行更新UI线程。

    eg:

    new Thread(new Runnable() {

    @Override

    public void run() {

    try {

    Thread.sleep(200);

    } catch (InterruptedException e) {

    e.printStackTrace();

    }

    iv_img.setBackground(ContextCompat.getDrawable(MainActivity.this,R.drawable.ic_launcher));

    btn_commit.setText("iiiiiiiiiiiiiiiiiiiii ");

    Toast.makeText(MainActivity.this,"阿斯蒂芬噶是的发送到",Toast.LENGTH_LONG).show();

    }

    }).start();

    此时就会出现1下毛病:

    ffe5b8e7691a03ef6270374f0ce95b53.png

    此时我们都知道最简单的1种解决方式就是:

    new Thread(new Runnable() {

    @Override

    public void run() {

    Message msg = handler.obtainMessage();

    msg.what = 100;

    msg.obj = "发送消息";

    handler.sendMessage(msg);

    }

    }).start();

    }

    接下来我们就来探究1下:子线程和UI线程之间更新问题。

    首先我们要知道:

    android利用程序遵守的是依照单线程模式的原则

    这是由于:Android UI操作其实不是线程安全的并且这些操作必须在UI线程中履行。

    android的UI线程就是主线程,当做粗第1次启动的时候 Android会同时启动1个对应的主线程(Main Thread),主线程主要负责处理与UI相干的事件,如:用户的按键事件,用户接触屏幕的事件和屏幕绘图事件,并把相干的事件分发到对应的组件进行处理。所以主线程通常又被叫做UI线程。这个和Java的运行机制是不同的。

    andorid在UI中进行绘图和处理事件启动1个监听的作用 ,此时就必须要在UI中时时刻刻的进行相利用户的点击事件和UI操作事件。异步操作和耗时操作需要另外气1个线程,不然UI线程在5s内未响利用户的操作,系统就会弹出弹出对话框停止程序终止进程的提示。

    我们想看看报错的这行代码在ViewRootImpl.java:

    void checkThread() {

    if (mThread != Thread.currentThread()) {

    throw new CalledFromWrongThreadException(

    "Only the original thread that created a view hierarchy can touch its views.");

    }

    }

    mThread是1个线程,如果改线程是当前的线程的时候,则继续向下走,不会抛出异常。大家可以看到,我在onCreate方法,只有1个线程,肯定是当前的线程,那为何会报错呢?我们接着往下看。

    接下来我们看看checkThread()在那几处用到了,其中有

    @Override

    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {

    checkThread();

    if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);

    .......

    invalidateRectOnScreen(dirty);

    return null;

    }

    @Override

    public void requestFitSystemWindows() {

    checkThread();

    mApplyInsetsRequested = true;

    scheduleTraversals();

    }

    @Override

    public void requestLayout() {

    if (!mHandlingLayoutInLayoutRequest) {

    checkThread();

    mLayoutRequested = true;

    scheduleTraversals();

    }

    }

    从报错的点at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:848)我们可以看到是848行是invalidateChildInParent方法里调用的。我们进1步与发现invalidateChildInParent又在invalidateChild()里调用,在View中那个地方调用了呢?我们进1步向下跟进。

    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,

    boolean fullInvalidate) {

    ......

    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);

    }

    ......

    }

    而invalidateInternal方法在invalidate()方法中是这样的

    void invalidate(boolean invalidateCache) {

    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);

    }

    到这里1目了然了。原来View中再重新回执的时候刷新方法里调用了ViewRootImpl的checkThre(),刷新是检查该线程是否是当前的线程,即主线程。

    我们根据源码可以知道invalidate()–>checkThread()是1步1步去调用的。

    android.view.View.setBackground()

    android.view.View.setBackground()—->setBackgroundDrawable()–> invalidate()–>invalidateInternal()–>invalidateChild()–>checkThread()–>invalidateRectOnScreen()

    现在我们清楚了,严格的来讲原来子线程是不能刷新UI线程的。

    解决方式:

    第1种方式:

    new Thread(new Runnable() {

    @Override

    public void run() {

    Message msg = handler.obtainMessage();

    msg.what = 100;

    msg.obj = "发送消息";

    handler.sendMessage(msg);

    }

    }).start();

    }

    第2中方式:

    runOnUiThread(new Runnable() {

    @Override

    public void run() {

    try {

    Thread.sleep(200);

    } catch (InterruptedException e) {

    e.printStackTrace();

    }

    iv_img.setBackground(ContextCompat.getDrawable(MainActivity.this,R.drawable.ic_launcher));

    }

    });

    第3种方式: 利用AsyncTask方法。

    以上都是进程间通讯的几种方式。这里我不再做过量的描写。

    如果您觉我的文章对您有所帮助,

    QQ交换群 :232203809,欢迎入群

    微信公众号:终端研发部

    (欢迎关注学习和交换)

    展开全文
  • 1. 为什么不能子线程更新UI ?一般我们在项目中,进行联网请求后,这里我们就用子线程来表示联网请求,开了线程后获取到我们服务器返回的数据后,需要去更新UI,在这里我们就需要去调用setText()、setImageView()...
  • 关于这个问题在面试的时候可能会被问到,其实在某些情况下是可以在子线程更新UI的!比如:在一个activity的xml文件中中随便写一个TextView文本控件,然后在Activity的onCreate方法中开启一个子线程并在该子线程的...
  • 今天在做练习时,在一个新开启的线程中调用“Toast.makeText(MainActivity....”报错:Can't create handler inside thread that has not called Looper.prepare()在新线程中添加Looper.prepare();和Looper.loo...
  • 作为android开发人员,总是被要求着不能子线程更新UI,必须得再主线程更新UI,由于好奇,也由于看这些源码也可以提升自己,就去查了相关资料来学习(本文是自我学习记录的文章,欢迎讨论,若有不对还麻烦指正出来...
  • Android的UI访问是没有加锁...使得我们在子线程中访问UI也可以使程序跑起来呢?接下来我们用一个例子去证实一下。 新建一个工程,activity_main.xml布局如下所示: <?xml version="1.0" encoding="utf-8"?> &
  • 使用HandlerThread获取Looper对象,后创建Handler(mHandlerThread.getLooper),此时的HandleMessage()在子线程中运行,为何此处可以更新UI?public class MainActivity extends AppCompatActivity {private TextView ...
  • Android在子线程更新UI的几种方法 方式一:Handler和Message 方式二:在子线程中直接调用Activity.runOnUiThread(Runnable action)方法 方式三:在子线程中调用View的post()方法 方式四:在子线程中调用View的...
  • 看到这个标题,好多人第一时间想到的是什么? 感兴趣的不妨跟着下面的代码看看会发生什么? 首先我在 onCreate 方法里调用 setText() 方法 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate...
  • 为什么子线程不能更新UI?是不是子线程一定可以更新UI?SurfaceView是为什么可以直接子线程绘制呢?用SurfaceView 做一个小游戏,别踩百块,so easy!今天我们来一起讨论一下这些问题,在看下面讨论时,你需要掌握...
  • 一部分搜索于u0116826736的博友;仅供参考!============================================================================当app访问数据遇到阻塞时候,...等到线程处理接受网络的数据回来后,在是有ui线程更新ui...
  • Android中的UI线程不安全的,也就是说,如果要更新应用程序里的UI 元素,则必须在主线程中进行,否则就会出现异常。在这里介绍两个方法来解决这个问题解析异步处理机制Android中的异步消息处理主要分为四个部分,...
  • 两种多线程更新UI

    2021-03-04 03:27:21
    1 AsyncTask实现的原理,和适用的优缺点AsyncTask,是android提供的轻量级的异步类,可以直接继承AsyncTask,在类中实现异步操作,并提供接口反馈当前异步执行的程度(可以通过接口实现UI进度更新),最后反馈执行的结果给UI...
  • 1、为什么说invalidate()不能直接在线程中调用?2、它是怎么违背单线程的?3、androidui为什么说不是线程安全的?4、androidui操作为什么一定要在UI线程中执行?1、为什么说invalidate()不能直接在线程中调用?答: ...
  • 有些特殊的场景,比如我们都知道,在Android开发中,子线程不能更新UI,而主线程又不能进行耗时操作,一种常用的处理方法就是,在子线程中进行耗时操作,完成之后发送消息,通知主线程更新UI。或者使用异步任务,异步...
  • 一般来讲,子线程是不能更新 UI 的,如果在子线程更新 UI,会报错。但在某种情况下直接开启线程更新 UI 是不会报错的。比如,在 onCreate 方法中,直接开启子线程更新 UI,这样是不会报错的。override fun onCreate...
  • 半天没有反映,一度觉得Toast写错了,把Toast放外面发现能够正常弹出提示的,后来查了下资料,原来okhttp的callback,onResponse其实仍是运行在子线程的,那么若是在子线程更新主线程呢。这里提供几种方法。java//...
  • AsyncTask抽象出后台线程运行的五个状态,分别是:1、准备运行,2、正在后台运行,3、进度更新,4、完成后台任务,5、取消任务,对于这五个阶段,AsyncTask提供了五个回调方法:1、准备运行:onPreExecute(),该回调...
  • 总所周知窗口UI 和 程序中生成的子线程是不同的两个线程,如果在子线程更新窗体UI 就会报错: 所以 需要解决这个问题,qt中有个叫信号的东西。可以通过信号(signal)来触发事件,传递消息 建立信号变量 //...
  • 从网上搜到的结果,做整理,以便自己日后使用。方法一:用Handler1、主线程中定义Handler:Handler mHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);...
  • 本文介绍了Android 在子线程更新UI的几种方法示例,分享给大家,具体如下:方式一:Handler和Message① 实例化一个Handler并重写handlerMessage()方法private Handler handler = newHandler() {public void ...
  • 子线程可以更新UI

    2021-02-13 01:50:34
    尝试直接在子线程更新textoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)var text: TextView = findViewById(R.id.text)Thread...
  • 1 .Handler子线程(非UI线程)调用handler对象sendMessage(msg)方法,将消息发送给关联Looper,Looper将消息存储在MessageQueue消息队列里面。然后轮巡取出MessageQueue中的消息给UI线程中handler处理,handler得到...
  • 在Android项目中经常有碰到这样的问题,在子线程中完成耗时操作之后要更新UI,下面就自己经历的一些项目总结一下更新的方法:在看方法之前需要了解一下Android中的消息机制。方法1 Activity.runOnUiThread方法如下:...
  • Android关于线程更新UI的方法在一个Android 程序开始运行的时候,会单独启动一个Process。默认的情况下,所有这个程序中的Activity或者Service(Service和 Activity只是Android提供的Components中的两种,除此之外...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 92,216
精华内容 36,886
关键字:

为什么子线程不能更新ui