前言
因为对于应用层来说,很多的API等于是黑盒,所以往往会发生一些始料未及的,甚至是十分奇怪的问题。如果只是在应用侧进行尝试,无疑是大海捞针,十分费力。这个时候如果能够灵活地把Android的开源特性利用起来,在Framework层面,在Server层面调查问题,就可以快速地定位问题,解决问题,我认为这可以说是一种“降维打击”的战术。下面用一个例子来说明我的实践方法。文章有点长,文字比较朴实,希望你能耐心读完,我认为对你应该会有一些启发。
问题描述
在进行项目的测试时发现一个切换Display Size的case无法通过。经过分析是因为Activity重建导致的,但原来已经指定了如下属性:
android:configChanges="orientation|screenSize|screenLayout|density"
这个并没有任何修改,于是猜测是osv变更导致的,因为我们修改了应用的target sdk到Q,于是写TP测试。
奇怪的事情发生了!TP表现出了跟原来O版本相同的行为,即Activity没有重建,而是调用了onConfigurationChanged方法。
调查过程
首先调试有问题的重建的Activity的onDestory方法,通过方法调用栈。看到了这个方法:
private void handleRelaunchActivityInner(ActivityClientRecord r, int configChanges,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingIntents,
PendingTransactionActions pendingActions, boolean startsNotResumed,
Configuration overrideConfig, String reason) {
}
虽然没有详细研究过代码,但猜测framework需要根据系统真实的configChange来跟Activity设定的configChange的值进行比较来决定是否重建Activity。所以就需要找到比较的地方。
通过对上述代码中的configChanges的追踪(反复ctrl+左键,查找引用),找到了如下代码:
com/android/server/wm/ActivityRecord.java
//一度以为是forceNewConfig导致的,最终发现不是。
if (shouldRelaunchLocked(changes, mTmpConfig) || forceNewConfig) {
// Aha, the activity isn't handling the change, so DIE DIE DIE.
configChangeFlags |= changes;
startFreezingScreenLocked(app, globalChanges);
forceNewConfig = false;
preserveWindow &= isResizeOnlyChange(changes);
final boolean hasResizeChange = hasResizeChange(changes & ~info.getRealConfigChanged());
if (hasResizeChange) {
final boolean isDragResizing =
getTaskRecord().getTask().isDragResizing();
mRelaunchReason = isDragResizing ? RELAUNCH_REASON_FREE_RESIZE
: RELAUNCH_REASON_WINDOWING_MODE_RESIZE;
} else {
mRelaunchReason = RELAUNCH_REASON_NONE;
}
//省略部分代码,下面就是执行重建Activity的调用
}
private boolean shouldRelaunchLocked(int changes, Configuration changesConfig) {
int configChanged = info.getRealConfigChanged();
boolean onlyVrUiModeChanged = onlyVrUiModeChanged(changes, changesConfig);
if (appInfo.targetSdkVersion < O
&& requestedVrComponent != null
&& onlyVrUiModeChanged) {
configChanged |= CONFIG_UI_MODE;
}
//很明显比较是否需要重建,当任意一个FLAG,Activity不包含就执行重建。
return (changes&(~configChanged)) != 0;
}
于是在上面的位置Debug,之后发现下面的数据:
real | change | |
---|---|---|
重建的App | 5507 CONFIG_DENSITY CONFIG_SCREEN_SIZE CONFIG_SCREEN_LAYOUT CONFIG_ORIENTATION CONFIG_MCC CONFIG_MNC | 6400 CONFIG_DENSITY CONFIG_SMALLEST_SCREEN_SIZE CONFIG_SCREEN_LAYOUT |
没有重建的App | 5507 | 4352 CONFIG_DENSITY CONFIG_SCREEN_LAYOUT |
这证明就是changes不同导致的,重建的情形多了一个CONFIG_SMALLEST_SCREEN_SIZE,那么为什么两个Activity的changes会不同呢,注意这里不是RealConfigChanged不同,RealConfigChanged是Activity自己通过Manifest定义的,changes是WMS根据实际的变化生产的。
这时继续向上追溯,看是什么导致了两的Activity的changes不同。然后就发现了下面的代码:
private int getConfigurationChanges(Configuration lastReportedConfig) {
final Configuration currentConfig = getConfiguration();
int changes = lastReportedConfig.diff(currentConfig);
// We don't want to use size changes if they don't cross boundaries that are important to
// the app.
if ((changes & CONFIG_SCREEN_SIZE) != 0) {
final boolean crosses = crossesHorizontalSizeThreshold(lastReportedConfig.screenWidthDp,
currentConfig.screenWidthDp)
|| crossesVerticalSizeThreshold(lastReportedConfig.screenHeightDp,
currentConfig.screenHeightDp);
if (!crosses) {
changes &= ~CONFIG_SCREEN_SIZE;
}
}
if ((changes & CONFIG_SMALLEST_SCREEN_SIZE) != 0) {
final int oldSmallest = lastReportedConfig.smallestScreenWidthDp;
final int newSmallest = currentConfig.smallestScreenWidthDp;
if (!crossesSmallestSizeThreshold(oldSmallest, newSmallest)) {
//这里看到changes去除了CONFIG_SMALLEST_SCREEN_SIZE属性
changes &= ~CONFIG_SMALLEST_SCREEN_SIZE;
}
}
// We don't want window configuration to cause relaunches.
if ((changes & CONFIG_WINDOW_CONFIGURATION) != 0) {
changes &= ~CONFIG_WINDOW_CONFIGURATION;
}
return changes;
}
private boolean crossesSmallestSizeThreshold(int firstDp, int secondDp) {
return crossesSizeThreshold(mSmallestSizeConfigurations, firstDp, secondDp);
}
//这个方法由上个方法调用,判断是否需要去除CONFIG_SMALLEST_SCREEN_SIZE
private static boolean crossesSizeThreshold(int[] thresholds, int firstDp,
int secondDp) {
if (thresholds == null) {
return false;
}
for (int i = thresholds.length - 1; i >= 0; i--) {
final int threshold = thresholds[i];
//判断是否在两个值之间,本例中firstDp和secondDp是320,376
if ((firstDp < threshold && secondDp >= threshold)
|| (firstDp >= threshold && secondDp < threshold)) {
//返回true表示不去除,返回false表示去除
return true;
}
}
return false;
}
经过debug两个AcitvityRecord的mSmallestSizeConfigurations不同,重建的包含两个数据360、600,而没有重建的只有600。因为360在320和376之间,所以返回了true,不去除属性CONFIG_SMALLEST_SCREEN_SIZE。
这个时候我就有了一个猜测,这个360、600对应资源文件夹的sw360,sw600,经过反编译两个apk证实了我的猜测。然后我想到了本次版本升级加入了androidx.preference,原来没有使用support库,多出来的sw360和sw600就是这个库引入的。
后记
至此问题得解,可以看出这个问题很怪,怪到我会写一篇博文记录下来。如果只是在App侧进行分析测试无疑会非常困难,但从framework侧进行debug和分析却能比较简单的找到答案。当然工作中这样的问题有很多,记录下这个,希望对看到这篇博文的人有一些帮助或者启发。