可能是全网最简单透彻的安卓子线程更新 UI 解析
相信下面的代码大家看过很多遍了,在 onCreate() 生命周期里开启一个线程来更新 UI ,居然没有闪退和异常( 在大概率情况下是没有问题的 )
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.e("MyButton", "onCreate");
new Thread(new Runnable() {
@Override
public void run() {
btn.setText("子线程更新UI");
Log.e("MyButton", "子线程更新UI");
}
}).start();
}
我们在子线程里睡眠一秒试试看
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.e("MyButton", "onCreate");
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
btn.setText("子线程更新UI");
Log.e("MyButton", "子线程更新UI");
}
}).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:7512)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1206)
at android.view.View.requestLayout(View.java:22029)
at android.view.View.requestLayout(View.java:22029)
at android.view.View.requestLayout(View.java:22029)
at android.view.View.requestLayout(View.java:22029)
at android.view.View.requestLayout(View.java:22029)
at android.view.View.requestLayout(View.java:22029)
at android.widget.ScrollView.requestLayout(ScrollView.java:1533)
at android.view.View.requestLayout(View.java:22029)
at android.view.View.requestLayout(View.java:22029)
at android.widget.TextView.checkForRelayout(TextView.java:8538)
at android.widget.TextView.setText(TextView.java:5401)
at android.widget.TextView.setText(TextView.java:5257)
at android.widget.TextView.setText(TextView.java:5214)
at demo.rzj.com.androiddemo.activity.MainActivity$1.run(MainActivity.java:93)
at java.lang.Thread.run(Thread.java:764)
这个分享一个解决 Bug 时的小技巧,异常的起点在最下面,最顶上的是抛出异常的方法栈,我们只需从下往上就可以知道方法的调用顺序了,跟着 TextView 的源码从 setText() 里去查看源码,setText()方法经过多次跳转进入以下方法
3561 private void setText(CharSequence text, BufferType type,
3562 boolean notifyBefore, int oldlen) {
....
//过滤掉一些非关键代码
// 这段代码是核心,当 mLayout 不为空的时候才会触发 checkForRelayout();
3695 if (mLayout != null) {
3696 checkForRelayout();
3697 }
3698
3699 sendOnTextChanged(text, 0, oldlen, textLength);
3700 onTextChanged(text, 0, oldlen, textLength);
3701
3702 if (needEditableForNotification) {
3703 sendAfterTextChanged((Editable) text);
3704 }
3705
3706 // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
3707 if (mEditor != null) mEditor.prepareCursorControllers();
3708 }
这个方法是关键,当 mLayout 不为空时才会进入,我们进入 checkForRelayout() 方法
6400 /**
6401 * Check whether entirely new text requires a new view layout
6402 * or merely a new text layout.
6403 */
6404 private void checkForRelayout() {
6405 // If we have a fixed width, we can just swap in a new text layout
6406 // if the text height stays the same or if the view height is fixed.
6407
6408 if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
6409 (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
6410 (mHint == null || mHintLayout != null) &&
6411 (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
6412 // Static width, so try making a new text layout.
6413
6414 int oldht = mLayout.getHeight();
6415 int want = mLayout.getWidth();
6416 int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
6417
6418 /*
6419 * No need to bring the text into view, since the size is not
6420 * changing (unless we do the requestLayout(), in which case it
6421 * will happen at measure).
6422 */
6423 makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
6424 mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
6425 false);
6426
6427 if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
6428 // In a fixed-height view, so use our new text layout.
6429 if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
6430 mLayoutParams.height != LayoutParams.MATCH_PARENT) {
6431 invalidate();
6432 return;
6433 }
6434
6435 // Dynamic height, but height has stayed the same,
6436 // so use our new text layout.
6437 if (mLayout.getHeight() == oldht &&
6438 (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
6439 invalidate();
6440 return;
6441 }
6442 }
6443
6444 // We lose: the height has changed and we have a dynamic height.
6445 // Request a new view layout using our new text layout.
6446 requestLayout();
6447 invalidate();
6448 } else {
6449 // Dynamic width, so we have no choice but to request a new
6450 // view layout with a new text layout.
6451 nullLayouts();
6452 requestLayout();
6453 invalidate();
6454 }
6455 }
这个方法的核心就是 requestLayout() 以及 invalidate() ,相信大家也都清楚这两个方法的用途,requestLayout() 方法会执行 onMeasure() 和 onLayout() 方法,不会执行 onDraw() 方法,而 invalidate() 只会触发 onDraw() 方法,根据 View 的绘制流程,所以一般都是先调用 requestLayout() 然后 invalidate() ,废话不多说,我们回到那个异常报错继续跟进 View 的 requestLayout(),这个报错说明当我们在子线程睡眠一秒后,mLayout 是不为空的,所以才会触发父层的方法。
15463 /**
15464 * Call this when something has changed which has invalidated the
15465 * layout of this view. This will schedule a layout pass of the view
15466 * tree.
15467 */
15468 public void requestLayout() {
15469 mPrivateFlags |= PFLAG_FORCE_LAYOUT;
15470 mPrivateFlags |= PFLAG_INVALIDATED;
15471
15472 if (mParent != null && !mParent.isLayoutRequested()) {
15473 mParent.requestLayout();
15474 }
15475 }
View 类中的 mParent 是一个 ViewParent 接口类型变量,其实这个是 ViewRootImpl 的实例对象,为什么这么说,下面的代码会有解释,也就是说这个 mParent.requestLayout() 会触发 ViewRootImpl 里的 requestLayout()
11526 /*
11527 * Caller is responsible for calling requestLayout if necessary.
11528 * (This allows addViewInLayout to not request a new layout.)
11529 */
11530 void assignParent(ViewParent parent) {
11531 if (mParent == null) {
11532 mParent = parent;
11533 } else if (parent == null) {
11534 mParent = null;
11535 } else {
11536 throw new RuntimeException("view " + this + " being added, but"
11537 + " it already has a parent");
11538 }
11539 }
遍寻 View 的源码,只有这个方法里有对 mParent 进行赋值,进入 ViewRootImpl 查看有没有调用该方法
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
....
//过滤掉一些非关键代码
view.assignParent(this);
}
答案很明显,我们再延伸一下, ViewRootImpl 是通过 WindowManager 实例化的,它的实现类是 WindowManagerImpl,这里分享一个查看源码的小知识点,一个接口或抽象类的实现类往往都是以它本身的类名 + Impl 的命名方式,这里也体现了规范化命名的好处,便于查找。
46 private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
67 @Override
68 public void addView(View view, ViewGroup.LayoutParams params) {
69 mGlobal.addView(view, params, mDisplay, mParentWindow);
70 }
也就是说,这个实例化 ViewRootImpl 是在 WindowManagerGlobal 里的 addView
163 public void addView(View view, ViewGroup.LayoutParams params,
164 Display display, Window parentWindow) {
165 if (view == null) {
166 throw new IllegalArgumentException("view must not be null");
167 }
168 if (display == null) {
169 throw new IllegalArgumentException("display must not be null");
170 }
171 if (!(params instanceof WindowManager.LayoutParams)) {
172 throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
173 }
174
175 final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
176 if (parentWindow != null) {
177 parentWindow.adjustLayoutParamsForSubWindow(wparams);
178 }
179
180 ViewRootImpl root;
181 View panelParentView = null;
182
183 synchronized (mLock) {
184 // Start watching for system property changes.
185 if (mSystemPropertyUpdater == null) {
186 mSystemPropertyUpdater = new Runnable() {
187 @Override public void run() {
188 synchronized (mLock) {
189 for (ViewRootImpl viewRoot : mRoots) {
190 viewRoot.loadSystemProperties();
191 }
192 }
193 }
194 };
195 SystemProperties.addChangeCallback(mSystemPropertyUpdater);
196 }
197
198 int index = findViewLocked(view, false);
199 if (index >= 0) {
200 throw new IllegalStateException("View " + view
201 + " has already been added to the window manager.");
202 }
203
204 // If this is a panel window, then find the window it is being
205 // attached to for future reference.
206 if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
207 wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
208 final int count = mViews != null ? mViews.length : 0;
209 for (int i=0; i<count; i++) {
210 if (mRoots[i].mWindow.asBinder() == wparams.token) {
211 panelParentView = mViews[i];
212 }
213 }
214 }
215
216 root = new ViewRootImpl(view.getContext(), display);
217
218 view.setLayoutParams(wparams);
219
220 if (mViews == null) {
221 index = 1;
222 mViews = new View[1];
223 mRoots = new ViewRootImpl[1];
224 mParams = new WindowManager.LayoutParams[1];
225 } else {
226 index = mViews.length + 1;
227 Object[] old = mViews;
228 mViews = new View[index];
229 System.arraycopy(old, 0, mViews, 0, index-1);
230 old = mRoots;
231 mRoots = new ViewRootImpl[index];
232 System.arraycopy(old, 0, mRoots, 0, index-1);
233 old = mParams;
234 mParams = new WindowManager.LayoutParams[index];
235 System.arraycopy(old, 0, mParams, 0, index-1);
236 }
237 index--;
238
239 mViews[index] = view;
240 mRoots[index] = root;
241 mParams[index] = wparams;
242 }
243
244 // do this last because it fires off messages to start doing things
245 try {
246 root.setView(view, wparams, panelParentView);
247 } catch (RuntimeException e) {
248 // BadTokenException or InvalidDisplayException, clean up.
249 synchronized (mLock) {
250 final int index = findViewLocked(view, false);
251 if (index >= 0) {
252 removeViewLocked(index, true);
253 }
254 }
255 throw e;
256 }
257 }
最后我们在看一下 Activity 的 ViewRootImpl 是在哪里实例化的,作为单线程模型,我们可以从 应用的 Java 层入口,ActivityThread 也就是 UI 线程的实现类去查看
1131 private class H extends Handler {
1132 public static final int LAUNCH_ACTIVITY = 100;
1133 public static final int PAUSE_ACTIVITY = 101;
1134 public static final int PAUSE_ACTIVITY_FINISHING= 102;
1135 public static final int STOP_ACTIVITY_SHOW = 103;
1136 public static final int STOP_ACTIVITY_HIDE = 104;
...
// 省略大量的生命周期状态码
1175 String codeToString(int code) {
1176 if (DEBUG_MESSAGES) {
1177 switch (code) {
...
// 省略大量的 case 判断
1185 case RESUME_ACTIVITY: return "RESUME_ACTIVITY";
1221 }
1222 }
1223 return Integer.toString(code);
1224 }
1225 public void handleMessage(Message msg) {
1226 if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
1227 switch (msg.what) {
...
// 省略大量的生命周期处理
1274 case RESUME_ACTIVITY:
1275 Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityResume");
1276 handleResumeActivity((IBinder)msg.obj, true,
1277 msg.arg1 != 0, true);
1278 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
1279 break;
}
1433 if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
1434 }
1461 }
ActivityThread 里的 H Handler实例是核心中的核心,关键中的关键,一句话,我们的所有消息都需要通过它的处理分发,Activity 的生命周期、用户的触碰事件,一切的反馈都是通过这个来交互,如果没有这个,应用就会像一个 Java 程序,运行然后结束,轮询器的阻塞让 ActivityThread 的 main 方法持续处于运行状态,根据代码中的逻辑,非常明显,当 Activity 的 onResume() 方法被触发时会调用 handleResumeActivity()方法,而 handleResumeActivity 方法里实例化了 ViewRootImpl
2765 final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,
2766 boolean reallyResume) {
2767 // If we are getting ready to gc after going to the background, well
2768 // we are back active so skip it.
2769 unscheduleGcIdler();
2770
2771 ActivityClientRecord r = performResumeActivity(token, clearHide);
2772
2773 if (r != null) {
2774 final Activity a = r.activity;
2775
2776 if (localLOGV) Slog.v(
2777 TAG, "Resume " + r + " started activity: " +
2778 a.mStartedActivity + ", hideForNow: " + r.hideForNow
2779 + ", finished: " + a.mFinished);
2780
2781 final int forwardBit = isForward ?
2782 WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
2783
2784 // If the window hasn't yet been added to the window manager,
2785 // and this guy didn't finish itself or start another activity,
2786 // then go ahead and add the window.
2787 boolean willBeVisible = !a.mStartedActivity;
2788 if (!willBeVisible) {
2789 try {
2790 willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
2791 a.getActivityToken());
2792 } catch (RemoteException e) {
2793 }
2794 }
2795 if (r.window == null && !a.mFinished && willBeVisible) {
2796 r.window = r.activity.getWindow();
2797 View decor = r.window.getDecorView();
2798 decor.setVisibility(View.INVISIBLE);
// 通过Activity 获取 WindowManager 的实例对象
2799 ViewManager wm = a.getWindowManager();
2800 WindowManager.LayoutParams l = r.window.getAttributes();
2801 a.mDecor = decor;
2802 l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
2803 l.softInputMode |= forwardBit;
2804 if (a.mVisibleFromClient) {
2805 a.mWindowAdded = true;
// WindowManager 的 addView 方法,一切的源头
2806 wm.addView(decor, l);
2807 }
...
// 省略部分无关代码
2880 }
那么我们回到最顶部的报错方法栈
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7512)
4744 void checkThread() {
4745 if (mThread != Thread.currentThread()) {
4746 throw new CalledFromWrongThreadException(
// 只有创建视图层次结构的原始线程才能访问它的视图
4747 "Only the original thread that created a view hierarchy can touch its views.");
4748 }
4749 }
还记得 TextView 里的 setText 方法吗,当 mLayout 不为空时才会进入,而事实上只有 View 在 测量 方法里才会对这个值进行赋值,答案也就很明显了,当我们在子线程里 setText 的时候,其实只是简单的设置了这个控件要显示的值,并不会立即去显示,因为 mLayout 是为空,为什么为空,因为只有在 Activity 的onResume 生命周期里才会去实例化 ViewRootImpl 一个个方法栈的调用最后才会触发 View 的测量。
最后扩展一下,如果就是想在子线程里更新 UI 怎么办呢,在onResume 之前就行,或者把 View 的 ViewRootImpl 实例化放到子线程来进行,这样就不会因为非 UI 线程抛出异常。
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Button button = new Button(MainActivity.this);
WindowManager wm = MainActivity.this.getWindowManager();
WindowManager.LayoutParams params = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,0, 0, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
wm.addView(button, params);
button.setTextColor(MainActivity.this.getResources().getColor(R.color.colorPrimaryDark));
button.setText("子线程更新UI");
Looper.loop();
Log.e("MyButton", "子线程更新UI");
}
}).start();