OTA升级后多个应用crash

问题描述

Android P的项目,进行OTA升级。升级后多个应用出现异常:启动时crash,切换系统语言后应用名称和图标混乱。恢复出厂设置后正常。

问题场景

OTA高版本主要是进行了GMS包的升级,也就是对GMS包中的应用进行了升级,而出现问题的应用也都是GMS包中的应用。

问题分析过程

首先当然是收集log。好在问题是必现的,所以就先抓了下有问题的应用的启动log,结果发现各种各样的错误都有:类找不到的、资源找不到的、AndroidManifest文件出错的……毫无规律,加上应用名字和图标的错误,反正问题的整个感觉就是乱。再加上恢复出厂设置后问题会消失,所以基本确定不是应用本身的问题,而是在升级过程中应用的更新出了问题。

接着用adb shell dumpsys package对比了一下OTA前后应用的信息,发现升级前后应用的版本号等信息都没有变化,但是恢复出场后的应用版本是不一样的,但是/system分区中的应用拉出来看是换成了新应用的。很明显,就是在升级过程中应用的扫描出了问题,导致应用的信息没有得到更新。那应用的信息为什么没有升级成功呢?恢复出厂设置后问题消失,说明新的版本也是没问题的,恢复出厂主要就是清除/data中的东西,而OTA升级是不会清除/data的,是不是/data中缓存了什么东西导致了应用的信息没有得到更新?

对这部分的代码不是很清楚,所以就用了个本办法,在PackageManagerService中搜索“OTA”,找到了下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// If this is first boot after an OTA, and a normal boot, then
// we need to clear code cache directories.
// Note that we do *not* clear the application profiles. These remain valid
// across OTAs and are used to drive profile verification (post OTA) and
// profile compilation (without waiting to collect a fresh set of profiles).
if (mIsUpgrade && !onlyCore) {
Slog.i(TAG, "Build fingerprint changed; clearing code caches");
for (int i = 0; i < mSettings.mPackages.size(); i++) {
final PackageSetting ps = mSettings.mPackages.valueAt(i);
if (Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, ps.volumeUuid)) {
// No apps are running this early, so no need to freeze
clearAppDataLIF(ps.pkg, UserHandle.USER_ALL,
StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE
| Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
}
}
ver.fingerprint = Build.FINGERPRINT;
}

里面有个log输出,在正常升级中应该是能输出的,但是做了到userdebug的升级包,抓了log后没发现这个记录,也就是说mIsUpgrade && !onlyCore这条件不满足。正常启动中!onlyCore这个条件肯定是满足的(加密第一次启动时才不满足),所以mIsUpgrade这个是false的,也就是说PKMS压根就没觉得这是升级了。

看看mIsUpgrade的值是怎么确定的:

1
2
final VersionInfo ver = mSettings.getInternalVersion();
mIsUpgrade = !Build.FINGERPRINT.equals(ver.fingerprint);

也就是说比较两个版本的指纹,如果不相同才判定为升级。mSettings.getInternalVersion()的结果可以在dumpsys package的头几行看到:

1
2
3
4
5
6
7
Database versions:
Internal:
sdkVersion=28 databaseVersion=3
fingerprint=Mobicel/HYPE/HYPE:8.1.0/O11019/1533524463:user/release-keys
External:
sdkVersion=28 databaseVersion=3
fingerprint=Mobicel/HYPE/HYPE:8.1.0/O11019/1533524463:user/release-keys

然后看了两个版本的指纹,发现确实是一样的,所以PKMS认为这不是在升级。

在PKMS的构造方法中会对预置的apk进行扫描,不管是不是升级、是不是第一次启动,然后和packages.xml里的信息进行比较。前面也说了手机里的apk已经发生变化了,那么按说扫描过程中也会更新应用的信息吧?这就又回到刚才说的缓存问题了。在PKMS的构造方法里看了下,发现了下面的内容:

1
mCacheDir = preparePackageParserCache(mIsUpgrade);

看起来和缓存有关,而且还和升级有关,很可能就是这个了。mCacheDir的作用没有注释,那就进这方法里看下吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private static File preparePackageParserCache(boolean isUpgrade) {
if (!DEFAULT_PACKAGE_PARSER_CACHE_ENABLED) {//是否开启缓存
return null;
}

// Disable package parsing on eng builds to allow for faster incremental development.
if (Build.IS_ENG) {//ENG版本不使用缓存
return null;
}

if (SystemProperties.getBoolean("pm.boot.disable_package_cache", false)) {
Slog.i(TAG, "Disabling package parser cache due to system property.");
return null;
}

// The base directory for the package parser cache lives under /data/system/.
final File cacheBaseDir = FileUtils.createDir(Environment.getDataSystemDirectory(),
"package_cache");
if (cacheBaseDir == null) {
return null;
}

// If this is a system upgrade scenario, delete the contents of the package cache dir.
// This also serves to "GC" unused entries when the package cache version changes (which
// can only happen during upgrades).
if (isUpgrade) {
FileUtils.deleteContents(cacheBaseDir);
}


// Return the versioned package cache directory. This is something like
// "/data/system/package_cache/1"
File cacheDir = FileUtils.createDir(cacheBaseDir, PACKAGE_PARSER_CACHE_VERSION);

if (cacheDir == null) {
// Something went wrong. Attempt to delete everything and return.
Slog.wtf(TAG, "Cache directory cannot be created - wiping base dir " + cacheBaseDir);
FileUtils.deleteContentsAndDir(cacheBaseDir);
return null;
}

// The following is a workaround to aid development on non-numbered userdebug
// builds or cases where "adb sync" is used on userdebug builds. If we detect that
// the system partition is newer.
//
// NOTE: When no BUILD_NUMBER is set by the build system, it defaults to a build
// that starts with "eng." to signify that this is an engineering build and not
// destined for release.
if (Build.IS_USERDEBUG && Build.VERSION.INCREMENTAL.startsWith("eng.")) {
Slog.w(TAG, "Wiping cache directory because the system partition changed.");

// Heuristic: If the /system directory has been modified recently due to an "adb sync"
// or a regular make, then blow away the cache. Note that mtimes are *NOT* reliable
// in general and should not be used for production changes. In this specific case,
// we know that they will work.
File frameworkDir = new File(Environment.getRootDirectory(), "framework");
if (cacheDir.lastModified() < frameworkDir.lastModified()) {
FileUtils.deleteContents(cacheBaseDir);
cacheDir = FileUtils.createDir(cacheBaseDir, PACKAGE_PARSER_CACHE_VERSION);
}
}

return cacheDir;
}

从上面的代码可以得到这个缓存的以下信息:

  • 可以在代码里通过DEFAULT_PACKAGE_PARSER_CACHE_ENABLED配置是否使用缓存;
  • eng版本默认关闭缓存;
  • 可以通过pm.boot.disable_package_cache属性控制是否使用缓存;
  • 缓存位于/data/system/package_cache/;
  • 升级后第一次开机会清空缓存,然后重新创建;
  • 通过adb sync更新system分区的东西后,也会重新创建缓存;

下面再来看下缓存是怎么对应用的更新产生影响的。以前大致了解过PKMS启动过程,PKMS扫描预置应用包含以下流程,
PKMS构造方法->scanDirTracedLI()-> scanDirLI(),然后就进入了应用解析的流程,来看下scanDirLI()的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//scanDir就是system/app等目录
private void scanDirLI(File scanDir, int parseFlags, int scanFlags, long currentTime) {
final File[] files = scanDir.listFiles();
if (ArrayUtils.isEmpty(files)) {
Log.d(TAG, "No files in app dir " + scanDir);
return;
}
//……
try (ParallelPackageParser parallelPackageParser = new ParallelPackageParser(
mSeparateProcesses, mOnlyCore, mMetrics, mCacheDir,
mParallelPackageParserCallback)) {
// Submit files for parsing in parallel
int fileCount = 0;
for (File file : files) {
final boolean isPackage = (isApkFile(file) || file.isDirectory())
&& !PackageInstallerService.isStageName(file.getName());
if (!isPackage) {
// Ignore entries which are not packages
continue;
}
parallelPackageParser.submit(file, parseFlags);
fileCount++;
}
//……
}
}

在构造parallelPackageParser时会将mCacheDir传递给构造方法,然后在commit()中传递给PackageParser,应用的解析最终都是通过PackageParser进行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Submits the file for parsing
* @param scanFile file to scan
* @param parseFlags parse falgs
*/
public void submit(File scanFile, int parseFlags) {
mService.submit(() -> {
ParseResult pr = new ParseResult();
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parallel parsePackage [" + scanFile + "]");
try {
//……
pp.setCacheDir(mCacheDir);
//……
pr.pkg = parsePackage(pp, scanFile, parseFlags);
} catch (Throwable e) {
pr.throwable = e;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
//……
});
}

之后在下面的方法中调用PackageParser中的方法进行应用解析:

1
2
3
4
protected PackageParser.Package parsePackage(PackageParser packageParser, File scanFile,
int parseFlags) throws PackageParser.PackageParserException {
return packageParser.parsePackage(scanFile, parseFlags, true /* useCaches */);
}

PackageParser解析应用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* If {@code useCaches} is true, the package parser might return a cached
* result from a previous parse of the same {@code packageFile} with the same
* {@code flags}. Note that this method does not check whether {@code packageFile}
* has changed since the last parse, it's up to callers to do so.
*
* @see #parsePackageLite(File, int)
*/
public Package parsePackage(File packageFile, int flags, boolean useCaches)
throws PackageParserException {//parallelPackageParser传过来的useCaches是true的
Package parsed = useCaches ? getCachedResult(packageFile, flags) : null;
if (parsed != null) {
return parsed;
}

long parseTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;
if (packageFile.isDirectory()) {
parsed = parseClusterPackage(packageFile, flags);
} else {
parsed = parseMonolithicPackage(packageFile, flags);
}

long cacheTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;
cacheResult(packageFile, flags, parsed);
//……
return parsed;
}

注释里说的很清楚,如果使用缓存,则可能返回上次解析的结果(路径和flags一样),不管应用是不是发生了变化,这是调用者应该关心的。代码的逻辑也很简单,如果存在缓存就从缓存了获取结果直接返回;否则重新进行解析,并缓存结果。来看下缓存的获取过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* Returns the cached parse result for {@code packageFile} for parse flags {@code flags},
* or {@code null} if no cached result exists.
*/
private Package getCachedResult(File packageFile, int flags) {
if (mCacheDir == null) {
return null;
}

final String cacheKey = getCacheKey(packageFile, flags);
final File cacheFile = new File(mCacheDir, cacheKey);

try {
// If the cache is not up to date, return null.
if (!isCacheUpToDate(packageFile, cacheFile)) {
return null;
}

final byte[] bytes = IoUtils.readFileAsByteArray(cacheFile.getAbsolutePath());
Package p = fromCacheEntry(bytes);
if (mCallback != null) {
String[] overlayApks = mCallback.getOverlayApks(p.packageName);
if (overlayApks != null && overlayApks.length > 0) {
for (String overlayApk : overlayApks) {
// If a static RRO is updated, return null.
if (!isCacheUpToDate(new File(overlayApk), cacheFile)) {
return null;
}
}
}
}
return p;
} catch (Throwable e) {
Slog.w(TAG, "Error reading package cache: ", e);

// If something went wrong while reading the cache entry, delete the cache file
// so that we regenerate it the next time.
cacheFile.delete();
return null;
}
}

该方法会从以byte数组的形式读取缓存然后再把结果转换成Package对象,也就是说缓存里缓存的不是apk本身,而是对Package对象的持久化。另外有三种情况下缓存会返回null:
- 缓存的修改时间比apk的早(可以通过stat命令查看时间);
- apk 任何一个rro的修改时间比缓存修改时间晚;
- 获取缓存出错,此时会删除缓存,等下次再生成;

通过上面的代码可以知道,缓存在两种情况下不会影响升级,一是系统认为是升级,会把缓存清掉,等应用解析完再创建;二是应用(或RRO)的修改时间比缓存晚,按说通过OTA后的应用修改时间应该比旧版本的缓存新的,但是不知道为什么,升级后system/app的修改时间是2009-01-01的(看了Pixel的时间也是这样,不知道是不是bug),而缓存的时间是最近的,所以最后缓存还是生效了……信息是老的,但是运行的时候是用的新应用,然后就出现了乱七八糟的问题。

总结

以前看PKMS扫描应用的时候把缓存部分跳过去了,所以遇到相关问题时还要分析log、重新看代码,解决问题的逻辑也不是很顺畅,不过连蒙带猜结合代码还是找到原因了。虽然发现是指纹的问题,不需要修改,但是补充了一个PKMS的知识点,还是有点收获的。

最后还是简单的总结下这个问题:因为项目还在开发中,为了方便测试,项目负责人设置了一个固定的指纹,导致OTA后PKMS不知道升级了,没有清除缓存;而且OTA后应用的时间是2009年的,导致PKMS认为缓存比应用新,是可用的,所以直接从缓存中反持久化了Package对象,这就导致了应用是新的但是信息是旧的,两者是不匹配的就出了一堆莫名其妙的问题。之前的版本应用没有进行大的改动,所以没发现这个问题,这次恰好升级了GMS包,就暴露了出来。所以说,项目的指纹最好是每个版本自动变化啊……

Powered by Hexo and Hexo-theme-hiker

Copyright © 2018 - 2022 得一 All Rights Reserved.

访客数 : | 访问量 :