qigsaw插件化流程解析
aab方式介绍
Android App Bundle
一种发布格式,将所有经过编译的代码和资源包含在一个构件工件中;
是经过签名的二进制文件,可将应用的代码和资源组织到模块中;
简单来说, android插件化是 分为 base apk 和feature apk;
base 为基础包; feature为单个模块包, 可通过cdn网上动态下载 ,动态添加需要的功能模块;
组件化和插件化的区别:
同:
- 两者都可以使基础包和模块包作为一个app运行,便于开发;
- 充分解耦;
异:
- 组件化还是作为统一的app,只是开发时作为单独的app;
- 插件化所有模块就是单独的app,但单个模块不能安装,由基础包统一调动;
apk拆分
- 基本apk: 包含所有其他apk都可以访问的代码和资源,并提供应用的基本功能;
- 配置apk: 每个配置apk都包含针对特定屏幕密度,cpu架构或语言的原生库和资源;用户只会下载与设备对应的apk;
- 功能模块apk: 包含模块化处理的功能的代码和资源,
分发分类
- 安装时分发
- 按需分发
- 根据条件提供
- 免安装分发
Qigsaw 插件化介绍
原地址文档
Qigsaw_wiki
爱奇艺提供的 一套基于 Android App Bundle(AAB) 的动态化方案;
特点:
- 单base apk + 多个Split apk;
- apk 安装后,可以按需请求下发或者更新模块apk;
- api 21(5.0) 以上使用,低版本下发完整apk;
优势:
- 使用aab原生的开发套件 google 的 play core library 公开接口实现,支持所有aab的功能特性;
- 任何进程均可动态加载插件,支持四大组件;
- 可无缝切换aab方案
- 仅一处hook(android9.0+无需hook),少量私有api;
方案:
- 基线工程和插件工程独立开发,基线工程提供基础sdk供插件工程编译使用,避免类重复;
- 插件化方案一般将插件app 运行在 :plugin 插件进程,用ipc方案进程间通信;
当项目编译完成后,可通过adb install-multiple
命令将baseapk和插件apk安装到手机中;
qigsaw打包插件支持内置插件,所有内置插件都会拷贝到base apk 的assets
目录; 非内置插件,qigsaw打包插件会将其上传到cdn服务器,解决业务方后顾之忧;
apk 安装命令
aab安装或者defalutApk安装都是通过adb install-multiple
命令来安装baseapk和splitapks;
- 多apk安装,必须保证baseapk存在; 如 base-master.apk、base-xxhdpi、base-zh.apk 三个apk的安装;
- 已安装baseapk,继续安装splitapk,添加 -p参数(partial application install) 部分安装, 后跟baseapk包名;
apk update 步骤
ondemand =true表示按需分发 即不会将插件模块打进主apk中 而是上传到服务端 随后在apk安装之后进行下发 不过目前插件化模块的上传逻辑我们并没有实现 所以其实只能支持设置ondemand =false
- 创建插件更新分支,修改代码;
- 修改插件版本号;
- 配置qigsawSplit中的版本号;
- splitInfoVersion
- 配置mapping
- 配置old apk
- 再次执行qigsawAssemble${variantName}
Qigsaw 源码分析
前言:
Qigsaw结构主要分为:
- 编译期的Aop ; 由QigsawPlugin实现,主要是经transform api 处理四大组件的Resource对象和生成统计Split模块中四大组件等信息的ComponentInfo类,涉及到的主要技术点关键词为:ASM ,TransformApi, gradleTasks;
- 运行期插件的获取和激活; 主要分为split的安装和加载过程,涉及到的主要技术点关键词为:Binder通信,ClassLoader,loadDex;
Qigsaw plugin 源码分析
QigsawAppBasePlugin app模块应用的插件
注册的transform
- SplitComponentTransform (taskName:processSplitComponent) 处理split manifest文件, 生成class文件记录除provider的信息,生成代理provider
- 通过模块的copySplitManifestTask 在split模块的Manifest生成后会将xml复制到临时目录;
- 读取临时目录中所有split的xml,获取activity,service,receiver,provider和application的名称,建立Map添加key为featureName和各组件大写后缀名,value为各组件名称的键值对;
- Asm创建
com.iqiyi.android.qigsaw.core.extension.ComponentInfo
类,遍历Map,Asm写入属性- application: 创建常量属性
- provider:构建
providerName + _Decorated_ + splitName
作为代理provider的名称,asm创建代理Provider继承自com.iqiyi.android.qigsaw.core.splitload.SplitContentProvider
;此代理provider会在AABExtension
激活split时处理; - 其他: 创建
,
分隔的常量属性
- SplitResourcesLoaderTransform (taskName: splitResourcesLoader) 主要是在组件中插入 SplitInstallHelper#loadResources}方法,此处transform处理jar和Directory的字节码;
- 如果是基线包
- 获取插件扩展参数的act,对参数中指定activity使用
SplitActivityWeaver
进行方法的注入操作:- SplitActivityWeaver: 访问activity的所有方法,在
getResources
方法中(注意:没有此方法则新建插入,有则直接插入 )注入SplitInstallHelper#loadResources
方法,即通过SplitCompatResourcesLoader#loadResources加载插件资源;
- SplitActivityWeaver: 访问activity的所有方法,在
- 获取插件扩展参数的act,对参数中指定activity使用
- 如果是插件包
- 获取split的mergedManifest文件,读取activities,services,receivers,分别使用各自的Weaver 进行编织:
- SplitServiceWeaver: 访问service的所有方法,在
onCreate
方法中注入SplitInstallHelper#loadResources
方法 - SplitReceiverWeaver: 访问receiver的所有方法,在
onReceive
方法中注入SplitInstallHelper#loadResources
方法
- SplitServiceWeaver: 访问service的所有方法,在
- 获取split的mergedManifest文件,读取activities,services,receivers,分别使用各自的Weaver 进行编织:
- 如果是基线包
主要的tasks
- ExtractTargetFilesFromOldApk : 如果插件参数中设置oldApk,则复制
assets/qigsaw/**
目录文件到临时目录; - GenerateQigsawConfig: 构建 QigsawConfig.java 文件, 注意如果指定了oldApk,qigsawId即为oldApk中的qigsawId;
- Split:copySplitManifestTask: 取split合并后的manifest,复制到app的临时目录;
- Split:ProcessSplitApkTask :在split的assembleTask运行后,处理生成的splitApk,按照abi收集apkData信息,支持单个abi打包,最终会生成一个签名后的apk包和单个split.json记录split的信息,默认的apkData.url为
asset://
开头,buildIn标记(qigsawAssembleTask中用于判断是否上传至cdn中的标记) 由manifest中onDemand和插件参数releaseSplitApk 决定boolean builtIn = !onDemand || !releaseSplitApk
; - QigsawProcessManifestTask: 将app中merge/bundleManifest 的provider和splitManifest中的代理Provider 做同名处理,将同名的appProvider名称修改为
providerName + _Decorated_ + splitName
在写回,用于启动时AABExtension的解析; - CreateSplitDetailsFileTask :即qigsawAssembleTask,真正的打基线包的Task
- 先获取所有split生成的json信息
- 生成SplitDetails信息,收集app的所有打包信息
- 如果设置了oldApk,会判断是否是更新态,如果不是更新态直接使用oldApk中的SplitDetails信息;
- 如果是更新态,获取oldApk 的qigsaw.json信息,判断如果当前打的包moduleVersion(versionName@versionCode)和oldApk中的同一splitModule的version是否相同,如果split版本变化意为split的版本已经更新,标记当前更新状态,添加当前splitName到updateSplits字段中,创建SplitDetails对象;
- 如果当前的split的buildIn参数为false,尝试上传操作,上传成功指定apkData.url 为
http://
开头数据,表示在qigsaw安装阶段会从本地的asset目录获取apk还是从网络路径获取apk;
- 根据依赖关系重排序splitInfo 列表,根据SplitDetail写出qigsaw.json文件,获取当前abi列表数据并写出baseAppCpuAbiListFile文件,此文件也会复制到asset目录;
- 当前split的buildIn为true,如果是更新状态则将每个split的已签名的apk复制到即将打包的asset目录中,否则直接复制oldApk中的apk;
- SplitBaseApkForABIsTask:packageAppTask之后运行,用于根据baseAppCpuAbiListFile文件缩减到指定的abi的再输出apk;
- QigsawProguardConfigTask: 额外添加qigsaw的混淆和接收插件参数mapping文件保持混淆的一致性;
QigsawDynamicFeaturePlugin feature模块应用的插件
FeaturePlugin 注册 SplitResourcesLoaderTransform 和 SplitLibraryLoaderTransform
注册的transform
SplitLibraryLoaderTransform (taskName:splitLibraryLoader) 创建用于加载 splitNativeLib的类
- Asm创建
com.iqiyi.android.qigsaw.core.splitlib." + project.name + "SplitLibraryLoader
类和System.loadLibrary(var1);
的方法;
- Asm创建
图:QigsawPlugin task 依赖图
Qigsaw java 源码分析
1. 初始化阶段
构建 SplitConfiguration 配置Qigsaw
SplitConfiguration
基于构建者
的配置项:
splitLoadMode 指定split 的load模式
- multiple classloader 多classLoader模式; 实现类:SplitLoadTaskImpl
- single classloader 单classLoader模式; 实现类: SplitLoadTaskImpl2
workProcesses / forbiddenWorkProcesses 在application启动阶段,指定允许/禁止 load所有的installed splits的进程名称;其中这两个不能一起设置,会在加载split(启动SplitApplication,SplitLoadManagerImpl#isProcessAllowedToWork)前添加过滤;
verifySignature 是否验证split签名
loadReporter splits完全load后报告load状态
installReporter splits完全安装后报告install状态
uninstallReporter splits完全卸载后报告卸载状态
updateReporter split info 版本json更新后报告更新状态
obtainUserConfirmationDialogClass 自定义dialog的class,请求用户使用手机下载splits;
Qigsaw#install(configuration)
构建Qigsaw单例,调用Qigsaw#onBaseContextAttached,Qigsaw#onApplicationCreated,Qigsaw#preloadInstalledSplits,Qigsaw#registerSplitActivityLifecycleCallbacks;重写application中getResource方法 Qigsaw#onApplicationGetResources;
Qigsaw#onBaseContextAttached : 注入代理类加载器,清除缓存,激活aab安装的splitApplication;
SplitLoadManagerService#install:通过SplitLoadManagerService单例,初始化 SplitLoadManagerImpl(SplitLoadManager)加载管理器;
SplitLoadManagerService单例 SplitLoadManagerImpl 清除缓存数据: 清除
Set<Split> loadedSplits
的缓存数据,用于后续加载时的重新记录;SplitLoadManagerImpl#injectPathClassloader 注入代理ClassLoader,替换原Cl:
- 获取Context的类加载器 PathClassLoader(:BaseDexClassLoader:ClassLoader)
- PathClassLoader 是用于操作本地文件系统中文件和目录的ClassLoader,android 使用此cl作为系统的class loader和作为application 的class loader;
- DexClassLoader 是用于load 包含classes.dex 元素的
.jar
和.apk
文件的classLoader,可以执行一个应用中没有安装部分的代码;
- 创建代理PathClassloader的
SplitDelegateClassloader(PathClassLoader)
,通过反射,替换context中的Context.mPackageInfo.mClassLoader
为代理SplitDelegateClassloader; - 创建
DefaultClassNotFoundInterceptor
,用于tryCatch delegateCl#findClass 失败时的默认处理,用于兼容查找split的class;
- 获取Context的类加载器 PathClassLoader(:BaseDexClassLoader:ClassLoader)
AABExtension单例 AABExtensionManagerImpl 清除缓存的split 的contentproviderProxy和application信息 ;
- 因为QigsawPlugin 在编译期间会将split中的provider 改为父类为
com.iqiyi.android.qigsaw.core.splitload.SplitContentProvider
的Provider,而SplitContentProvider继承于 ContentProviderProxy,且将split中的provider class名称改为originProviderName + _Decorated_+splitName
,然后存放在ComponentInfo中; - 故在AABExtension启动split的ContentProvider时,会统一解析className,还原原始的ProviderName,再使用split的ClassLoader加载Provider;
- 因为QigsawPlugin 在编译期间会将split中的provider 改为父类为
AABExtension单例 调用 createAndActiveSplitApplication方法,找到已安装split的application,并调用attach方法(此处的安装应该是aab方式的安装,非qigsaw的安装);
SplitCompat 使用单例 初始化 SplitSessionLoaderImpl(SplitSessionLoader)、初始化 LoadedSplitFetcherImpl(LoadedSplitFetcher) ,提供SplitLoadManager的快捷调用入口;
- SplitSessionLoaderImpl : 提供 SplitLoadSessionTask的启动, 最终逻辑是通过
SplitLoadManagerImpl#createSplitLoadTask
启动split的加载过程; - LoadedSplitFetcherImpl: 提供快捷的获取已加载splitName,最终调用
SplitLoadManager#getLoadedSplitNames
;
- SplitSessionLoaderImpl : 提供 SplitLoadSessionTask的启动, 最终逻辑是通过
Qigsaw#onApplicationCreated:做split安装的准备工作;
- AABExtension单例,将aab已激活的applicaition 调用onCreate方法启动;
SplitApkInstaller#install
构建SplitInstallSupervisor类的单例SplitApkInstaller#startUninstallSplits
运行SplitInstallSupervisor#startUninstall 卸载split;- addIdleHandler 空闲时运行
Qigsaw#cleanStaleSplits
,开启服务运行 SplitPathManager#clearCache清除安装目录下不常使用的qigsawId目录;
Qigsaw#preloadInstalledSplits 预先加载已经安装的指定的split
获取SplitLoadManagerImpl单例,运行SplitLoadManagerImpl#loadInstalledSplitsInternal
启动预加载过程,和加载过程的区别是没有设置回调的加载过程,其中通过createLastInstalledSplitFileIntent
遍历安装目录时,只加载标记安装了的split;
Qigsaw#registerSplitActivityLifecycleCallbacks 注册split组件的生命周期监听:
自定义 SplitActivityLifecycleCallbacks ,在每个生命周期方法中使用AABExtension单例判断当前act是否是splitAct,记录在LruCache缓存中;
Qigsaw#onApplicationGetResource 重写Application#getResource
获取SplitLoadManagerImpl单例调用 SplitCompatResourcesLoader#loadResources 加载资源
2. 安装阶段
Split安装管理器 SplitInstallManagerFactory静态工厂构建SplitInstallManagerImpl(SplitInstallManager)
split的安装器,提供split的安装操作,主要是将asset目录下的apk复制到本地安装目录/data/data/pkgName/app_qigsaw/$qigsawid/
中并生成markFile等文件标记安装状态;
构建器逻辑
- 构建
splitinstall.SplitInstallService
实例 用于启动安装服务- 声明Intent的action
com.iqiyi.android.play.core.splitinstall.BIND_SPLIT_INSTALL_SERVICE
,用于启动remote.SplitInstallService
服务Service实现跨进程通信; - 构建
RemoteManager
封装绑定服务和提供跨进程通信方法- 构建
SplitRemoteImpl
单例 ,提供ISplitInstallServiceHolder#queryLocalInterface
方法处理跨进程连接成功返回的binder对象- 先检索binder对象接口的的实现(stub),如果没有找到则构建
ISplitInstallServiceImpl
作为本地的实现,binder接口都是类似的默认;- 从Parcel池中获取一个新的Parcel对象
- Parcel设置descriptor为queryLocalInterface查找的string
- Parcel写入指定的数据,Parcel#writeStrongBinder 写入回调Binder的接口ISplitInstallServiceCallbackProxy;
- transact 发送Parcel;
- 先检索binder对象接口的的实现(stub),如果没有找到则构建
- 构建
- 声明Intent的action
- 构建
SplitInstallListenerRegistry
, 封装了一个广播接收者,接受安装过程中的状态变化;- 提供注册安装状态监听者用于监听安装状态变化;
- onReceived 方法中获取传递的Bundle构建SplitInstallSessionState 代表进行的安装状态,如果监听的status为SplitInstallInternalSessionStatus.POST_INSTALLED(10)代表split已经安装和抽取优化(手机兼容),但是没有加载,此时需要直接进行加载过程:
- 使用SplitCompat保留的加载快捷入口SplitSessionLoaderImpl单例 发送
SplitLoadSessionTask
,调用SplitLoadManagerImpl#createSplitLoadTask
执行加载过程-加载 第二阶段,并设置加载监听; - 监听到安装结果发布到所有订阅Listener的监听;
- 使用SplitCompat保留的加载快捷入口SplitSessionLoaderImpl单例 发送
- 构建
安装过程-通信 SplitInstallManagerImpl#startInstall
主要是建立进程间通信的通道,然后调用Server端的安装方法asset目录中的splitApk包,使用回调Binder通知Client端安装状态;类调用过程: SplitInstallManagerImpl单例 -> splitinstall.SplitInstallService -> RemoteManager
- 判断是否已安装过:
SplitInstallManagerImpl#getInstalledModules
- 使用aab fusedModule方式和SplitCompat保留的入口类 LoadedSplitFetcherImpl判断是否已安装指定的splits,已安装发送
SplitInstalledDisposer
: 直接使用registry对象发送已安装状态的bundle并发布至listener;未安装则进行安装过程;
- 使用aab fusedModule方式和SplitCompat保留的入口类 LoadedSplitFetcherImpl判断是否已安装指定的splits,已安装发送
splitinstall.SplitInstallService#startInstall
->RemoteManage#bindService(RemoteTask)
开始安装- 通过RemoteManager 在HandlerThread中开启
bindService
,绑定的服务为初始化传入的Action开启的服务remote/SplitInstallService
,绑定成功即建立了与远程Rpc通信,如果绑定失败返回failureListener回调并使用pendingTask添加此Task,绑定成功则执行传入的Task; - 绑定成功后获取远程 ServiceConnectionImpl返回Binder对象,使用
SplitRemoteImpl
单例queryLocalInterface检索Binder的实现(ISplitInstallServiceProxy stub)并记录在RemoteManager中;开启Binder的死亡通知DeathRecipientImpl,如果回调会构建Bundle发送广播告知失败;将绑定失败时记录的pendingTask全部运行; - Client端StartInstallTask(RemoteTask)的执行:构建StartInstallCallback(ISplitInstallServiceCallbackProxy) 作为回调Binder,启动远程Binder的startInstall方法,Server端使用HandlerThread发送OnStartInstallTask(DefaultTask),此task初始化时会获取
SplitInstallSupervisor
的单例,调用SplitInstallSupervisorImpl#startInstall
才是真正的安装操作;- StartInstallCallback 回调Binder的实现,获取到反馈后都会立即解绑服务并回调相关信息,此task回调方法重写
onStartInstall
返回sessionId给注册监听者 ;
- StartInstallCallback 回调Binder的实现,获取到反馈后都会立即解绑服务并回调相关信息,此task回调方法重写
- 通过RemoteManager 在HandlerThread中开启
- 判断是否已安装过:
安装过程-安装 SplitInstallSupervisorImpl#startInstall
在Rpc通道建立后,此处才真正开始安装操作,主要是获取splitApk然后复制到本地安装目录中
/data/data/pkgName/app_qigsaw/$qigsawid/
,并进行抽取和优化;预校验split ,判断aab fuseModule的splits是否包含当前split和splitInfo是否合法,如校验失败使用回调Binder对象返回错误信息;
获取本次需要安装的splits,如果splits所依赖的dependenciesSplits不为空,将dependenciesSplits和传入的splits合在一起安装;
SplitInstallSupervisorImpl#startDownloadSplits
(详细见SplitInstallSupervisor单例)SplitInstallTask Runnable中运行 SplitInstallerImpl#install ,在上述安装过程中已经将QigsawPlugin生成的apk文件成功拷贝到了安装目录,其实已经算是完成了安装,接下来就要标记安装过程,遍历apkDataList获取每个split的apkData信息:
- 通过
SplitInfo#obtainInstalledMark
构建markFile文件名,先判断是否是libBuiltIn(buildIn && 判断apkData的Url是否以native://
开头),设置applicationInfo.nativeLibraryDir目录文件或者qigsaw安装目录作为分包目录文件作为splitApk;【存疑:为了兼容aab的分包形式?qigsawPlugin生成apkData的前缀都是http或者asset】 - 校验splitApk的签名和md5
- oat文件的兼容;如果apkData.abi 不为master,则抽取splitApk中lib文件native库到安装目录的nativeLib/abi 文件夹中;如果是master,先获取
/oat
作为optimizedDirectory目录,建立addedDexPaths
dex列表添加splitApk路径 ,如果支持multidex,抽取splitApk的dexs文件到安装目录的code_cache目录,添加至dex列表;- 如果markFile不存在,使用dex列表添加分隔符(:;)输出统一的dexPath,然后通过dexPath,parentClassLoader为当前ClassLoader等参数构建新的
DexClassLoader
,建立classloader 链; - 低版本下检查oat目录,用于兼容vivo&oppo;创建specialMarkFile文件,并作为第一次安装的标记;
- 如果markFile不存在,使用dex列表添加分隔符(:;)输出统一的dexPath,然后通过dexPath,parentClassLoader为当前ClassLoader等参数构建新的
* BaseDexClassLoader implements the Android * <a href=https://developer.android.com/guide/topics/manifest/uses-library-element> * shared libraries</a> feature by changing the typical parent delegation mechanism * of class loaders. * <p> Each shared library is associated with its own class loader, which is added to a list of * class loaders this BaseDexClassLoader tries to load from in order, immediately checking * after the parent. * The shared library loaders are always checked before the {@code pathList} when looking * up classes and resources.DexClassLoader 继承自BaseDexClassLoader
- 再构建markFile文件作为本次split的安装标记,返回结果对象InstallResult,后续通过
SplitStartInstallTask#onInstallCompleted
发送session状态POST_INSTALLED,触发加载过程;
- 通过
跨进程接口 ISplitInstallServiceProxy extends IInterface 发送Binder
- 作为建立RemoteManager <-> remote.SplitInstallService的通信的binder接口,SplitInstallService通过HandlerThread发送不同的task 将安装方法逻辑转交给SplitInstallSupervisor 执行;
跨进程接口 ISplitInstallServiceCallbackProxy extends IInterface 回调Binder
- 建立rpc通信后,作为SplitInstallSupervisor执行的回调接口的Binder,通过TaskWrapper将data信息发送给注册的观察者;
SplitApkInstaller#install 构建SplitInstallSupervisor 单例,封装安装过程的逻辑
构造器逻辑
- 构建SplitInstallSessionManagerImpl(SplitInstallSessionManager) 用于记录install过程的sessionState
- 提供emitSessionState方法,将SplitInstallInternalSessionState状态对象转化为Bundle,然后通过Intent发送广播给SplitInstallListenerRegistry进行反馈;
- 构建SplitInstallerImpl(SplitInstaller) 安装器
- 通过
SplitBaseInfoProvider
获取qigsawConfig信息;
- 构建SplitInstallSessionManagerImpl(SplitInstallSessionManager) 用于记录install过程的sessionState
startDownloadSplits
- 根据needInstallSplits 的splitInfos 构建sessionId
- 为每个splitInfo构建DownloadRequest,设置下载路径为split的安装目录,记录splitInfode apkData信息提供自定义的下载功能;构建当前的SplitInstallInternalSessionState状态对象;
- onPreDownloadSplits
- 遍历splitInfoList,构建SplitDownloadPreprocessor且运行SplitDownloadPreprocessor#load
- 获取split的安装目录,建立FileChannal准备读写文件
- 遍历SplitInfo中apkDataList(存储apk的md5,url,size等信息)
- 如果splitInfo是
buildIn
接入,apk包位于安装包的asset目录,调用copyBuiltInSplit
方法通过context.getAsset()读取本地asset目录中的splitApk,复制到qigsaw的安装目录;如果Configuration中的verifySplitApk开关打开则校验qigsaw安装目录中复制且重命名的splitApk,主要校验签名和md5;
- 如果splitInfo是
- 返回splitApks的总大小和本地需要下载的大小参数;
- 遍历splitInfoList,构建SplitDownloadPreprocessor且运行SplitDownloadPreprocessor#load
- 调用回调Binder 的
onStartInstall
回调sessionId; - 使用自定义下载接口
Downloader#calculateDownloadSize
返回的实际需要下载的字节realTotalBytesNeedToDownload;构建StartDownloadCallback 用于下载过程状态的监听且发送相应的广播给SplitInstallListenerRegistry;- StartDownloadCallback会构建SplitSessionInstallerImpl,提供install方法post SplitStartInstallTask 执行安装方法;
- 封装下载过程及安装状态回调的生命周期方法,提供快捷更改状态和发送广播方法;
- 如果realTotalBytesNeedToDownload<=0 意为split已经下载完成可以立即安装,调用
StartDownloadCallback#onCompleted
- 设置当前sessionState的状态为DOWNLOADED并发送广播;调用
SplitSessionInstallerImpl#install
发送SplitStartInstallTask extends SplitInstallTask
,主要逻辑为:- 设置当前sessionState状态为INSTALLING;
- 调用 SplitInstallerImpl#install 安装器 执行安装过程,返回结果对象InstallResult;
- 记录是否是第一次安装等信息,如果安装失败设置当前sessionState为FAILED;
- 安装成功时,构建记录本地安装split信息的splitFileIntents,且设置sessionState为POST_INSTALLED,用于执行后续的加载过程第二阶段;
- 设置当前sessionState的状态为DOWNLOADED并发送广播;调用
- realTotalBytesNeedToDownload>0 ,如果大于configuration设置的阈值,开启configuration设置的确认下载Act页面;更改当前sessionState为PENDING并发送广播; 使用自定义下载接口开启下载,安装目录等信息记录在DownloadRequest作为参数传递,待下载完成后同样调用
StartDownloadCallback#onCompleted
startUninstall
- 读取qigsaw的安装目录中uninstall目录
uninstallsplits.info
properties文件, - 从SplitInfoManagerImpl中获取properties中记录的已卸载split的SplitInfo;
- 删除卸载SplitInfo安装时构建的SplitMark文件(标记安装状态文件),并通过SplitInstallService发送SplitStartUninstallTask:
- 删除安装目录中已经卸载的Split的目录,已达到真正的卸载效果;
- 发送卸载report通知,删除
uninstallsplits.info
文件清除需要卸载的split信息;
- 通过SplitInstallService发送SplitDeleteRedundantVersionTask:
- 获取SplitInfoManager的所有的splits信息,通过markFile遍历已经安装的split目录,安装目录为
data/data/packageName/app_qigsaw/qigsawId/SplitName
,删除不常用split版本号目录,只保留一个最常使用的版本目录;
- 获取SplitInfoManager的所有的splits信息,通过markFile遍历已经安装的split目录,安装目录为
- 读取qigsaw的安装目录中uninstall目录
3. 加载阶段
Split加载管理器: SplitLoadManagerService 单例构建SplitLoadManagerImpl(SplitLoadManager)
split加载管理器,主要提供了split的加载过程的方法;
构造器逻辑 SplitLoadManagerImpl的初始化
- SplitInfoManagerService#install:SplitInfoManager单例的初始化 操作最新qigsaw.json
- 调用SplitInfoManagerService工厂构建SplitInfoManagerImpl(SplitInfoManager)单例,该类主要记录
qigsaw.json
的信息,描述所有插件信息的管理类,提供最新版本qigsaw.json的数据获取方法; - 静态方法构建SplitInfoVersionManagerImpl(SplitInfoVersionManager): 获取QigsawConfig类信息,在通过信息获取本地路径
/data/data/packagename/app_qigsaw/$qigsawid/split_info_version
中version.info (properties)文件,主要是记录split的更新信息存放在内存中,用于后续的安装和加载; - SplitInfoManagerImpl.attach(SplitInfoVersionManagerImpl) ,将version的管理器交给SplitInfo管理器统一管理;
- 调用SplitInfoManagerService工厂构建SplitInfoManagerImpl(SplitInfoManager)单例,该类主要记录
- SplitPathManager#install : 创建SplitPathManager 单例 操作本地安装目录
- 主要提供splits安装路径的快捷访问,即split本地存储路径:
/data/data/packagename/app_qigsaw/$qigsawid/
;
- 主要提供splits安装路径的快捷访问,即split本地存储路径:
- SplitInfoManagerService#install:SplitInfoManager单例的初始化 操作最新qigsaw.json
加载过程-准备 SplitLoadManagerImpl#loadInstalledSplitsInternal (or loadInstalledSplits) 构建
splitFileIntents
记录已安装的split本地包信息;先通过SplitInfoManager单例获取指定加载split的splitInfos;
SplitLoadManagerImpl#createInstalledSplitFileIntents
用于构建安装splitFile的splitFileIntents
;- 遍历SplitInfoList,调用
createLastInstalledSplitFileIntent
构建未加载的splitFileIntent:- 先判断是否是libBuiltIn(buildIn && 判断apkData的Url是否以
native://
开头)选取applicationInfo.nativeLibraryDir目录split文件或者qigsaw安装目录apk文件作为splitApk; 【存疑:为了兼容aab的分包形式?qigsawPlugin生成apkData的前缀都是http或者asset】 - 校验已安装split的本地文件生成的markFiles, 目录为:
/data/data/pgk/contextDir/$qigsawId/splitNames/splitsVersion
,包含:- markFile : 拼接ApkData的md5和size字段作为文件名,文件存在则表示该split已经被安装了;
- specialMarkFile: markFile+".ov" ,和markFile的功能一致,为了兼容vivo&oppo手机;
- markFiles相关大概逻辑为:
- 1.specialMarkFile存在,markFile不存在:
- 先获取
安装目录/oat
作为optimizedDirectory目录中的dex文件;检查dex文件,获取魔数是否为elf格式文件; 创建锁文件(FileChannel),线程安全的创建markFile;
- 先获取
- 2.markFile存在或者specialMarkFile存在:
- 先获取当前splitInfo依赖的dependencySplits,判断依赖的split是否已经加载,如果存在没有加载的依赖split则直接返回null不加载当前的split,结束createLastInstalledSplitFileIntent方法;
- 如果依赖的split都已经加载,记录splitApk的路径和安装缓存目录code_cache中的zip文件路径作为dex路径;建立Intent,添加dex路径等信息后作为splitFileIntent返回;
- 1.specialMarkFile存在,markFile不存在:
- 先判断是否是libBuiltIn(buildIn && 判断apkData的Url是否以
- 记录所有创建的intent返回splitFileIntents
- 遍历SplitInfoList,调用
使用splitFileIntent列表构建SplitLoadTask 的Runnable并运行
SplitLoadManagerImpl#createSplitLoadTask
,执行SplitLoadHandler#loadSplitsSync 加载split方法;- splitMode 为多Cl模式下,创建SplitLoadTaskImpl
- splitMode 为单Cl模式下,创建SplitLoadTaskImpl2
加载过程-加载 创建SplitLoadTask实现类,执行SplitLoadHandler#loadSplits方法
SplitLoadTask构建
SplitLoadHandler
类,SplitLoadHandler封装了真正加载split的入口方法SplitLoadHandler#loadSplitsSync
,主要是使用记录splitApk路径等信息的splitFileIntents
,使用不同的ClassLoader执行类加载和激活split模块的过程;- SplitLoadHandler
- 构建器: 构建NativePathMapperImpl 映射nativeLibPath到安装目录
common_so
目录中内容,用来兼容低版本;构建SplitActivator 封装AABExtension单例,提供快捷启动application的方法; SplitLoadHandler#loadSplits
执行加载入口方法- 获取传入的split 的intents数据,遍历splitFileIntents,校验数据和修正nativeLibPath数据
- 调用
SplitLoaderWrapper#loadCode
对于split 使用指定的ClassLoader进行类加载(单Cl使用dex插桩,多Cl使用多splitClassLoader进行加载) - 通过指定的Cl,使用AABExtension构建 split的application的实例;
- 激活split 插件
- 加载资源
SplitCompatResourcesLoader#loadResources
- AABExtension激活splitApplicaiton#attach方法
- AABExtension激活provider,这里需要说明一下:
- QigsawPlugin将所有的splitContentProvider 更改父类为ContentProviderProxy且重命名,为的是重写attachInfo(初始化ContentProvider调用)解析ContentProvide原名加入到容器中;待加载split过程中,遍历容器,使用splitClassLoader加载原ContentProvider在调用attachInfo方法激活split插件中的ContentProvider;
- AABExtension 启动splitApplicaiton#onCreate
- 加载资源
- SplitLoadManager 记录已加载splitInfo信息;如果设置了OnSplitLoadFinishListener监听则回调。其中安装过程结束开启加载过程会设置此监听,用于安装完开启加载过程,预加载方法不会设置此监听;
- 构建器: 构建NativePathMapperImpl 映射nativeLibPath到安装目录
- 按照cl的模式分为两个实现类(SplitLoaderWrapper),主要重写loadCode方法指定使用哪种SplitClassLoader加载类
- SplitLoadTaskImpl 多cl模式加载
- 为每个split构建SplitDexClassLoader,
SplitDexClassLoader#create
并存于容器单例SplitApplicationLoaders中,如果加载失败则使用当前split依赖的split的splitClassLoader尝试加载;
- 为每个split构建SplitDexClassLoader,
- SplitLoadTaskImpl2 单cl模式加载
- 直接获取当前类的classLoader 即BaseDexClassLoader
SplitCompatLibraryLoader#load
按照版本兼容加载NativeLib【存疑: 可能为了兼容aab】;v25的逻辑主要是反射BaseDexClassLoader的pathList(DexPathList)属性,再获取nativeLibraryDirectories目录属性,添加输入的native包目录到第一位,再把所有的包括systemNativeDir的native目录替代pathList的nativeLibraryPathElements属性;- loadDex操作,插桩splitDex到当前的dexElements中
SplitCompatDexLoader#load
多版本兼容加载dex(apkPath); >v23 的主要逻辑为反射BaseDexClassLoader的pathList(DexPathList)属性,通过makePathElements构建Element数组插入到pathList的属性dexElements中;SplitUnKnownFileTypeDexLoader#loadDex
低版本so文件兼容
- SplitLoadTaskImpl 多cl模式加载
- SplitLoadHandler
SplitInstallHelper#loadResources 加载split资源
SplitCompatResourcesLoader#loadResources 最终加载资源方法
加载split资源逻辑,此方法会在激活splitApplication和使用asm在所有的四大组件中重写调用此方法,所以传入的context是application和activity,service的context,核心原理是使用AssetManager添加插件资源:
- loadResource(context,resource) 四大组件重写调用
- 先获取AssetManager已加载资源路径assetPaths,在获取已加载split的资源路径splitResPath(apk路径),如果不包含直接兼容方式添加splitResPath到assetPath中;
- 重载三参loadResource方法 ,多了插件包路径参数 splitResPath
- 先以兼容的方式获取AssetManager的已加载资源assetPaths,如果其中不包含splitResPath
- 高版本中直接反射
addAssetPath
添加splitResPath到assetPaths中即可; - 低版本中较复杂,实现为:
- 将Resource.getSystem()中的asset和context的asset的所有resDir收集,然后构建一个新的AssetManager实例添加所有的resDir,在构建一个新的Resource对象,通过反射替换context中的resource对象;
- 获取当前ActivityThread入口类,反射获取正在运行的的activityClientRecord,遍历当前的activityRecord如果不是传入的context则替换activity的resource对象;(运行时安装split插件)
- 反射ActivityThread兼容获取ResourcesManager,将新建的resource对象put进去
- 反射ActivityThread的mPackages获取LoadedApk,主要维持了load apk的数据,使用新建的resource替换LoadedApk中的mResources对象;同样替换ActivityThread的mResourcePackages中的LoadedApk的mResources;
- 高版本中直接反射
- 先以兼容的方式获取AssetManager的已加载资源assetPaths,如果其中不包含splitResPath
DefaultClassNotFoundInterceptor : 代理SplitDelegateClassloader#findClass失败时对splitDex进行查找
单Cl模式下 splitMode
- 从AABExtension单例中判断当前findClass 是否和ComponentInfo中所记录的一样,如果存在,返回Fake组件(FakeActivity,FakeService,FakeReceiver) ;
- 在ComponentInfo中找到className或者是已指定的SplitEntryFragment中,首先会尝试调用
SplitLoadManagerService.getInstance().loadInstalledSplits();
去启动qigsaw加载过程,防止类没有加载; - 然后,使用原PathClassLoader 加载,如果还是加载失败而Fake组件存在,则直接返回Fake组件,此时就得到了一个空的Act :)
多Cl模式下 splitMode
- qigsaw加载过程中创建多个
SplitDexClassLoader
,存放在 SplitApplicationLoaders单例中,调用单例的getValidClassLoaders` 方法获取有效的已加载Split的ClassLoaders; - 遍历SplitDexCls , 尝试使用所有的splitCls去加载;
- 如果还是没有找到,先走单Cl模式下的1,2两步; 再重复一下多Cl模式下的1,2两步;确认没有找到则返回Fake组件;
- qigsaw加载过程中创建多个
SplitDelegateClassloader : PathClassLoader 和 SplitDexClassLoader : BaseDexClassLoader
SplitDelegateClassloader: 作为 context的Cl 的代理,即对PathClassLoader的代理;重写findResource等方法,如果找不到会从SplitDexClassLoader中查找;重写findClass方法,找不到会使用DefaultClassNotFoundInterceptor处理;
SplitDexClassLoader:作为单个Split包的Cl
- 首先创建时会获取当前splitInfo中的dependencies(QigsawPlugin中 从splitproject gradle 中dependencies中获取到) ,从SplitApplicationLoaders单例中(用于记录在多ClassLoader模式下创建的SplitDexClassLoaders)找齐所有当前split依赖的split的SplitDexClassLoaders;
- 在Build.VERSION_CODES.LOLLIPOP 版本之下需so的Native兼容;
SplitUnKnownFileTypeDexLoader#loadDex
:- 获取so库file的绝对路径,反射当前Cl的
pathList:DexPathList
- 反射dalvik.system.DexPathList#loadDexFile 加载file,无异常再次反射构建
dalvik.system.DexPathList$Element
Element对象; - 再反射使用pathList的属性
dexElements
添加新构建的Element对象;
- 获取so库file的绝对路径,反射当前Cl的
- 重写findClass方法,重写findResource等方法,找不到会使用dependencies split 的SplitDexClassLoaders 尝试加载;
AABExtension 单例 : 用于扩展AAB(android app bundle)功能,提供接口去创建split的application和四大组件及激活各组件;
- 初始化逻辑:
- 构建器中构建 AABExtensionManagerImpl(AABExtensionManager), 该类用于创建Application和调用Application#attach方法和通过
SplitComponentInfoProvider
判断是否属于split中的四大组件; - SplitComponentInfoProvider : 用于获取
ComponentInfoManager
中记录的Component信息,包括split的四大组件,application; - ComponentInfoManager : 由于QigsawPlugin 会在编译期间将split的四大组件和application 信息都记录下来,生成一个
com.iqiyi.android.qigsaw.core.extension.ComponentInfo
文件,此类用于反射此类获取split的组件信息;
- 构建器中构建 AABExtensionManagerImpl(AABExtensionManager), 该类用于创建Application和调用Application#attach方法和通过
- #createAndActiveSplitApplication 方法 通过
SplitAABInfoProvider
获取已安装的split名称,在使用aabExtension的cl启动和激活已安装的splitApplication;- 从packageManger的元数据中获取fused modules 作为 splitName,这个应该是android4.4以下的兼容;
- 取packageManager.packageInfo.splitNames
安装-加载过程 总结
其实qigsaw大致可分为:初始化过程
-> 预加载过程
-> 安装过程
-> 加载过程
, 其中最重要的是安装和加载过程,总结下重要节点:
安装的入口实现为
SplitInstallManagerImpl(SplitInstallManager)
,首先绑定rpc服务,使用remote服务提供的SplitInstallSupervisorImpl(SplitInstallSupervisor)
开始实际的安装功能,其中该方法中真正的核心安装类为SplitInstallerImpl(SplitInstaller)
; 完成从远程或者asset目录中将apk文件或nativelib文件拷贝到data目录,并生成标记文件标记安装的完成;加载的入口实现为
SplitLoadManagerImpl(SplitLoadManager)
,首先使用split的信息构建Intent列表,执行核心加载类SplitLoadHandler#loadSplits
方法进行实际的加载,加载过程提供两种可选Classloader模式,一种是多Cl模式SplitLoadTaskImpl
即每个split有单独的ClassLoader,另一种是单Cl模式SplitLoadTaskImpl2
使用Qigsaw代理的ClassLoader,然后使用Cl构建和激活application和provider等组件;相当于完成使splitApk以插件的方式加载到内存中;这两个过程的状态都是通过广播发送状态
资源加载通过
SplitCompatResourcesLoader#loadResources
处理AssetManager和Resource对象,并通过QigsawPlugin重写四大组件的getResource方法,用Asm进行Aop添加loadResources方法;qigsaw代理了默认的
BaseDexClassLoader
,当dexCl 类查找失败时,会在多Cl模式下遍历所有的splitClassLoader查找,在单Cl模式下进行一次全量加载过程后再用dexCl尝试类查找;
4. 更新split阶段
Qigsaw#updateSplits 提供重启更新的splits操作;
第一步 在启动更新服务之前需要将新的split包复制到安装目录中;
- 调用
SplitPathManager.require().getSplitDir(splitInfo);
获取split的本地文件存放路径,复制apk到此路径中;目录使用split模块的version(versionName+ versionCode)标注;
- 调用
第二步 调用
Qigsaw.updateSplits(context, newVersionName, splitInfoFilePath);
将qigsaw.json的版本名(SplitVersionName)和json路径进行更新;- 启动SplitUpdateService(IntentService) 进行qigsaw.json的拷贝过程;
- 获取Intent中的参数,一个为新的splitInfoVersion(即qigsaw.json的中间名),一个为新版qigsaw.json文件的路径,用于替换内存和复制到安装目录下,下次启动时指定解析新版qigsaw.json;
- 检验传入的参数和qigsaw.json是否合法,注意新版本和旧版本的判断只是equals;
- SplitInfoManager#updateSplitInfoVersion
- 获取当前的SplitInfoVersionManager,复制新版qigsaw.json 到安装目录split_info_version目录中,且写入新版和旧版名称到version.info ,属于Properties文件;
- 写入成功,更新阶段即成功,重启时会查找本地的version.info取最新的split安装路径中的apk包去进行安装和加载过程;
图:qigsaw插件化安装阶段Rpc通信图
图:qigsaw插件化启动过程图
qigsawUpdate tips
优化点记录
支持单独编译splitApk的task
支持打基线包使用远程或本地splitApk;
LoadSplitApkProcessTask 解压SplitApk的manifest文件可能之后不能合并,待测试;
- 使用axml的解析可以反编译
qigsaw 插件 指定mapping再次打包时 使用R8混淆 会出现error ;
- com.android.tools.r8.utils.AbortException , 升级R8为1.6.84解决;
- 关闭R8功能;
发现CreateSplitDetailsFileTask 每次新增的split,buildIn会默认为false,每次都会激活上传接口,需要关闭,至少编译成功;
- 添加关闭参数;
添加一个app的打包所有split的task
混淆不一致,split和app单独打包;
//split不能打开混淆文件,可指定混淆文件
//Dynamic feature modules cannot set minifyEnabled to true. minifyEnabled is set to true in build type ‘release’.
//To enable minification for a dynamic feature module, set minifyEnabled to true in the base module.
主要发现的坑
- Resources$NotFoundException
Q: int id = ctx.getResources().getIdentifier(name, type, pkName); 会出现资源找不到的问题;
A: 场景为 base -> apk 资源, 因为apk插件会生成自己的一份Resource对象,使用defPackage名称添加 模块名;
ctx.getResources().getIdentifier(name, type, pkName+“.”+“moduleName”)
所以封装一个方法类似DelegateClassLoader功能,找不到资源时需要从插件的资源中查找;
NoClassDefFoundError
Q: Qigsaw 中 java类不能使用lambda表达式
A: 替换匿名内部类 和更改::的写法;
qigsaw插件化流程解析相关推荐
- Android 插件化原理解析——Activity生命周期管理
之前的 Android插件化原理解析 系列文章揭开了Hook机制的神秘面纱,现在我们手握倚天屠龙,那么如何通过这种技术完成插件化方案呢?具体来说,插件中的Activity,Service等组件如何在A ...
- Android 插件化原理解析——Hook机制之AMSPMS
在前面的文章中我们介绍了DroidPlugin的Hook机制,也就是代理方式和Binder Hook:插件框架通过AOP实现了插件使用和开发的透明性.在讲述DroidPlugin如何实现四大组件的插件 ...
- Android插件化原理解析——ContentProvider的插件化
目前为止我们已经完成了Android四大组件中Activity,Service以及BroadcastReceiver的插件化,这几个组件各不相同,我们根据它们的特点定制了不同的插件化方案:那么对于Co ...
- Android插件化原理解析
概述 Android插件化技术,可以实现功能模块的按需加载和动态更新,其本质是动态加载未安装的apk. 本文涉及源码为API 28 插件化原理 插件化要解决的三个核心问题: 类加载. 资源加载. 组件 ...
- 插件化原理解析——ContentProvider的插件化
目前为止我们已经完成了Android四大组件中Activity,Service以及BroadcastReceiver的插件化,这几个组件各不相同,我们根据它们的特点定制了不同的插件化方案:那么对于Co ...
- Android 插件化原理解析——Service的插件化
在 Activity生命周期管理 以及 广播的管理 中我们详细探讨了Android系统中的Activity.BroadcastReceiver组件的工作原理以及它们的插件化方案,相信读者已经对Andr ...
- Android插件化原理解析——广播的管理
在Activity生命周期管理 以及 插件加载机制 中我们详细讲述了插件化过程中对于Activity组件的处理方式,为了实现Activity的插件化我们付出了相当多的努力:那么Android系统的其他 ...
- Android插件化原理解析——Hook机制之动态代理
使用代理机制进行API Hook进而达到方法增强是框架的常用手段,比如J2EE框架Spring通过动态代理优雅地实现了AOP编程,极大地提升了Web开发效率:同样,插件框架也广泛使用了代理机制来增强系 ...
- Android插件化原理解析——概要
2015年是Android插件化技术突飞猛进的一年,随着业务的发展各大厂商都碰到了Android Native平台的瓶颈: 从技术上讲,业务逻辑的复杂导致代码量急剧膨胀,各大厂商陆续出到65535方法 ...
最新文章
- Linux虚拟文件系统解析
- ABP文档翻译--值对象
- 中3d库后接负载_500W电源横评:交叉负载放倒3款产品
- 通过豆瓣Api,输入ISBN获取图书信息
- 蓝桥杯2017年第八届C/C++省赛C组第二题-兴趣小组
- 实验四------实验十二
- STM32-关于Proteus 仿真无法运行STM32CubeMX自动生成的代码
- unity 给模型绑定骨骼_五年游戏建模实战经验,总结了一套项目模型规范及制作的注意事项...
- 持续交付2.0 pdf_便捷下载发布v7.2.0版本更新
- win10系统怎么改奇摩输入法_教你打造最强「Windows 10」微软拼音输入法 + 600万词库下载...
- 在线web魔方和在线AI象棋
- 软件开发的需求文档如何去写
- 日记侠:如何提高朋友圈活跃度,给你5种实用方法
- 配置SecureCRT密匙登录
- 椭圆形建筑——逸夫演艺中心
- SHIMANO各个等级配件的区别
- java剑姬_Java虚拟机非常有用的性能监控工具
- C#之基于winform窗体绘制简单图形
- EWS Java API 的基本使用
- Holt_Winters三次平滑指数实现