为什么我们可以在非UI线程中更新UI

尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究!

炮兵镇楼

看到这样的标题……估计N多人会说我是逗比…………因为很多盆友在学习Android(特别是从4.0之后开始入门的)的时候都会常看见或听到别人说我们更新UI呢要在UI线程(或者说主线程)中去更新UI,不要在子线程中更新UI,而Android官方呢也建议我们不要在非UI线程直接更新UI,为什么呢?借助Android官方的一句话来说就是:

“The Android UI toolkit is not thread-safe and the view must always be manipulated on the UI thread.”

因此,很多童鞋会有这么一个惯性思维:在非UI线程中不能更新UI!既然Android不建议我们这么做,那其必定会对我们在code时做一些限制,比如当我们尝试运行如下代码时:

/**
 * 主界面
 *
 * @author Aige {@link http://blog.csdn.net/aigestudio}
 * @since 2014/11/17
 */
public class MainActivity extends Activity {
	private TextView tvText;

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

		tvText = (TextView) findViewById(R.id.main_tv);
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					Thread.sleep(200);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				tvText.setText("OtherThread");
			}
		}).start();
	}
}

为了把情况说明,这里我也将xml布局文件代码贴出来:

<!-- http://blog.csdn.net/aigestudio -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:background="#ffffff"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/main_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

当我们运行上述代码后,你便会在Logcat中得到如下error提示:

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

这句话非常简单,而且……我相信每个做Android开发到一定时间的盆友都碰到过,Android通过检查我们当前的线程是否为UI线程从而抛出一个自定义的AndroidRuntimeException来提醒我们“Only the original thread that created a view hierarchy can touch its views”并强制终止程序运行,具体的实现在ViewRootImpl类的checkThread方法中:

@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    // 省去海量代码…………………………

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

    // 省去巨量代码……………………
}

这就是Android在4.0后对我们做出的一个限制。写这篇Blog的具体原因来自凯子哥的一篇博文:来来来,同学,咱们讨论一下“只能在UI主线程更新View”这件小事,鉴于凯子哥如此好学,我想想呢也许很多盆友也有类似疑问:究竟TM到底能不能在非UI线程中更新UI呢?同时也为了引出我对在非UI线程更新UI方法的一些总结,我决定在3/4之前先撸一篇Blog扫清障碍先。首先,我先回答几个问题包括凯子哥的:

  1. 究竟TM到底能不能在非UI线程中更新UI呢?答案:能、当然可以
  2. View的运行和Activity的生命周期有什么必然联系吗?答案:没有、或者隐晦地说没有必然联系
  3. 除了Handler外是否还有更简便的方式在非UI线程更新UI呢?答案:有、而且还不少

OK,这里我们再来看一下上面的一段代码,在线程中我调用了Thread.sleep(200);来让我们的匿名线程暂停了200ms,如果……假如……我们去掉它的话……………………会发生什么?来试试:

/**
 * 主界面
 *
 * @author Aige {@link http://blog.csdn.net/aigestudio}
 * @since 2014/11/17
 */
public class MainActivity extends Activity {
	private TextView tvText;

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

		tvText = (TextView) findViewById(R.id.main_tv);
		new Thread(new Runnable() {
			@Override
			public void run() {
				tvText.setText("OtherThread");
			}
		}).start();
	}
}

这时你会发现我们的代码TM地正确执行了!而且我们的TextView正确显示出了“OtherThread”文本!看到屏幕上的这11个英文字母我相信大家又把刚放出来又吸进去的屁再一次地放了出来…………这就是凯子哥Blog中提到的问题,我们成功地在非UI线程中更新了UI。其实这里最最根本的原因是我们并没有checkThread我们的当前线程,而我在文章最开始的代码中通过Thread.sleep(200)暂停了一小段时间,这里为什么回暂停线程一段时间?在这段时间的背后Android背地里背着我们都干了什么?数百头母驴为何半夜惨叫?小卖部安全套为何屡遭黑手?女生宿舍内裤为何频频失窃?连环强奸母猪案,究竟是何人所为?老尼姑的门夜夜被敲,究竟是人是鬼?数百只小母狗意外身亡的背后又隐藏着什么?这一切的背后,
是人性的扭曲还是道德的沦丧?是性的爆发还是饥渴的无奈?抱歉……磕个药,上面我们讲到,我们能正确以上述代码的方式在非UI线程中更新UI而不报错,那么原因也许只有一个,那就是没有执行checkThread方法去检查我们的当前线程……但是,细看调用checkThread方法的调用方法们你就会发现,TM全是跟View创建生成相关:

也就是说一旦我们尝试去对我们的控件进行生成,这些方法其中一个必然会被调用,这时候很多朋友就会蛋疼了…………但是,请不要被checkThread方法的思维所束缚,这时候你该扩大你的思维范畴,既然checkThread方法属于ViewRootImpl的成员方法,那么会不会是此时我们的ViewRootImpl根本就没被创建呢?怀着这个出发点,我们再度审视ActivtyThread调度Activity生命周期的各个环节,首先看看performLaunchActivity方法中的处理:

public final class ActivityThread {
    // 省去海量代码…………………………

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ActivityInfo aInfo = r.activityInfo;

        // 省去对packageInfo的逻辑处理

        // 省去对ComponentName的逻辑处理

        Activity activity = null;
        try {
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();

            // 通过Instrumentation对象生成Activity类的实例
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);

            // 省去三行代码…………
        } catch (Exception e) {
            // 省去对异常的捕获处理
        }

        try {
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);

            // 省去多行无关代码

            if (activity != null) {
                Context appContext = createBaseContextForActivity(r, activity);
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);

                // 省去多行无关代码

                if (customIntent != null) {
                    activity.mIntent = customIntent;
                }
                r.lastNonConfigurationInstances = null;
                activity.mStartedActivity = false;
                int theme = r.activityInfo.getThemeResource();
                if (theme != 0) {
                    activity.setTheme(theme);
                }

                /*
                 * 调用callActivityOnCreate方法处理Create逻辑
                 */
                activity.mCalled = false;
                mInstrumentation.callActivityOnCreate(activity, r.state);
                if (!activity.mCalled) {
                    // 省去多行无关代码
                }
                r.activity = activity;
                r.stopped = true;

                /*
                 * 调用performStart方法处理Start逻辑
                 */
                if (!r.activity.mFinished) {
                    activity.performStart();
                    r.stopped = false;
                }
                // 省去多行无关代码
            }
            // 省去两行无关代码

        } catch (SuperNotCalledException e) {
            // 省去对异常的捕获处理

        } catch (Exception e) {
            // 省去对异常的捕获处理
        }

        return activity;
    }

    // 省去巨量代码……………………
}

performLaunchActivity方法中目测木有我我们想要的信息,其创建了Activity并调度了Create和Start的逻辑处理,那我们看看callActivityOnCreate方法呢:

public class Instrumentation {
	// 省去海量代码…………………………

    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        // 省去某些逻辑……

        activity.performCreate(icicle);

        // 省去某些逻辑……
    }

	// 省去巨量代码……………………
}

callActivityOnCreate中除了对MQ的一些调度外最重要的还是通过Activity的实例调用了performCreate方法:

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2 {
    // 省去海量代码…………………………

    final void performCreate(Bundle icicle) {
        onCreate(icicle);
        mVisibleFromClient = !mWindow.getWindowStyle().getBoolean(
                com.android.internal.R.styleable.Window_windowNoDisplay, false);
        mFragments.dispatchActivityCreated();
    }

    // 省去巨量代码……………………
}

performCreate方法逻辑就更干脆了,最主要的还是调用了我们Activity的onCreate方法,我们没在这里找到我们想要的东西,那再来看performStart:

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2 {
    // 省去海量代码…………………………

    final void performStart() {
        mFragments.noteStateNotSaved();
        mCalled = false;
        mFragments.execPendingActions();
        mInstrumentation.callActivityOnStart(this);
        if (!mCalled) {
            throw new SuperNotCalledException(
                "Activity " + mComponent.toShortString() +
                " did not call through to super.onStart()");
        }
        mFragments.dispatchStart();
        if (mAllLoaderManagers != null) {
            final int N = mAllLoaderManagers.size();
            LoaderManagerImpl loaders[] = new LoaderManagerImpl[N];
            for (int i=N-1; i>=0; i--) {
                loaders[i] = mAllLoaderManagers.valueAt(i);
            }
            for (int i=0; i<N; i++) {
                LoaderManagerImpl lm = loaders[i];
                lm.finishRetain();
                lm.doReportStart();
            }
        }
    }

    // 省去巨量代码……………………
}

performStart相对于performCreate有更多的逻辑处理,但依然木有我们想要的结果,其最终还是同过Instrumentation对象调用callActivityOnStart:

public class Instrumentation {
	// 省去海量代码…………………………

    public void callActivityOnStart(Activity activity) {
        activity.onStart();
    }

	// 省去巨量代码……………………
}

