1、单线程模型
Android 的用户界面(UI),是基于一个主线程(UI 线程)的单线程控制执行的,这意味着所有的触摸事件处理,布局计算,视图绘制都在这个线程中执行,为了保证 UI 的流畅性,系统要求这些操作必须快速完成,避免阻塞主线程出现 ANR 的情况。
1.1 ANR(Application not Responding)
当在主线程上执行操作耗时过长或被阻塞,以至于无法及时响应用户的输入或者系统事件,系统就会判定应用程序没有响应。
不同类型的 ANR 阈值:
- 按键分发超时:对于用户的输入(触摸或者按键),如果应用在 5 秒内未作出响应,就会触发 ANR
- 广播队列超时:对于 BroadCaseReceiver,如果在 10 秒内未完成onReceive方法,会触发 ANR
- 服务超时: Service Timeout 如果一个服务在 20 秒内没有处理完它的启动操作或者绑定请求,也会触发 ANR。服务应避免在主线程执行耗时操作,以免影响到 UI 响应。
1.如何在其他线程访问 UI 线程
1.Activity.runOnUiThread(Runable)
2.View.post(Runable)
3.View.postDelay(Runable,long)
4.Handle(Looper.getMainLooper()).post(Runable)
5.Handle(Looper.getMainLooper()).postDelay(Runable,long)
7.协程 withContext(Dispathers.Main)
2.为什么 UI 线程是不安全的
有以下几点原因:
- 单线程模型,如 1.单线程模型中所说
- 线程安全 API,Android的 UI 框架并未设计为多线程环境的,它们内部没有内置的同步机制来保证并发访问的安全性如 findviewbyid()
- 并发问题,因为 UI框架并不适合多线程环境,所以如果出现同时又多个线程尝试修改 UI 元素,就会出现线程不安全状态,这样会导致状态混乱
- 抛出异常,如果尝试再非 UI 线程直接修改 UI,系统会抛出异常:CallFromWrongThreadException
Android 实现 UI 更新有两组方法,Invalidate 和 PostInvalidate。
Invalidate 是只能在UI线程使用的,如果在子线程调用 Invalidate方法的同事在 UI 线程调用 Invalidate方法,
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
* <p>
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
*/
public void invalidate() {
invalidate(true);
}
PostInvalidate 是在子线程中使用的
/**
* <p>Cause an invalidate to happen on a subsequent cycle through the event loop.
* Use this to invalidate the View from a non-UI thread.</p>
*
* <p>This method can be invoked from outside of the UI thread
* only when this View is attached to a window.</p>
*
* @see #invalidate()
* @see #postInvalidateDelayed(long)
*/
public void postInvalidate() {
postInvalidateDelayed(0);
}
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
//铜锅 handler 将刷新 UI的消息发送到主线程
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
实际上虽然可以在子线程中调用 PostInvalidate方法刷新 UI,但实际上 方法内部还是通过 Handler 将事件发送到子线程进行 UI更新。
3.子线程到底能不能更新 UI
通常情况下,如果在子线程尝试更新 UI,会出现报错
Only the original thread that created a view hierarchy can touch its views.
这个报错源自于android/view/ViewRootImpl.java 核心就是 checkThread 方法这里判断了线程,
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
3.1子线程更新 UI 的思路
3.1.1 在子线程创建 RootViewImpl
原理:
这里并没有判断线程是不是 UI 线程,而是判断了是不是mThread.那么当 nThread 就是当前线程(子线程)时,是不是就能在子线程修改 UI 了。
我们找到 mThread 的初始化位置,
public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
mContext = context;
mWindowSession = session;
mDisplay = display;
mBasePackageName = context.getBasePackageName();
//!!!!mThread 在 ViewRootImpl 的构造函数中进行了初始化
mThread = Thread.currentThread();
//...省略其他代码
}
那么证明,如果在子线程创建了 ViewRootImpl ,那么 mThread就会被初始化成子线程,即可在子线程进行 UI 操作。那么看下ViewRootImpl 在哪可以创建实例。
直接调用构造函数显然是不行的
调查发现,在调用windowmanager.addView 方法时会初始化 viewrootimpl,具体实现是在这个类中的addView 方法,代码如下:
android/view/WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent, then hardware acceleration for this view is
// set from the application's hardware acceleration setting.
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// Start watching for system property changes.
if (mSystemPropertyUpdater == null) {
mSystemPropertyUpdater = new Runnable() {
@Override public void run() {
synchronized (mLock) {
for (int i = mRoots.size() - 1; i >= 0; --i) {
mRoots.get(i).loadSystemProperties();
}
}
}
};
SystemProperties.addChangeCallback(mSystemPropertyUpdater);
}
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
// If this is a panel window, then find the window it is being
// attached to for future reference.
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
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, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
也就是说,如果直接在子线程调用 windowmanager的 addview方法,viewrootimpl 中的 mthread就会被初始化成子线程,这样在 checkThread()方法时,就不会被阻拦了。
还有一个重要的点是:
ViewRootImpl 会创建Handler,而子线程是默认没有 Looper的,所以要在子线程创建自己的 Looper
代码demo:
thread {
//创建 Looper,ViewRootImpl 中会使用 handler
Looper.prepare()
val textView = TextView(this)
textView.text = "在子线程创建 ViewRootImpl"
textView.setTextColor(ContextCompat.getColor(this,R.color.white))
textView.setBackgroundColor(ContextCompat.getColor(this,R.color.primary_red))
textView.setOnClickListener {
textView.text = "修改了内容"
}
//此处创建了子线程的 ViewRootImpl
windowManager.addView(textView, WindowManager.LayoutParams().apply {
this.width = WindowManager.LayoutParams.WRAP_CONTENT
this.height = WindowManager.LayoutParams.WRAP_CONTENT
})
Looper.loop()
}
3.1.2 在 Resume回调函数之前修改 UI
如果 checkThread 方法不被执行,那么就不会报这个错误了。
如果在子线程修改 UI 时,ViewRootImpl 还没有被创建,就不会触发 checkThread 方法。因此,我们只要找到 acitivity本身创建viewrootimpl 也就是 activity本身调用 windowmanager 的 addView方法的调用时机,并在它之前修改 UI 即可
既然 AndroidUI是单线程模型,那么这块的逻辑就是在 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.
*
这个类管理了应用程序进程中的主线程的执行,组织和执行 activity,broadcast,和其他 activity 管理请求
* {@hide}
*/
public final class ActivityThread extends ClientTransactionHandler
implements ActivityThreadInternal {
不出意外,在handleResumeActivity方法内部找到了调用的地方
@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
mSomeActivitiesChanged = true;
//此处调用 onResume 方法
if (!performResumeActivity(r, finalStateRequest, reason)) {
return;
}
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) {
willBeVisible = ActivityClient.getInstance().willActivityBeVisible(
a.getActivityToken());
}
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 (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//调用 windowmanager的 addview 方法,创建 ViewRootImpl 对象
wm.addView(decor, l);
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}
}
onResume方法的调用路径
activitythread.handleResumeActivity -》activitythread.performResumeActivity-》activity.performResume ->Instrumentation.callActivityOnResume -> activity.onResume
3.1.3 在子线程修改 UI 前手动调用 requestlayout()
对 view的修改通常会调用 requestlayout 方法,每一层级的又会调用父层级的 requestlayout 知道最顶层的 ViewRootImpl中,ViewRootImpl的 requestlayout 方法中调用了 checkthread.
但 requestlayout 有一个类似锁的机制,避免重复的布局请求,在请求requestlayout 时会设置一个标志位
/**
* <p>Indicates whether or not this view's layout will be requested during
* the next hierarchy layout pass.</p>
*
* @return true if the layout will be forced during next layout pass
*/
public boolean isLayoutRequested() {
return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
}
在本次layout 结束前,其他的 requestlayout 方法不会被执行,因此不会触发 checkthread 方法
demo:
//2.先执行requestLayout()再更新UI
tv.setOnClickListener {
it.requestLayout()
thread {
tv.text = "先执行requestLayout()再更新UI"
}
}
3.1.4 开启硬件加速,并让目标 view保持固定大小
当TargetSDK > 14时,默认是开启硬件加速的,即 android:hardwareAccelerated=“true”
在 textview 中,修改 text会触发 textview 的 checkforrelayout
@UnsupportedAppUsage
private void checkForRelayout() {
//如果宽度不是自适应
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
|| (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
&& (mHint == null || mHintLayout != null)
&& (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
// Static width, so try making a new text layout.
int oldht = mLayout.getHeight();
int want = mLayout.getWidth();
int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
/*
* No need to bring the text into view, since the size is not
* changing (unless we do the requestLayout(), in which case it
* will happen at measure).
*/
makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
false);
if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
// 如果高度不是 wrapcontent或者 matchparent
if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
&& mLayoutParams.height != LayoutParams.MATCH_PARENT) {
autoSizeText();
invalidate();
return;
}
//如果豪赌没有变化
if (mLayout.getHeight() == oldht
&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
autoSizeText();
invalidate();
return;
}
}
// We lose: the height has changed and we have a dynamic height.
// Request a new view layout using our new text layout.
requestLayout();
invalidate();
} else {
// Dynamic width, so we have no choice but to request a new
// view layout with a new text layout.
nullLayouts();
requestLayout();
invalidate();
}
}
从代码中可以看出,如果高度没有变化,不会调用 requestlayout 方法,而是直接去刷新。
invalidate 方法内部层层调用走到了 viewgroup 的 invalidateChild方法
onDescendantInvalidated 方法会递归调用到 ViewRootImpl的onDescendantInvalidated方法
然后内部调用ViewRootImpl的invalidate方法
3.2 其他方式
使用 Surface,Surface 的绘制流程不走 checkThread()
val sf = findViewById<SurfaceView>(R.id.tv_splash)
sf.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
thread {
while (true) {
HdyjLog.d(Thread.currentThread().name)
val canvas = holder.lockCanvas()
val random = java.util.Random()
val r = random.nextInt(255)
val g = random.nextInt(255)
val b = random.nextInt(255)
canvas.drawColor(Color.rgb(r, g, b))
holder.unlockCanvasAndPost(canvas)
SystemClock.sleep(500)
}
}
}
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
})
参考文档: