忽然有一天,我想要做一件事:去代码中去验证那些曾经被“灌输”的理论。
-- 服装学院的IT男【Android 13源码分析】
在我知识体系里,如果说旋转屏幕后一个 Activity 如果不想重启(排除强制横竖屏场景),就在清单文件加上="orientation|screenSize”就不会走重启流程,如果没加,旋转屏幕则会走重启流程,但是现在发现好像也不一定是这样,也不知道是bug 还是个google有意为之,难道还有别的地方可以控制屏幕旋转后 Activity 不走重启逻辑~。
本篇记录项目中遇到一个奇怪的问题,解决方案是有了,但是问题的根本原因还没有找到。写这篇的目的一是为了记录,二也是希望有道友能够指点迷津。
1. 问题描述
问题现象: 在高通预置的应用内的一个 Activity 旋转后,界面的 EditText 内容被置空了。
期望结果: 旋转后 EditText 内容保留。
刚看到这个问题感觉就是一个普通的旋转后 Activity 重启的问题,查看日志后确实也看到了重启的日志。 查看该 Activity 的清单文件,发现配置如下:
android:configChanges="orientation|keyboardHidden|layoutDirection|fontScale|locale"
看这个配置,原业务逻辑加了 “orientation”说明确实不希望走重启流程,但是这个配置确实是有问题的,需要再加上 “screenSize” 才不会走重启罗辑,修改后验证也正常了。
但是隐隐约约感觉不对,高通不应该犯这么简单的错误,然后拿了一个同为 T 平台的另一个高通项目验证,发现另一个项目是正常的,用 BCompare 比对了2个项目的应用的代码后,发现代码一模一样。
为了便于描述,把有问题的项目成为项目A,显示正常的成为项目 B。
现在感觉更不对了, 项目 A,B 都是 T 平台的高通项目,应用代码也没区别,按理说表现都是一致的,但是可能基线版本不一样,于是我又写了一个demo, Activity 在清单文件的配置与高通应用保持一致。 然后发现这个 demo 的 Activity 在项目A,B 都走了重启逻辑。
问题愈发迷惑,整理下信息:
-
- 同高通T平台项目,同样的高通应用代码,一个重启,一个不重启
-
- 清单文件一样的配置的2个 Activity ,在项目B上,高通的那个应用不重启,我的demo重启
按照已知只是,清单文件需要加 “orientation|screenSize” 才会走 onConfigurationChanged 回调,如果只有 “orientation” 是会走重启的。 目前几个验证操作,发现只有在项目B上,高通自己的那个应用不会重启,其他的还是走正常重启逻辑。
为了搞懂问题原因,还是得撸一遍代码。(结论是还没搞懂~)
2. 代码分析
2.1 旋转后重启逻辑
旋转后会走到 ActivityRecord::ensureActivityConfiguration 在这个方法会对清单文件的配置做判断,Activity 是否要走重启逻辑。
# ActivityRecord
boolean ensureActivityConfiguration(int globalChanges, boolean preserveWindow,
boolean ignoreVisibility) {
......//忽略其他代码,有很多的ProtoLog
// 计算出改变
final int changes = getConfigurationChanges(mTmpConfig);
// 这里会输出这次配置的改变,由此可判断当前Activity清单文件是否配置了忽略,要不要走重启流程
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration changes for %s, "
+ "allChanges=%s", this, Configuration.configurationDiffToString(changes));
......
// 重点 根据清单文件的配置判断是不是走重启逻辑
if (shouldRelaunchLocked(changes, mTmpConfig) || forceNewConfig) {
// Aha, the activity isn't handling the change, so DIE DIE DIE.
configChangeFlags |= changes;
// 冻屏
startFreezingScreenLocked(globalChanges);
......
if (mState == PAUSING) {
......
} else {
// 根据日志走的这
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Config is relaunching %s",
this);
if (!mVisibleRequested) {
ProtoLog.v(WM_DEBUG_STATES, "Config is relaunching invisible "
+ "activity %s called by %s", this, Debug.getCallers(4));
}
// 重启处理
relaunchActivityLocked(preserveWindow);
}
return false;
}
......
}
这个方法代码如下,打开 WM_DEBUG_CONFIGURATION 后可以看到都会打印 "Configuration changes for XXX“ 这段日志,这个日志很重要,打印了本次 Configuration 哪些改变, 然后执行 ActivityRecord::shouldRelaunchLocked 这个方法就是判断是否要走重启逻辑,如果走重启,则会打印 "Config is relaunching XXX"。 也就是说可以根据这个日志来判断 Activity 是不是走了重启逻辑。
Activity 如果需要重启会执行 ActivityRecord::relaunchActivityLocked ,在这个方法里会打印 wm_relaunch_resume_activity 日志,并且构建执行 ActivityRelaunchItem 来触发重启逻辑。
# ActivityRecord
void relaunchActivityLocked(boolean preserveWindow) {
......
// 正常为true
final boolean andResume = shouldBeResumed(null /*activeActivity*/);
......
// 这里的events 日志很重要,可以知道到底走的哪个逻辑
if (andResume) {
// wm_relaunch_resume_activity
EventLogTags.writeWmRelaunchResumeActivity(mUserId, System.identityHashCode(this),
task.mTaskId, shortComponentName);
} else {
// wm_resume_activity
EventLogTags.writeWmRelaunchActivity(mUserId, System.identityHashCode(this),
task.mTaskId, shortComponentName);
}
// ActivityRelaunchItem 事务的创建与执行
......
}
所以判断是否要重启的关键在于 ActivityRecord::shouldRelaunchLocked 的返回值,这个方法如下:
/**
* When assessing a configuration change, decide if the changes flags and the new configurations
* should cause the Activity to relaunch.
*
* @param changes the changes due to the given configuration.
* @param changesConfig the configuration that was used to calculate the given changes via a
* call to getConfigurationChanges.
*/
private boolean shouldRelaunchLocked(int changes, Configuration changesConfig) {
// 判断当前配置的改变是否在 清单文件配置了需要忽略
int configChanged = info.getRealConfigChanged();
boolean onlyVrUiModeChanged = onlyVrUiModeChanged(changes, changesConfig);
// Override for apps targeting pre-O sdks
// If a device is in VR mode, and we're transitioning into VR ui mode, add ignore ui mode
// to the config change.
// For O and later, apps will be required to add configChanges="uimode" to their manifest.
if (info.applicationInfo.targetSdkVersion < O
&& requestedVrComponent != null
&& onlyVrUiModeChanged) {
configChanged |= CONFIG_UI_MODE;
}
// 如果都被忽略了则就是0,返回false,不需要relaunch
return (changes&(~configChanged)) != 0;
}
这个方法简单理解下来就是对看传进来的标志位里改变的配置,是不是都被 Activity 在清单文件忽略了,如果是,就不用重启,如果不是,则位运算不为0,返回true, 表示 Activity 需要重启。
也就是说,当前的问题是高通的那个 Activity 在这里返回了 false。
根据自己加的日志以及已有的proto 日志发现这里返回 false 的原因在于传进来的参数。
下面的日志是不重启的,所以问题就在这个 changes ,为什么少了 "CONFIG_SCREEN_SIZE" 。
而这个 changes 的赋值在 ActivityRecord::getConfigurationChanges 方法,所以要看看这个方法的具体实现
2.2 配置的改变--ActivityRecord::getConfigurationChanges
# ActivityRecord
private int getConfigurationChanges(Configuration lastReportedConfig) {
// Determine what has changed. May be nothing, if this is a config that has come back from
// the app after going idle. In that case we just want to leave the official config object
// now in the activity and do nothing else.
int changes = lastReportedConfig.diff(getConfiguration());
android.util.Log.d("biubiubiu", " getConfigurationChanges 1: "+Configuration.configurationDiffToString(changes));
changes = SizeConfigurationBuckets.filterDiff(
changes, lastReportedConfig, getConfiguration(), mSizeConfigurations);
android.util.Log.d("biubiubiu", " getConfigurationChanges 2: "+Configuration.configurationDiffToString(changes));
// We don't want window configuration to cause relaunches.
if ((changes & CONFIG_WINDOW_CONFIGURATION) != 0) {
changes &= ~CONFIG_WINDOW_CONFIGURATION;
}
android.util.Log.d("biubiubiu", " getConfigurationChanges 3: "+Configuration.configurationDiffToString(changes));
return changes;
}
这边加上3个打印后,输出如下:
可以看到第二个地方就开始少了 “CONFIG_SCREEN_SIZE” ,也就是说执行完 SizeConfigurationBuckets::filterDiff 后就出现了异常。
# SizeConfigurationBuckets
public static int filterDiff(int diff, @NonNull Configuration oldConfig,
@NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets) {
if (buckets == null) {
return diff;
}
android.util.Log.d("biubiubiu", " filterDiff diff 1: "+Configuration.configurationDiffToString(diff));
android.util.Log.d("biubiubiu", " filterDiff oldConfig: "+oldConfig + " newConfig = " + newConfig + " buckets=" +buckets );
final boolean nonSizeLayoutFieldsUnchanged =
areNonSizeLayoutFieldsUnchanged(oldConfig.screenLayout, newConfig.screenLayout);
// 如果目前标记了 CONFIG_SCREEN_SIZE 则进行再次确认
if ((diff & CONFIG_SCREEN_SIZE) != 0) {
final boolean crosses = buckets.crossesHorizontalSizeThreshold(oldConfig.screenWidthDp,
newConfig.screenWidthDp)
|| buckets.crossesVerticalSizeThreshold(oldConfig.screenHeightDp,
newConfig.screenHeightDp);
if (!crosses) {
// 撤销了 CONFIG_SCREEN_SIZE 标志位
diff &= ~CONFIG_SCREEN_SIZE;
}
}
android.util.Log.d("biubiubiu", " filterDiff diff 2: "+Configuration.configurationDiffToString(diff));
......//忽略其他对diff的赋值
return diff;
}
通过日志和代码可以确定,就是在这里 撤销了 CONFIG_SCREEN_SIZE 标志位 ,要进入撤销的条件就是 crosses = false ,而这个值根据目前理解是说屏幕宽高是否发生了交互改变(横竖屏切换的话,确实就是宽高交换)
而对比正常的流程,这个 crosses = true 。 所以下一步就是确定这个 crosses 什么场景下为 false 。
需要 crossesHorizontalSizeThreshold 和 crossesVerticalSizeThreshold 都为 false 的时候 crosses 才为 false。
而正常逻辑 crossesHorizontalSizeThreshold 返回 true 后就结束了,不会执行后面的 crossesVerticalSizeThreshold 。
所以先看一下 crossesHorizontalSizeThreshold
# SizeConfigurationBuckets
/** Horizontal (screenWidthDp) buckets */
@Nullable
private final int[] mHorizontal;
/** Vertical (screenHeightDp) buckets */
@Nullable
private final int[] mVertical;
private boolean crossesHorizontalSizeThreshold(int firstDp, int secondDp) {
return crossesSizeThreshold(mHorizontal, firstDp, secondDp);
}
@VisibleForTesting
public static boolean crossesSizeThreshold(int[] thresholds, int firstDp,
int secondDp) {
android.util.Log.d("biubiubiu", "crossesSizeThreshold thresholds= "+Arrays.toString(thresholds) + " firstDp="+ firstDp+ " firstDp="+ secondDp );
if (thresholds == null) {
return false;
}
for (int i = thresholds.length - 1; i >= 0; i--) {
final int threshold = thresholds[i];
if ((firstDp < threshold && secondDp >= threshold)
|| (firstDp >= threshold && secondDp < threshold)) {
return true;
}
}
return false;
}
@Override
public String toString() {
return Arrays.toString(mHorizontal) + " " + Arrays.toString(mVertical) + " "
+ Arrays.toString(mSmallest) + " " + Arrays.toString(mScreenLayoutSize) + " "
+ mScreenLayoutLongSet;
}
SizeConfigurationBuckets::crossesHorizontalSizeThreshold 和 SizeConfigurationBuckets::crossesVerticalSizeThreshold 方法内部其实都是执行 SizeConfigurationBuckets::crossesSizeThreshold 方法,只不过传递的参数不同。
第2,3 个参数就是之前的宽(高)和现在最新的宽(高)。然后做一堆计算,这2个参数一般不会有问题。 而第一个参数,却会出现异常。
根据代码,如果第一个参数是 null,则直接返回 false 了,或者后面的比较不满足条件也返回 false 。 宽(高)参数就是 Configuration 里的,这个前面也有打印没啥问题,主要是第一个参数。
第一个参数传进来的是 mHorizontal 或者 mVertical 。这个类的 toString 方法有打印。 我在 SizeConfigurationBuckets::filterDiff 法也加上了打印。 对比正常和移除的日志如下:
发现异常的执行了2次, 正常重启的 Activity 就打印了一次。 这是因为前面说了 crosses 的赋值 是2个方法的 “ || ” ,所以正常情况下,执行第一个返回true,就不会执行 “ || ” 的方法了,也就只打印了一次。 而当前异常的这个逻辑, “ || ” 前面的方法也就是 SizeConfigurationBuckets::crossesHorizontalSizeThreshol 方法返回了false,(打印一次),所以会再执行 SizeConfigurationBuckets::crossesVerticalSizeThreshold (打印第二次)。
然后这里的打印要对着 SizeConfigurationBuckets::toString 方法,这个方法是打印了5个对象,看日志打印也是分为了5块, 我们之关系前面2个就好,分别对应 mHorizontal 和 mVertical。
通过日志可以知道异常逻辑下 mHorizontal = null, mVertical = [720] 。 把这2个值带入到 SizeConfigurationBuckets::crossesSizeThreshold 方法 。2次执行都会返回 false 。
最终结果就是把这次的 CONFIG_SCREEN_SIZE 标志位撤销了,最终让Activity不走重启逻辑。
正常情况打印应该是下面的那个 mHorizontal = [320, 360, 480, 600] , mVertical = [320, 360, 480, 550, 720] 。
2.3 mHorizontal 和 mVertical
所以现在的是需要找到 mHorizontal 和 mVertical 是怎么赋值的,为什么会出现异常的赋值情况。
这2个属性的赋值在构造 SizeConfigurationBuckets 对象的时候,不过它有2个构造方法。
# SizeConfigurationBuckets
/** Horizontal (screenWidthDp) buckets */
@Nullable
private final int[] mHorizontal;
/** Vertical (screenHeightDp) buckets */
@Nullable
private final int[] mVertical;
// 直接构建
public SizeConfigurationBuckets(
@Nullable int[] horizontal,
@Nullable int[] vertical,
@Nullable int[] smallest,
@Nullable int[] screenLayoutSize,
boolean screenLayoutLongSet) {
this.mHorizontal = horizontal;
this.mVertical = vertical;
android.util.Log.d("biubiubiu", "mVertical 1= "+Arrays.toString(mVertical));
this.mSmallest = smallest;
this.mScreenLayoutSize = screenLayoutSize;
this.mScreenLayoutLongSet = screenLayoutLongSet;
// onConstructed(); // You can define this method to get a callback
}
// 序列化构建
SizeConfigurationBuckets(@NonNull android.os.Parcel in) {
// You can override field unparcelling by defining methods like:
// static FieldType unparcelFieldName(Parcel in) { ... }
byte flg = in.readByte();
boolean screenLayoutLongSet = (flg & 0x10) != 0;
int[] horizontal = (flg & 0x1) == 0 ? null : in.createIntArray();
int[] vertical = (flg & 0x2) == 0 ? null : in.createIntArray();
int[] smallest = (flg & 0x4) == 0 ? null : in.createIntArray();
int[] screenLayoutSize = (flg & 0x8) == 0 ? null : in.createIntArray();
this.mHorizontal = horizontal;
this.mVertical = vertical;
android.util.Log.d("biubiubiu", "mVertical 2= "+Arrays.toString(mVertical));
this.mSmallest = smallest;
this.mScreenLayoutSize = screenLayoutSize;
this.mScreenLayoutLongSet = screenLayoutLongSet;
// onConstructed(); // You can define this method to get a callback
}
触发的地方在应用进程 ActivityThread::reportSizeConfigurations 方法。
LaunchActivityItem 事务会触发 ActivityThread::handleLaunchActivity ,然后就会触发 ActivityThread::reportSizeConfigurations 方法的执行。
# ActivityThread
private void reportSizeConfigurations(ActivityClientRecord r) {
if (mActivitiesToBeDestroyed.containsKey(r.token)) {
// Size configurations of a destroyed activity is meaningless.
return;
}
// 获取资源配置
Configuration[] configurations = r.activity.getResources().getSizeConfigurations();
if (configurations == null) {
return;
}
// 创建一个新的SizeConfigurationBuckets对象,存储从配置数组中获取的尺寸配置
r.mSizeConfigurations = new SizeConfigurationBuckets(configurations);
// 跨进程通知 system_service 报告活动的尺寸配置信息
ActivityClient.getInstance().reportSizeConfigurations(r.token, r.mSizeConfigurations);
}
这边看到先是直接 new 了一个 SizeConfigurationBuckets 对象,传递进去的是 Configuration 数组,然后下面的跨进程其实也会构建一个 SizeConfigurationBuckets 。
# ActivityClient
void reportSizeConfigurations(IBinder token, SizeConfigurationBuckets sizeConfigurations) {
try {
// 拿到ActivityClientController
getActivityClientController().reportSizeConfigurations(token, sizeConfigurations);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
# ActivityClientController
public void reportSizeConfigurations(IBinder token,
SizeConfigurationBuckets sizeConfigurations) {
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Report configuration: %s %s",
token, sizeConfigurations);
synchronized (mGlobalLock) {
final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
if (r != null) {
r.setSizeConfigurations(sizeConfigurations);
}
}
}
可以看到这里也有日志打印 SizeConfigurationBuckets 对象了,打印结果和之前的看到的一致。有人可能会好奇,这里和构建 SizeConfigurationBuckets 什么关系?
这个方法是个binder 跨进程通信,会序列化创建 SizeConfigurationBuckets 对象,看一下下面的代码就知道了。
目前已经看到了个构建 SizeConfigurationBuckets 对象的地方,但是问题的本质还是 ActivityThread::reportSizeConfigurations 方法中获取的 Configuration 数组有问题,导致的最终 Activity 不走重启逻辑。
Configuration[] configurations = r.activity.getResources().getSizeConfigurations();
但是这句代码是什么情况,获取到的值是什么,我目前看到逻辑走到了 native 层。对这块没有了解,暂时还没发现问题所在。
3. 未完待续~
目前看不下去了,后续还会继续研究,如果有知道的道友还请指教。