遇到的问题
问题一
N 开始情况就不一样了:libxxx.so 能正常加载,而 liblog.so 会出现加载失败错误
E/ExceptionHandler: Uncaught Exception java.lang.UnsatisfiedLinkError: dlopen failed: library "liblog.so" not found at java.lang.Runtime.loadLibrary0(Runtime.java:xxx) at java.lang.System.loadLibrary(System.java:xxx)
问题一原因分析
Android P 以后,Linker 里检索的路径在创建 ClassLoader 实例后就被系统通过 Namespace 机制绑定了,当我们注入新的路径之后,虽然 ClassLoader 里的路径增加了,但是 Linker 里 Namespace 已经绑定的路径集合并没有同步更新
问题一解决方案
完全自己控制 so 文件的检索逻辑
ARM 手机主动检测 so,进入核心的 activity 时,开启延时检测,先停掉下载,减少对页面加载的影响,x 秒后重新下载
public void checkArmSoDelayWhenCreate(int delayMillis) { if (localSoStatus.isDownloading) { ThreadManager.getBackgroundPool().execute(this::pauseDownloadTask); } weakHandler.removeCallbacks(startCheckRunnable); weakHandler.postDelayed(startCheckRunnable, delayMillis); }
放在单独的线程中检测 so 是否完整
private void checkSoLibsInBackThread() { weakHandler.removeCallbacks(startCheckRunnable); final ThreadManager.ThreadPoolProxy singlePool = ThreadManager.getSinglePool("so-download"); // 避免产生重复的检测任务。 singlePool.remove(checkSoRunnable); singlePool.execute(checkSoRunnable); }
接下来我们详细了解一下具体的检测逻辑吧
zip 文件存在,则校验是否合法,md5 校验
String soZipPath = soFileDownloader.getSoZipFilePath(SOURCE_MD5); final boolean allSoFilesExist = isAllSoFilesExist(soZipPath); //统计触发检测时,不存在so的情况 StatisticsForSoLoader.sendSoFilesNotExist(allSoFilesExist); boolean hasInstalledSoPath = soFileDownloader.hasInstalledSoPath(); localSoStatus.hasInstalledSoPath = hasInstalledSoPath; final boolean isPrepared = allSoFilesExist && hasInstalledSoPath;
完整解压,不完整删除缓存,重新下载
localSoStatus.isPrepared = isPrepared; Log.d(TAG, "handleSoBackground isPrepared=" + isPrepared); if (isPrepared) { // 一切就绪,回调出去,ok if (soLoaderListener != null) { soLoaderListener.prepared(true); } else { // 回调出去继续执行 MKWeexSoLoader.reloadWeexSoLib(this::notifyCallback); } return; } private void startDownload(SoLoaderListener soLoaderListener, String soZipPath) { //pauseDownloadTask();//每次下载前暂停上次任务,防止so读写出现问题 String soUrl = getServerUrl(); soFileDownloader.downloadSoFile(soUrl, soZipPath, soLoaderListener); }
是否存在 soNameList 里面指定的 so 文件
for (File currentFile : currentFiles) { final String currentFileName = currentFile.getName(); // so库,size>0,且是预先定义的合法so,统计so个数 final boolean contains = allSoNameList.contains(currentFileName); if (currentFileName.endsWith(".so") && currentFile.length() > 0 && contains) { localSoFileCount++; } } // 如果本地下载目录中的so文件总数目,少于应该有的so文件数目,说明不完整 localSoStatus.isAllSoFilesExist = localSoFileCount >= allSoNameList.size(); return localSoStatus.isAllSoFilesExist;
问题二:将相关加载代码挪出静态代码块
so 动态化改造之后,如果项目后续开发中有人不小心在 so 插件尚未安装完成之前引用了相关的 JNI 类,则在改造成动态化的时候,最好将相关加载代码挪出静态代码块,并且增加 so 加载失败时候的 onFail 逻辑
- 如果是 X86 的手机,初始化 x86 平台的 so 文件名列表
问题二:将相关加载代码挪出静态代码块 so 动态化改造之后,如果项目后续开发中有人不小心在 so 插件尚未安装完成之前引用了相关的 JNI 类,则在改造成动态化的时候,最好将相关加载代码挪出静态代码块,并且增加 so 加载失败时候的 onFail 逻辑 如果是 X86 的手机,初始化 x86 平台的 so 文件名列表 作者:小木箱 链接:https://juejin.cn/post/6897095308157648910 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 如果是 armeabi-v7a 的手机,初始化 armeabi-v7a 平台的 so 文件名列表
else { SOURCE_MD5 = MD5_ARMEABI_V7A; initArmSoFileNameList(); } private void initArmSoFileNameList() { if (allSoNameList != null) { addAgoraSoLibs(); addWeexSolibs(); } } private void addAgoraSoLibs() { allSoNameList.add("xxxxxx.so"); ``` ``` } private void addWeexSolibs() {//weex 核心库,x86 arm都需要下发,不需要的不要乱加入 allSoNameList.add("xxxxxx.so"); ``` ``` }
- 检测 so 库是否准备好了。ARM 手机只有声网相关业务和 weex 的创建,需要检测 so
- 其他不需要;x86 手机则无论如何需要检测
public void checkSoLibReady(Context context, boolean isNeedCheckWhenArm, CheckSoCallback callback) { if (isX86Phone && isNeedCheckWhenArm) {//如果是x86手机,arm需要检测的地方无视 doCallback(callback); return; } if (!isX86Phone && !isNeedCheckWhenArm) {//arm手机,无需检测则无视 doCallback(callback); return; } this.mCallback = callback; boolean doCheck = doCheck(context); if (doCheck) {//直接callback回去 doCallback(callback); } }
然后再把检测 so 的回调传给业务层处理
public interface CheckSoCallback { void prepared(); }
问题三: Google Play Store 动态代码禁用问题
包含有动态代码的 APK 包是无法上传到 Play Store 的,可以向 APK 客户端下发绑定版本的“一个主资源包 + 一个 patch 包”,体积上限个 1G。so 动态化和版本绑定非,一旦发布就无法修改
问题四: 部分 ROM 机型删了 Build.VERSION.PREVIEW_SDK_INT 属性,导致无法获取 SDK 版本信息
@TargetApi(Build.VERSION_CODES.M) private static int getPreviousSdkInt() { try { return Build.VERSION.PREVIEW_SDK_INT; } catch (Throwable ignore) { } return 1; }
总结
实际项目中,so 动态下发遇到的坑比较多,熟悉系统加载 so 库的工作流程和反射流程。才能解决动态化过程中的安全性问题,版本控制问题,abi 兼容性判断和 System#load 加载代码侵入问题。当然理论是基石,线上打点分析 so 状态和网络状态才能保证我们应用在线上的稳定性。关于 App 瘦身可以聊的东西太多,如果本篇文章阅读量超过 2000,下一节写一下关于 png 转 webpng 自动化转化教程,满足大家对 App 瘦身的好奇心。