callActivityOnStart仅仅是调用了Activity的onStart方法,同样……onStart方法中也没有我们想要的结果~~~~我们抱着即将从埃菲尔铁塔顶端做自由落体的心态继续看onResume方法的调度,其在ActivityThread中通过handleResumeActivity调度:

public final class ActivityThread {
    // 省去海量代码…………………………

    final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,
            boolean reallyResume) {
        unscheduleGcIdler();

        ActivityClientRecord r = performResumeActivity(token, clearHide);

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

            // 省去无关代码…………

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

            boolean willBeVisible = !a.mStartedActivity;
            if (!willBeVisible) {
                try {
                    willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
                            a.getActivityToken());
                } catch (RemoteException e) {
                }
            }
            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) {
                // 省去无关代码…………

                r.hideForNow = true;
            }

            cleanUpPendingRemoveWindows(r);

            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
                if (r.newConfig != null) {
                    // 省去无关代码…………

                    performConfigurationChanged(r.activity, r.newConfig);
                    freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.newConfig));
                    r.newConfig = null;
                }

                // 省去无关代码…………

                WindowManager.LayoutParams l = r.window.getAttributes();
                if ((l.softInputMode
                        & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
                        != forwardBit) {
                    l.softInputMode = (l.softInputMode
                            & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
                            | forwardBit;
                    if (r.activity.mVisibleFromClient) {
                        ViewManager wm = a.getWindowManager();
                        View decor = r.window.getDecorView();
                        wm.updateViewLayout(decor, l);
                    }
                }
                r.activity.mVisibleFromServer = true;
                mNumVisibleActivities++;
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }

            if (!r.onlyLocalRequest) {
                r.nextIdle = mNewActivities;
                mNewActivities = r;

                // 省去无关代码…………

                Looper.myQueue().addIdleHandler(new Idler());
            }
            r.onlyLocalRequest = false;

            // 省去与ActivityManager的通信处理

        } else {
            // 省略异常发生时对Activity的处理逻辑
        }
    }

    // 省去巨量代码……………………
}

handleResumeActivity方法逻辑相对要复杂一些,除了一啪啦对当前显示Window的逻辑判断以及没创建的初始化等等工作外其在最终会调用Activity的makeVisible方法:

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2 {
    // 省去海量代码…………………………

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

    // 省去巨量代码……………………
}

在makeVisible方法中逻辑相当简单,获取一个窗口管理器对象并将我们曾在自定义控件其实很简单7/12中提到过的根视图DecorView添加到其中,addView的具体实现在WindowManagerGlobal中:

public final class WindowManagerGlobal {
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        // 省去很多代码

        ViewRootImpl root;

        // 省去一行代码

        synchronized (mLock) {
            // 省去无关代码

            root = new ViewRootImpl(view.getContext(), display);

            // 省去一行代码

            // 省去一行代码

            mRoots.add(root);

            // 省去一行代码
        }

        // 省去部分代码
    }
}

在addView生成了一个ViewRootImpl对象并将其保存在了mRoots数组中,每当我们addView一次,就会生成一个ViewRootImpl对象,其实看到这里我们还可以扩展一下问题一个APP是否可以拥有多个根视图呢?答案是肯定的,因为只要我调用了addView方法,我们传入的View参数就可以被认为是一个根视图,但是!在framework的默认实现中有且仅有一个根视图,那就是我们上面makeVisible方法中addView进去的DecorView,所以为什么我们可以说一个APP虽然可以有多个Activity,但是每个Activity只会有一个Window一个DecorView一个ViewRootImpl,看到这里很多童鞋依然会问,也就是说在onResume方法被执行后我们的ViewRootImpl才会被生成对吧,但是为什么下面的代码依然可以正确运行呢:

/**
 * 主界面
 *
 * @author Aige {@link http://blog.csdn.net/aigestudio}
 * @since 2014/11/17
 */
public class MainActivity extends Activity {
	private TextView tvText;

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

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

	@Override
	protected void onResume() {
		super.onResume();
		new Thread(new Runnable() {
			@Override
			public void run() {
				tvText.setText("OtherThread");
			}
		}).start();
	}
}

没错,可以执行!首先我们这里的是个线程,其次这里要涉及framework对UI事件处理的方式,我们在Android翻页效果原理实现之引入折线中曾说过Android对UI事件的处理需要依赖于Message Queue,当一个Msg被压入MQ到处理这个过程并非立即的,它需要一段事件,我们在线程中通过Thread.sleep(200)在等,在等什么呢?在等ViewRootImpl的实例对象被创建,有关于GUI中Message
Queue的处理如有机会我会浓缩在《深入理解 Android GUI 框架》系列中,这里就暂且先不说了,那么又有同学会问了!纳尼,既然ViewRootImpl还未被创建那么为什么会能绘制出文本?!!!如果你有这个疑问,我只能说你观察细致问得好,但是,这个问题我不打算解答,留给各位,上面我其实就在教大家如何去寻找原因了,渔已授之于你所以就不再多说了~~~~既然我们找到了原因所在,那么我们该如何摆脱“The Android UI toolkit is not thread-safe and the view must
always be manipulated on the UI thread.”这个噩梦呢?

时间: 2024-11-03 23:46:19

为什么我们可以在非UI线程中更新UI的相关文章

Android 非UI线程中更新UI

Android 非UI线程中更新UI runOnUiThread(new Runnable() { public void run() { onDown(null); } });

Android开之在非UI线程中更新UI

当在非UI线程中更新UI(程序界面)时会出现如下图所示的异常: 那如何才能在非UI线程中更细UI呢? 方法有很多种,在这里主要介绍两种: 第一种:在需要更新UI的代码行后加Looper.prepare();与Looper.loop();两句话即可.如: new Thread(){ @Override public void run() { // TODO Auto-generated method stub txtRotation.setText("在非UI线程中更新UI!"); Lo

Android在非UI线程中更新UI的方法

1.使用Thread+Handler实现非UI线程更新UI界面 private MyHandler mHandler = new MyHandler(); ...... mHandler.sendResult(MyHandler.UPDATE_VIEW, null); ...... private class MyHandler extends Handler{ private static final int UPDATE_VIEW = 0; @Override public void han

基础篇-在非UI线程中更新UI元素

个人原创,转载请注明出处: http://blog.csdn.net/supluo/article/details/ 先了解两个概念 1.UI:User Interface的缩写,用户界面的意思.你可以不恰当的理解为我们能够看到的,操作的东西:在Android中什么才称为UI呢,可以简单的理解为View及其子类等元素.这是一个不够正确的概念,只是对新手做一个简单的抛砖引玉. 2.ANR:Application Not Responding,意思是程序没有响应. 在如下情况下,Android会报出

UWP 在非UI线程中更新UI

大家都知道,不可以在 其他线程访问 UI 线程,访问 UI 线程包括给 依赖属性设置值.读取依赖属性.调用方法(如果方法里面修改了依赖属性)等.一旦访问UI线程,那么就会报错,为了解决这个问题,需要使用本文的方法,让后台线程访问 UI 线程. 本文提供三个方法可以让其他线程访问 UI 线程 第一个方法是比较不推荐使用的,可能出现 win10 uwp Window.Current.Dispatcher中Current为null await Window.Current.Dispatcher.Run

android Looper 非UI线程中更新UI

测试service中使用了一个Toast来打印log. 提示无法调用Looper.prepare() 加入后代码正常执行.  Looper.prepare();      Toast.makeText(getApplicationContext(), "Services Thread", Toast.LENGTH_LONG).show()  Looper.loop(); 不过再其他处重新加了一个Toast后出现错误,告知每一个线程只能有一个Looper Caused by: java.

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

问题描述 做过android开发基本都遇见过ViewRootImpl$CalledFromWrongThreadException,上网一查,得到结果基本都是只能在主线程中更改ui,子线程要修改ui只能post到主线程或者使用handler之类.但是仔细看看exception的描述并不是这样的,"Only the original thread that created a view hierarchy can touch its views",只有创建该 view 布局层次的原始线程

C# WINFORM 线程中更新UI

幸好今天是周末,有时间把这个问题记录一下.在多种语言之间切换,发现开发效率降的很低了,开发成本都集中到调式上了,C/C++这些放弃很久了,突然感觉线程这个问题搞的有点烦躁 我这里提到的线程中更新UI,在大数据 大并发,以及CPU时间碎片上,未经过验证,项目紧 你懂的..如果你和我一样急于实现 可以考虑一下 总体是这样 //NetDataHandler 是被非UI主线程调用的 你现在看到到这两个函数是放在UI窗体上的 public void NetDataHandler(string jsonDa

网络操作不能直接写在主线程中 以及 为什么不能在子线程中更新UI控件的属性

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ //注意: 所有网络操作不能直接写在主线程中 因为所有的网络操作都是耗时的,如果加载到主线程中,会导致与用户的交互出现问题 ,所以要加载到子线程中 // [self loadImage]; [self performSelectorInBackground:@selector(loadImage) withObject:nil]; } //加