一、问题表现
FirstActivity
有个点击事件可以进入到SecondActivity
,但是点击的时候有个Tag防止起多个SecondActivity
,而这个Tag是在SecondActivity#onDestroy()
里重置的,在某种情况下,Tag不生效了,于是便在SecondActivity#onDestroy()
增加日志,发现onPause()
未调用,onDestroy()
延迟了10s调用。
二、问题原因
前人栽树后人乘凉,感谢ZCJ风飞
大佬的分析:
深入分析Android中Activity的onStop和onDestroy()回调延时及延时10s的问题
通过这位大佬分析可以得知:在下一个要显示的Activity
的回调onResume
之后,ActivityThread
会注册一个主线程消息队列的一个IdleHandler
,用于ActivityManagerService
处理Activity#onStop()
和Activity#onDestroy()
,若主线程一直在循环处理消息队列中累积的Message
,则上述的IdleHandler
一直得不得调用,作为一个健壮的ROM,AMS会发送一个延时10s的消息,确保正常流程行不通的情况下也能销毁Activity,从而表现上便是onPause()
未调用,onDestroy()
延迟了10s调用。
三、问题验证
再次感谢ZCJ风飞
大佬:
public class SecondActivity extends Activity {
private static final String TAG = "SecondActivity";
private Handler handler = new Handler();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
postMsg();
}
private void postMsg() {
handler.post(new Runnable() {
@Override
public void run() {
try {
// 在主线程中休眠一小段时间
// 用来模拟主线程中诸如复杂的绘制、复杂数据处理、帧动画等等操作
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
handler.postDelayed(new Runnable() {
@Override
public void run() {
postMsg();
}
}, 10);
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.i(TAG, "onDestroy: ");
}
@Override
protected void onStop() {
super.onStop();
Log.i(TAG, "onStop: ");
}
}
通过上述示例代码,的确出现了onDestroy()
延迟了10s调用,故问题原因肯定是主线程MessageQueue
在循环处理累积的Message
。那么问题来了,这些累积的Message
是什么?又是谁发送的?
四、问题解决
1.累积的Message
是什么
由Handler机制可以,主线程持有Looper
, 而Looper
内部维持了一个MessageQueue
用于调度各种Message
,故可以尝试通过MessageQueue
去了解Message
是什么;阅读Looper
的源码,发现有以下代码:
/**
* Dumps the state of the looper for debugging purposes.
*
* @param pw A printer to receive the contents of the dump.
* @param prefix A prefix to prepend to each line which is printed.
*/
public void dump(@NonNull Printer pw, @NonNull String prefix) {
pw.println(prefix + toString());
mQueue.dump(pw, prefix + " ", null);
}
MessageQueue#dump()
源码如下:
void dump(Printer pw, String prefix, Handler h) {
synchronized (this) {
long now = SystemClock.uptimeMillis();
int n = 0;
for (Message msg = mMessages; msg != null; msg = msg.next) {
if (h == null || h == msg.target) {
pw.println(prefix + "Message " + n + ": " + msg.toString(now));
}
n++;
}
pw.println(prefix + "(Total messages: " + n + ", polling=" + isPollingLocked()
+ ", quitting=" + mQuitting + ")");
}
}
看起来可以拉取到主线程消息队列中某一刻的Message
列表,尝试以下代码:
private void tryDump() {
// 仅仅用于示例,不建议直接new Thread()
new Thread() {
@Override
public void run() {
while (true) {
Looper.getMainLooper().dump(new Printer() {
@Override
public void println(String x) {
Log.i("SecondActivity", "println: " + x);
}
}, "");
try {
Thread.sleep(500);
} catch (Exception ignore) { }
}
}
}.start();
}
得到类似如下日志:
其中when
是指当前时候之后多少ms之后该执行此Message
,看起来似乎这个SlideShineImageView
有问题,便兴奋的将其修改成普通的ImageView,沮丧的是仍旧有问题,修改后的日志如下:
于是便陷入了沉思,细想可以发现此方法存在致命漏洞:无法打印主线程中所有的Message
!!!再次阅读Looper
的源码,发现有如下代码:
public static void loop() {
// *** 省略部分代码 ***
for (; ; ) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
// *** 省略部分代码 ***
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// *** 省略部分代码 ***
}
}
故若设置了Printer
则可打印出主线程执行的Message
,Looper
中提供了此方法:
/**
* @param printer A Printer object that will receive log messages, or
* null to disable message logging.
*/
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
尝试设置,得到类似如下日志:
通过了解View
的绘制了解便知,主线程累积的Message
是系统的绘制消息。一般来说,只有调用了View#requestLayout()
或者View#invalidate()
才会引起重绘
2.罪魁祸首是谁
虽然知道了问题的原因,但是由于View
纵多,直接在所有View
中增加日志工作量大且容易做无用功,联想到View
本身是一棵树,View#requestLayout()
或者View#invalidate()
最终肯定会回调到树根上(DecorView
),故可以尝试通过重写FirstActivity
的ContentView
中的某些方法从而找到问题View
。
查看View#requestLayout()
源码:
public void requestLayout() {
// *** 省略部分代码 ***
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
最终回调到ViewRootImpl#requestLayout()
,代码如下:
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
看代码意思便是接下来会触发View的绘制流程相关逻辑,故尝试使用如下ViewGroup
作为FirstActivity
的根布局:
public class FirstActivityRootLayout extends FrameLayout {
private static final String TAG = "FirstActivityRootLayout";
public FirstActivityRootLayout(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
}
@Override
public void requestLayout() {
Log.i(TAG, "requestLayout: ", new Exception());
super.requestLayout();
}
}
但是requestLayout()
的代码逻辑执行正常,于是便查看View#invalidate()
的相关流程,invalidate()
代码如下:
public void invalidate() {
invalidate(true);
}
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
// *** 省略部分代码
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
// *** 省略部分代码
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);
}
// *** 省略部分代码
}
}
从上述代码可知,invalide()
只会刷新当前View
所在的区域,故直接在FirstActivityRootLayout
重写invalide()
方法是不能找到问题View
的,因为其可能不是全屏的;注意到上述代码中的invalidateChild()
方法,其在ViewGroup
中代码如下:
/**
* Don't call or override this method. It is used for the implementation of
* the view hierarchy.
*
* @deprecated Use {@link #onDescendantInvalidated(View, View)} instead to observe updates to
* draw state in descendants.
*/
@Deprecated
@Override
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
// HW accelerated fast path
onDescendantInvalidated(child, child);
return;
}
ViewParent parent = this;
if (attachInfo != null) {
// *** 省略部分代码
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
// *** 省略部分代码
parent = parent.invalidateChildInParent(location, dirty);
// *** 省略部分代码
} while (parent != null);
}
}
此方式是final
方法,故无法被子类重写;支持硬件加速的设备会走onDescendantInvalidated()
,否则走invalidateChildInParent()
,其最终都会调用到ViewRootImpl#scheduleTraversals()
,触发View的绘制流程相关逻辑,注意到这两个方法均非final
,于是修改FirstActivityRootLayout
代码:
public class FirstActivityRootLayout extends FrameLayout {
private static final String TAG = "FirstActivityRootLayout";
public FirstActivityRootLayout(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
}
@Override
public void requestLayout() {
Log.i(TAG, "requestLayout: ", new Exception());
super.requestLayout();
}
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
Log.i(TAG, "invalidateChildInParent: ", new Exception());
return super.invalidateChildInParent(location, dirty);
}
@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
super.onDescendantInvalidated(child, target);
Log.i(TAG, "onDescendantInvalidated: ", new Exception());
}
}
终于,满屏重复日志:
故问题原因便是:在自定义TextView#onDraw()
中调用了setTextColor()
,间接调用了invalidate()
,造成循环调用,从而造成主线程中全是请求绘制的消息,于是便出现onPause()
未调用,onDestroy()
延迟了10s调用现象。
- 站在巨人的肩膀上,再次感谢
ZCJ风飞
大佬:
深入分析Android中Activity的onStop和onDestroy()回调延时及延时10s的问题