近年来,随着计算机硬件的不断发展,32位的旧架构程序的性能瓶颈越来越明显了,适配64位已经是业内公认的大势所趋,从工业化的大型商用软件到移动平台上的app,都已经将适配64位提上了日程。在Android平台,大多数设备都采用Arm架构,最新的64位架构则是Arm64-v8a,全民k歌也将顺应潮流,拥抱64位程序的时代。
本次适配工作由全民k歌Android团队合作完成
ARM架构,也被称作高级精简指令集机器(Advanced RISC Machine),是一个精简指令集(RISC)处理器架构家族,其广泛地使用在许多嵌入式系统设计。由于节能的特点,ARM处理器广泛应用于移动通信领域,符合其主要设计目标为低成本、高性能、低耗电的特性。安谋控股(ARM Holdings),也就是arm公司开发此架构并授权其他公司使用,以供他们实现ARM的某一个架构,开发自主的芯片或者系统模块,也就是soc。目前,世界上移动设备移动设备处理器中,99%都采用了了arm架构。
Arm架构最早于1978年物理学家赫尔曼·豪泽(Hermann Hauser)和工程师Chris Curry,在英国剑桥创办了Cambridge Processing Unit,主要业务是为当地市场供应电子设备。1979年,CPU公司改名为Acorn计算机公司。
1985年,Roger Wilson和Steve Furber设计了他们自己的第一代32位、6MHz的处理器,用它做出了一台RISC指令集的计算机,简称ARM(Acorn RISC Machine)。这就是ARM这个名字的由来。
进入21世纪之后,由于手机的快速发展,arm架构的特性得到了充分的发挥,出货量呈现爆炸式增长,ARM处理器占领了全球手机市场。
2011年,ARMv8架构诞生,Cortex-A32/35/53/57/72/73采用的是该架构,这是ARM公司的首款支持64位指令集的处理器架构。由于ARM处理器的授权内核被广泛用于手机等诸多电子产品,故ARMv8架构作为下一代处理器的核心技术而受到普遍关注。新的架构有如下几个特性:
最新的64位指令集,支持64位操作(指令长度依然为32位);
64位地址;
继承LPAE格式,支持最高48位的虚拟地址;
更新了内存模型,和最新的C++11/C1x标准的内存模型更加统一等等更优秀的特性。
一个直观的表现就是,32位软件最大只能使用2^32=4G的内存,64位则是2^40=1T,可以看到,V8架构在性能方法无疑有着质的提升,程序适配后将有更好的性能表现。
虽然Arm64架构推出的时间也比较早,但由于市面上适配的设备寥寥,android厂商的主要soc提供商高通直到2014年才推出了第一款适配arm64-v8a的产品,同时由于android系统的碎片化和厂商众多,市面上的设备架构分布参差不齐,直到2016年之后,android开发者才逐渐把适配提上日程。全民k歌于2014年上线,最初由于支持arm64的设备市场占比较少,也仅适配了arm32。
但是随着目前设备硬件的发展和软件功能的日益复杂,32位程序的性能瓶颈与局限性也越来越明显,相比于旧的32位架构,适配64位架构有以下优势:
1.提高内存使用效率:32位程序由于内存寻址地址有限,最高能使用的内存就是4G,随着软件功能的越来越复杂,内存的消耗也是水涨船高,32位的内存性能就显得越来越捉襟见肘了。目前k歌外网native crash很大一部分就是由于内存问题导致的,适配Arm64能让该现象有所缓解。
2.优化中高端设备用户体验:随着手机硬件的不断发展换代,支持Arm64位的设备已经逐渐成为主流,尤其是中高端设备,适配Arm64能够充分发挥中高端机器的性能和内存优势,带来更好的用户体验。
3.积累相关技术经验:64位程序是未来大势所趋,越早进行适配,越早积累相关行业和技术经验,能更好支持未来业务发展(Android studio已经计划在Android12或以上的版本的模拟器不再支持32位架构)。
4.提高推广优势:适配最新的架构,渠道方会有一定的资源倾斜,可以在推广期比未适配的app更有优势,在海外发行尤其如此(Google play要求上架的应用必须适配Arm64)。
附上数据参考:
同时,全民k歌正常用户设备(排除黑产)中使用支持Arm64-v8a的占比已经达到百分之90以上:
适配之前,我们也考察了国内各大app的对arm64的适配情况,我们以应用宝的top50应用为例:
目前国内头部app对arm64进行了适配的app并不占多数,过千万DAU的App仅微信,qq、手淘还有优酷视频进行进行了适配。已经适配了arm64的无一例外都没有打包Univesal通用包以降低apk大小,我们也在网上搜索了一下有没有相关团队的适配分享,但是也仅是找到了google官方的As设置说明和一些简单的转载说明,我们能参考的的也就是业内一般采用仅打包单一架构的so来降低apk大小这一点。
既然没有现成的经验可以参考,那也只能靠我们自己探索了。
全民k歌适配的过程中,主要包含以下几个工作:
工程编译配置改造;
so库更新;
so动态加载框架扩展;
应用更新方案;
踩坑记录。
以Android Studio为参考,android app在编译前,可以在对应的build.gradle文件里设置split参数来确定需要包含哪些架构的so库,像这样:
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'x86', 'arm64-v8a'
}
}
include这行表示编译时将armeabi-v7a,x86和arm64-v8a架构三种架构的so打包到apk中,这样同一个apk可以安装到cpu架构为这三种的设备上,如果某个设备的cpu使用来不同于以上三种架构的cpu,安装时会报错无法兼容该设备。但是设备在实际运行时,仅使用apk中包适配自己的最新的那个架构,比如上面同时包含了armeabi-v7a和arm64-v8a两种架构,设备的cpu最新架构是arm64-v8a,虽然设备使用V7a的so也能使用,但会优先选择更新的v8a的so来执行,。
目前Android手机百分之99以上都是arm的架构,同时为了降低apk的大小,全民K歌目前仅打包armeabi-v7a架构的so库,适配arm64也一样,仅针对arm64的设备打包包含64位so,也就是适配之后,我们要打包两个apk,一个给64位的设备使用,一个给比较旧的32位设备使用,结合我们打包机器的相关配置,我们如下配置了build文件:
splits {
def arc = "arm64-v8a"
//获取系统环境变量,是否为编译32位
boolean compile32 = Boolean.valueOf(System.getenv("COMPILE32"))
if (compile32) {
arc = "armeabi-v7a"
}
abi {
enable true
reset()
include arc
universalApk false //是否同时生成一个包含全部 Architecture 的包
}
}
同时,构建机新增了如上配置,true构建32位包,false构建64位包。这样,可以根据对应的配置,来确定打包64位还是32位,我们在构建机器上也新增了两个构建任务,分别编译对应64位的release包和debug包 。另外,为了方便业务区分当前的apk是32位还是64位,在buildConfig配置中新增了一个自定义字段来标记:
boolean compile32 = Boolean.valueOf(System.getenv("COMPILE32"))
buildConfigField("boolean", "COMPILE32", "$compile32")
业务可以这样使用:
if (BuildConfig.COMPILE32) {
//32位
} else{
//64位
}
另外,我们需要在存放so的jni路径里新建一个arm64-v8a文件夹,用于存放64位的so文件:
到这,我们的编译配置就完成了,业务同学可以根据自己的需要按需打包对应的apk。
全民k歌的业务多且复杂,更新so库之前,我们以模块为维度对so进行了整理,共有120多个so文件需要更新,包括k歌的自研模块和第三方sdk。
自研模块需要我们自己调整我们native c&c++模块的编译配置重新编译对应的so,以Android Stuido使用ndk,编译脚本使用cmake为例,可以在build.gralde这样配置:
defaultConfig {
applicationId "com.test.mymodule"
minSdkVersion 19
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
}
abiFilters就是编译的对应架构,run完毕之后,如果没有编译错误,在build output文件夹就能看到对应的so文件了。各业务更新好so之后,添加到上面工程对应路径libs/arm64-v8a即可,如果对应的java接口没有变更,到这就算完成里初步的适配了,剩下的就是功能调试和业务自测。
第三方sdk的更新的过程相对来说比较简单,主要就是问第三方要对应的更新,但是也有许多注意事项,当前版本就提供了64位so的是最理想的情况,不用做任何改动或就是简单的把so丢到对应目录里就行,但是如果涉及到更新就比较复杂了。我们这里总结了以下几个要点:
1.只更新so不用更新sdk,更新后简单在64位包测试一遍相关功能即可;
2.需要更新sdk,则需要走sdk更新流程,包括32位包和64位包完整的功能测试和性能测试,如果java层接口有变更,还需要相关业务进行适配调整;
3.最好能要到对应so的符号表,方便查询native crash堆栈;
4.如果发现有已经不再维护或者其他原因没有办法提供64位的sdk,则需要有兜底方案解决。
一般来说,so库都是我们打包时内置在apk中,然后在系统安装app的时候将so拷贝到对应到加载路径当中。但全民k歌的业务多且杂,导致so也不少,全部内置会让apk体积膨胀不少,为了避免so的数量对apk对体积影响太大,我们为此开发了一套可以按需冷加载so的框架DynamicResLoader。
DynamicResLoader资源动态加载器可以让业务可以将不需要首次加载的资源文件抽离,需要用到的时候再从网络下载解压使用,业务只需要设置好对应的资源下载地址和版本号,模块名,配置完毕之后简单调用load方法,在对应回调中处理业务逻辑即可,支持抽离的文件除了简单的图片,文件等资源外,还支持so文件。
框架包括加载模块和对应的配置文件自动生成脚本。
加载模块的主要流程如下:
1.ResourceManager初始化时把对应的资源信息注册生成对应对资源State,并设置state的初始状态;
2.业务调用load,框架调用loadTask调用Download接口下载资源,进行资源的解压校验;
3.校验通过调用NativeLoad加载资源,并回调业务资源加载状态;
为了轻量化实现,框架的下载功能和本地信息的存储功能都是接口化通过代理模式抛给业务自己实现的,以便在基础功能上的性能和业务对齐。
框架初始化时,会将资源的信息配置保存在一个map中记录资源的状态,同时会将资源的保存路径通过反射注入到系统的so搜索路径当中,这样当相关业务调用System.load或者System.loadLibrary时会搜索我们对应的资源储存路径:
// 将资源文件目录添加至系统目录
DexUtils.installDexAndSo(mContext, this.getClass().getClassLoader(), null, path);
/**
* @param cl classloader
* @param dexFile dex的压缩文件,可以是apk,zip
* @param soDir so存放路径
* @throws Throwable
*/
public static void installDexAndSo(Context context, ClassLoader cl, @Nullable File dexFile, @Nullable File soDir) throws Throwable {
if (Build.VERSION.SDK_INT >= 26) {
V26.install(cl, dexFile == null ? null : Arrays.asList(dexFile), context.getFilesDir(), soDir);
} else if (Build.VERSION.SDK_INT >= 25) {
V25.install(cl, dexFile == null ? null : Arrays.asList(dexFile), context.getFilesDir(), soDir);
} else if (Build.VERSION.SDK_INT >= 23) {
V23.install(cl, dexFile == null ? null : Arrays.asList(dexFile), context.getFilesDir(), soDir);
} else if (Build.VERSION.SDK_INT >= 19) {
V19.install(cl, dexFile == null ? null : Arrays.asList(dexFile), context.getFilesDir(), soDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(cl, dexFile == null ? null : Arrays.asList(dexFile), context.getFilesDir(), soDir);
}
}
也可以发现,该框架最初的设计仅是一个模块一个资源包,一个资源包也只能包含一种架构的so文件,这样很明显无法满足我们适配多架构的需求,需要对框架进行功能拓展。
配置自动文件生成脚本
自动生成脚本是动态资源加载框架配套的gradle脚本,主要用于自动生成资源配置文件,避免人为修改配置文件导致的问题。
自动脚本主要包括以下几个步骤:
1.读取业务配置的资源信息,包括模块名,下载链接,版本号等;
2.读取已有资源的缓存信息,如果已有缓存信息,对比缓存中的资源版本号和配置中的版本号,版本号不一致则开始更新对应的资源信息;
3.把需要更新的资源信息的对应资源进行下载解压,读取每一个解压文件的大小,文件名,并计算md5,把这些信息写到配置文件当中;
4.更新资源缓存信息。
业务一般这样配置资源:
my_resource_list = [
//<资源名>: [<模块名><压缩包名>, <下载连接>, <资源版本号>]
module_SO1: ["module_SO1","aaa.zip", "https://xxxxxx", 1] ]
脚本将对应的资源文件下载,解析后,会生成对应的配置java类文件,可以从配置文件中获取对应模块资源的标识,下载链接,对应的资源文件列表和对应文件的大小与md5等详细的信息。
和框架相同,脚本文件也一样需要扩展多cpu架构支持。
动态加载框架有两个需求点:
需要支持多cpu架构配置;
兼容版本升级和回退(外网可能存在各种覆盖安装的情况)。
为了适配多cpu架构,我们修改了配置的方式:
resource_list = [
//<moduleName>: [<标识>,<压缩包名>, <下载连接>, <版本号>,<cup架构>]
MODULE_A: ["aaa","aaa.zip", "http://xxxx", 1,"armeabi-v7a"],
MODULE_A_64: ["aaa","aaa.zip", "http://xxxxxxx", 1,"arm64-v8a"],
]
这里改为了以标识为模块的唯一key,相同标识可以配置多个资源包,每个资源包配置对应架构的so。
同时,配置文件生成脚本也进行了调整,最终生成的配置文件中的资源信息也是每个模块对应一个资源信息数组,每个资源信息里包含对应架构资源包的详细信息。
这样,一个模块就可以有多个资源包适配多种cpu架构了。同时,我们调整了资源的注册流程,结合系统支持的cpu架构和业务自己的配置,挑选最合适的资源包注册到框架之中,这了我们是按照架构推出对先后顺序来判断获取哪个配置的:
private Info getSupportedResource(ResourceConfig config) {
HashMap<String, Info> architectures = new HashMap<>();
for(Info resource : config.getPackageInfoArray()) {
if (TextUtils.isEmpty(resource.architecture)) {
continue;
}
architectures.put(resource.architecture,resource);
}
if (architectures.isEmpty()) {
LogUtil.i(TAG,config.getIdentifier() + "did not config architecture");
}
if (isCurrentAcrSupport(architectures,ARM64_V8)) {
return architectures.get(ARM64_V8);
}
if (isCurrentAcrSupport(architectures,ARMEABI_V7)) {
return architectures.get(ARMEABI_V7);
}
if (isCurrentAcrSupport(architectures,ARMEABI)) {
return architectures.get(ARMEABI);
}
if (isCurrentAcrSupport(architectures,X86)) {
return architectures.get(X86);
}
if (isCurrentAcrSupport(architectures,X86_64)) {
return architectures.get(X86_64);
}
//没有配置对应架构,返回第一个
if (config.getPackageInfoArray().length > 0) {
return config.getPackageInfoArray()[0];
}
return null;
}
另外把对应的加载方法进行微调,业务就能正常获取到资源加载的状态信息了,这个步骤比较简单,我们就不细说了。
同时,为了兼容32位和64位包的互相覆盖安装的情况,和系统方案类似,我们把资源的储存路径根据架构配置分开存放在不同的路径当中,尽量避免互相覆盖升级时导致可能的io问题。
到此,动态加载框架适配多cpu架构多适配就基本完成了,后续的灰度问题优化我们在下文再介绍。
如何让用户能更新到正确位数的apk?理想的情况的是支持arm64的设备更新64位包,不支持的依然使用32位包。
全民K歌目前的更新方式有两种:
1.应用内更新;
2.渠道商店更新。
应用内灰度更新相对来说比较简单,我们在获取应用更新信息的时候,把用户设备的cpu架构信息新增到请求参数里就可以了,服务端收到之后,根据对应的规则下发安装包下载链接,list里包含arm64-v8a的就下发64位包,不包含的就下发32位。
系统恰好提供了对应的获取设备支持abi列表的方法:
/**
* 获取当前cpu支持架构列表
*/
public static String getSupportAbiList() {
String[] abiList = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
abiList = Build.SUPPORTED_ABIS;
} else {
//可以这么说5.1以下的系统基本不支持64位了
abiList = new String[]{Build.CPU_ABI, Build.CPU_ABI2};
}
StringBuilder s = new StringBuilder();
int size = abiList.length;
if (size > 0) {
for (int i = 0; i < size; i++) {
s.append(abiList[i]);
if (i != size - 1) {
s.append(",");
}
}
}
LogUtil.i(TAG,"ca:" + s.toString());
return s.toString();
}
这里的CPU_ABI和CPU_ABI2一般是早期的arm手机cpu架构的名字,android api level 21之后已经弃用,可以认为是官方默认api level低于21的都不支持arm64,也可以作为app升级时的一个参考。
这里我们把cpu支持的架构列表和cpu型号都上传了,上传cpu型号主要是为了如果出现某种特殊的cpu型号有适配问题,后端可以屏蔽下发64位包。
渠道更新则复杂很多,由于一些客观原因,国内的android渠道商众多,提供的服务水平也参差不齐,目前k歌上架的第三方渠道有17个,我们对齐进行了一波调研,各个渠道对针对设备cpu架构下发安装包的支持情况如下:
目前仅头部6个渠道支持了这个特性(小米,应用宝,三星,ov,华为),其他的一些小渠道和others厂商(魅族)均不支持这个特性,但相对来说已经能覆盖大多数用户了,另外,外网难免还会出现32位和64位互相覆盖的情况,这也是我要做覆盖安装兼容的原因。
全民k歌的64位包发布主要有如下几个阶段:
1.粉丝外团体验,主要在粉丝q群中发体验包,主要排除是否有大的机型兼容性问题;
2.号码包小范围体验,验证是否有启动crash等严重问题;
3.Alpha测试,排除启动等严重问题之后,开始验证主要功能;
4.Beta测试,放量最大,也是耗时最大的阶段,该阶段和主干功能发布同步进行,主要排查遗留问题和部分偶发的新增问题。
目前,全民k歌64位包已经上架小米应用商店,其他渠道上架也在进行中。
灰度过程中,我们也遇到了几个比较麻烦的问题,主要有以下几个:
1.So搜索路径扩展在Android 5.x系统上的问题
最开始在测试的时候,我们发现在5.0机器上我们使用的跨端框架hippy的so加载失败了,直接的报错提示是:
最开始我们以为是业务的问题和打包的问题,但反复确认之后,这里没有异常,并且也仅是Android 5.x的系统出现,因此最后我们从系统源码入手,才发现这个是动态资源加载框架在扩展系统so搜索路径时的一个适配5.x系统的问题。
我们先简单的看一下系统加载so的流程:
这是我们调用系统System.load或者System.loadLibrary时,系统执行的搜索并加载so的流程,为了拓展对应的系统加载路径,我们做了如下扩展:
if (soFolder != null) {
expandFieldArray(dexPathList, "nativeLibraryDirectories", new File[]{soFolder});
}
public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[]) jlrField.get(instance);
Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length);
// NOTE: changed to copy extraElements first, for patch load first
System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
System.arraycopy(original, 0, combined, extraElements.length, original.length);
jlrField.set(instance, combined);
}
这里我们修改了dexpathList中nativeLibraryDirectories这个数组的长度,把我们自定义的路径添加到这个数组当中,这数组通过传递,最终会赋值给linker.cpp中的g_ld_library_paths这边变量,这样,系统在搜索so文件的时候就会搜索我们的自定义资源路径。
但是,在Android5.x的代码中,这个数组是这样的:
#define LDPATH_BUFSIZE (LDPATH_MAX*64)
105#define LDPATH_MAX 8
106
107#define LDPRELOAD_BUFSIZE (LDPRELOAD_MAX*64)
108#define LDPRELOAD_MAX 8
109
110static char g_ld_library_paths_buffer[LDPATH_BUFSIZE];
111static const char* g_ld_library_paths[LDPATH_MAX + 1];
在linker.cpp中,这个数组的的长度被固定为9,当加载64位so的时候,使用的是就是g_ld_libary_paths这个数组,动态资源加载框架扩展的数组也就是这个,所以当外部传过来的数组长度大于9时,这里肯定会出现异常,要么是部分路径丢失,要么是直接抛出错误。而全民K歌通过动态加载的模块较多已经有8个模块,这样8个模块的资源路径都注册到这个数组里肯定导致了大小溢出从而导致了问题的出现。
既然原因已经找到,那么解决方法也就很简单了,临时的解决方案是缩短数组长度,我们临时先将部分资源改为了内置继续灰度。同时,我们着手对框架进行改造,不再按照模块一个个去拓展对应的搜索路径,也是和系统一样,是将所有模块的so按照其架构区分统一放到一个路径,再注入到系统的搜索路径中:
// 将资源文件目录添加至系统目录,如果包含so或者Dex的话
if (isResourcesContainSoOrDex(type)) {
if (!extSoDirs.contains(wokingDir.getAbsolutePath())) {
DexUtils.installDexAndSo(mContext, this.getClass().getClassLoader(), null, wokingDir);
extSoDirs.add(wokingDir.getAbsolutePath());
}
}
这样,就解决这个问题了。
2.动态资源加载框架的io问题
动态加载肯定少不了文件的下载,删除和复制等各种IO操作,但是众所周知,IO操作不是百分百可靠的,灰度过程中,动态加载框架就暴露出了几个io导致的问题。
/**
* 创建下载到本地的文件路径
*/
private String createDestFilePath() {
return mDestDirPath + File.separator + getInfo().saveFileName;
}
我们在app中的一个私有路径当中创建每个模块的资源路径,在该路径中对资源进行解压,文件大小和md5的校验,校验通过之后,我们会把下载的zip包删除以减少空间的浪费。
1月份的时候,我们发现外网有部分用户反馈资源加载失败,查看日志直接的原因是资源校验失败了,排查之后发现,对应模块的资源有更新,资源包也下载成功了,但是校验的时候发现解压的居然仍然是旧的资源包。那么导致这个问题的原因只有一个,那就是旧的资源包没有成功删除,同时新的资源包也没有成功覆盖旧文件,理论上来说,简单的单个文件删除和覆盖操作成功率应该是极高的,但是由于android厂商众多,市面上存在这各种各样的android系统魔改版本,这种抽风性的问题没有办法杜绝,还是需要app兼容一下。为此,我们优化了资源包的下载存储的方式:为不同的资源包创建不同的解压校验路径,以文件的md5为前缀区分:
/**
* 创建下载到本地的文件路径
*/
private String createDestFilePath() {
String pre = getInfo().md5;
return mDestDirPath + File.separator + pre + getInfo().saveFileName;
}
这样,不同的资源包就会存放在不同的路径当中,互不影响,就算旧的包因为io问题没有成功删除,新的资源包也不会因为覆盖的问题而校验失败了。
3.主播摄像头采集卡顿率增加 6 倍
直播是全面k歌一个核心使用场景,底层的音视频采集和编解码主要基础实现都是在so库当中,在适配完成灰度验证的过程中,发现主播摄像头采集卡顿率比之前增加了6倍,会导致主播端和观众端同时出现卡顿。经过排查,是发现我们目前的采集方案在64位上有个性能问题,当前的方案是使用RGBATexturProcess调用系统的glTexture去采集摄像头数据,将纹理转换为二进制数据,但是,市面上的某些GPU驱动对arm64-v8a的优化没有到位,导致此方案的纹理转数组耗时增加50ms左右,从而导致主播侧采集帧率降低。
经过考察,我们改为优先使用PBO方式读取摄像头纹理数据,把数据先写到缓冲池,再通过子线程异步读取缓冲池中的数据传给直播sdk进行推流:
这样,直播卡顿率就大为缓解了。但方案仅支持OpenGL3以上的版本,不支持的情况就降级回老方案处理。为了避免对32位机器产生影响,这里我们判断为64位包并且opengl版本大于等于3的时候再使用PBO方案。PBO方案虽然提高了64位的性能,但是这个方案也有两个缺点:
1.仅支持opengl3及以上版本,覆盖率还有待提高(目前94%);
2.对内存和cpu对占用有稍微提升。
4.webview缓存问题
灰度过程中,我们发现有部分用户反馈打开webview崩溃,经过排查发现是用户使用了32位apk覆盖按照64位,或者反过来,覆盖安装的情况下,系统api level如果在25-27,那么webview在打开的过程中会使用之前GPU cache,但是这个缓存的结构在armeabi和arm64上是不一样的,使用了错误格式的缓存会导致加载出现崩溃。
既然是缓存出现了问题,那么解决方案也很简单了,只要加载前判断位数发生了改变,就把旧的缓存删除即可:
if (clearCache()) {
try {
//移除:shared_prefs/WebViewChromiumPrefs.xml
final SharedPreferences chromiumPrefs = appContext.getSharedPreferences(
CHROMIUM_PREFS_NAME,
Context.MODE_PRIVATE
);
SharedPreferences.Editor editor = chromiumPrefs.edit().clear();
if (editor != null) {
editor.apply();
}
//移除:app_webview 目录
final File appWebViewDir = new File(appContext.getDataDir() + File.separator
+ APP_WEB_VIEW_DIR_NAME + File.separator
+ GPU_CACHE_DIR_NAME);
deleteDirectory(appWebViewDir);
} catch (Exception e) {
LogUtil.e(TAG, "", e);
}
}
总体来看,整个适配过程中,更新工程配置和对应so包的耗时仅占百分之30左右,后续的系统与机型适配,灰度问题的排查才是工作的重点,并且64位打包分支独立与主干单独维护,也有一定的管理成本。
从2020年Q3启动适配工作到2021Q1基本适配完成。至此,全民k歌就正式进入64位程序的时代了。
QQ音乐/全民K歌招聘Android/ios客户端开发,点击左下方“查看原文”投递简历~
也可将简历发送至邮箱:tmezp@tencent.com