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加载插件资源;
    • 如果是插件包
      • 获取split的mergedManifest文件,读取activities,services,receivers,分别使用各自的Weaver 进行编织:

        • SplitServiceWeaver: 访问service的所有方法,在onCreate方法中注入SplitInstallHelper#loadResources方法
        • SplitReceiverWeaver: 访问receiver的所有方法,在onReceive方法中注入SplitInstallHelper#loadResources方法

主要的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);的方法;

图: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;
  • 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;
  • AABExtension单例 调用 createAndActiveSplitApplication方法,找到已安装split的application,并调用attach方法(此处的安装应该是aab方式的安装,非qigsaw的安装);

  • SplitCompat 使用单例 初始化 SplitSessionLoaderImpl(SplitSessionLoader)、初始化 LoadedSplitFetcherImpl(LoadedSplitFetcher) ,提供SplitLoadManager的快捷调用入口;

    • SplitSessionLoaderImpl : 提供 SplitLoadSessionTask的启动, 最终逻辑是通过SplitLoadManagerImpl#createSplitLoadTask启动split的加载过程;
    • LoadedSplitFetcherImpl: 提供快捷的获取已加载splitName,最终调用SplitLoadManager#getLoadedSplitNames;

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的actioncom.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;
    • 构建SplitInstallListenerRegistry, 封装了一个广播接收者,接受安装过程中的状态变化;
      • 提供注册安装状态监听者用于监听安装状态变化;
      • onReceived 方法中获取传递的Bundle构建SplitInstallSessionState 代表进行的安装状态,如果监听的status为SplitInstallInternalSessionStatus.POST_INSTALLED(10)代表split已经安装和抽取优化(手机兼容),但是没有加载,此时需要直接进行加载过程:
        • 使用SplitCompat保留的加载快捷入口SplitSessionLoaderImpl单例 发送SplitLoadSessionTask,调用SplitLoadManagerImpl#createSplitLoadTask执行加载过程-加载 第二阶段,并设置加载监听;
        • 监听到安装结果发布到所有订阅Listener的监听;
  • 安装过程-通信 SplitInstallManagerImpl#startInstall

    主要是建立进程间通信的通道,然后调用Server端的安装方法asset目录中的splitApk包,使用回调Binder通知Client端安装状态;类调用过程: SplitInstallManagerImpl单例 -> splitinstall.SplitInstallService -> RemoteManager

    • 判断是否已安装过:SplitInstallManagerImpl#getInstalledModules

      • 使用aab fusedModule方式和SplitCompat保留的入口类 LoadedSplitFetcherImpl判断是否已安装指定的splits,已安装发送SplitInstalledDisposer: 直接使用registry对象发送已安装状态的bundle并发布至listener;未安装则进行安装过程;
    • 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给注册监听者 ;
  • 安装过程-安装 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文件,并作为第一次安装的标记;
      * 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信息;
  • 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;
      • 返回splitApks的总大小和本地需要下载的大小参数;
    • 调用回调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,用于执行后续的加载过程第二阶段;
    • realTotalBytesNeedToDownload>0 ,如果大于configuration设置的阈值,开启configuration设置的确认下载Act页面;更改当前sessionState为PENDING并发送广播; 使用自定义下载接口开启下载,安装目录等信息记录在DownloadRequest作为参数传递,待下载完成后同样调用StartDownloadCallback#onCompleted
  • startUninstall

    • 读取qigsaw的安装目录中uninstall目录uninstallsplits.infoproperties文件,
    • 从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版本号目录,只保留一个最常使用的版本目录;

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管理器统一管理;
    • SplitPathManager#install : 创建SplitPathManager 单例 操作本地安装目录
      • 主要提供splits安装路径的快捷访问,即split本地存储路径: /data/data/packagename/app_qigsaw/$qigsawid/;
  • 加载过程-准备 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返回;
      • 记录所有创建的intent返回splitFileIntents
    • 使用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监听则回调。其中安装过程结束开启加载过程会设置此监听,用于安装完开启加载过程,预加载方法不会设置此监听;
    • 按照cl的模式分为两个实现类(SplitLoaderWrapper),主要重写loadCode方法指定使用哪种SplitClassLoader加载类
      • SplitLoadTaskImpl 多cl模式加载

        • 为每个split构建SplitDexClassLoader,SplitDexClassLoader#create并存于容器单例SplitApplicationLoaders中,如果加载失败则使用当前split依赖的split的splitClassLoader尝试加载;
      • 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文件兼容

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;

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组件;

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对象;
    • 重写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的组件信息;
  • #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插件化流程解析相关推荐

  1. Android 插件化原理解析——Activity生命周期管理

    之前的 Android插件化原理解析 系列文章揭开了Hook机制的神秘面纱,现在我们手握倚天屠龙,那么如何通过这种技术完成插件化方案呢?具体来说,插件中的Activity,Service等组件如何在A ...

  2. Android 插件化原理解析——Hook机制之AMSPMS

    在前面的文章中我们介绍了DroidPlugin的Hook机制,也就是代理方式和Binder Hook:插件框架通过AOP实现了插件使用和开发的透明性.在讲述DroidPlugin如何实现四大组件的插件 ...

  3. Android插件化原理解析——ContentProvider的插件化

    目前为止我们已经完成了Android四大组件中Activity,Service以及BroadcastReceiver的插件化,这几个组件各不相同,我们根据它们的特点定制了不同的插件化方案:那么对于Co ...

  4. Android插件化原理解析

    概述 Android插件化技术,可以实现功能模块的按需加载和动态更新,其本质是动态加载未安装的apk. 本文涉及源码为API 28 插件化原理 插件化要解决的三个核心问题: 类加载. 资源加载. 组件 ...

  5. 插件化原理解析——ContentProvider的插件化

    目前为止我们已经完成了Android四大组件中Activity,Service以及BroadcastReceiver的插件化,这几个组件各不相同,我们根据它们的特点定制了不同的插件化方案:那么对于Co ...

  6. Android 插件化原理解析——Service的插件化

    在 Activity生命周期管理 以及 广播的管理 中我们详细探讨了Android系统中的Activity.BroadcastReceiver组件的工作原理以及它们的插件化方案,相信读者已经对Andr ...

  7. Android插件化原理解析——广播的管理

    在Activity生命周期管理 以及 插件加载机制 中我们详细讲述了插件化过程中对于Activity组件的处理方式,为了实现Activity的插件化我们付出了相当多的努力:那么Android系统的其他 ...

  8. Android插件化原理解析——Hook机制之动态代理

    使用代理机制进行API Hook进而达到方法增强是框架的常用手段,比如J2EE框架Spring通过动态代理优雅地实现了AOP编程,极大地提升了Web开发效率:同样,插件框架也广泛使用了代理机制来增强系 ...

  9. Android插件化原理解析——概要

    2015年是Android插件化技术突飞猛进的一年,随着业务的发展各大厂商都碰到了Android Native平台的瓶颈: 从技术上讲,业务逻辑的复杂导致代码量急剧膨胀,各大厂商陆续出到65535方法 ...

最新文章

  1. Linux虚拟文件系统解析
  2. ABP文档翻译--值对象
  3. 中3d库后接负载_500W电源横评:交叉负载放倒3款产品
  4. 通过豆瓣Api,输入ISBN获取图书信息
  5. 蓝桥杯2017年第八届C/C++省赛C组第二题-兴趣小组
  6. 实验四------实验十二
  7. STM32-关于Proteus 仿真无法运行STM32CubeMX自动生成的代码
  8. unity 给模型绑定骨骼_五年游戏建模实战经验,总结了一套项目模型规范及制作的注意事项...
  9. 持续交付2.0 pdf_便捷下载发布v7.2.0版本更新
  10. win10系统怎么改奇摩输入法_教你打造最强「Windows 10」微软拼音输入法 + 600万词库下载...
  11. 在线web魔方和在线AI象棋
  12. 软件开发的需求文档如何去写
  13. 日记侠:如何提高朋友圈活跃度,给你5种实用方法
  14. 配置SecureCRT密匙登录
  15. 椭圆形建筑——逸夫演艺中心
  16. SHIMANO各个等级配件的区别
  17. java剑姬_Java虚拟机非常有用的性能监控工具
  18. C#之基于winform窗体绘制简单图形
  19. EWS Java API 的基本使用
  20. Holt_Winters三次平滑指数实现

热门文章

  1. CSS- 外边距重叠问题
  2. java 获取下拉框的值_Java获取下拉菜单选中的选项
  3. B站不再“死磕”二次元游戏,释放了怎样的信号?
  4. 兰州市第五医院内六病区感染科简介及部分疾病健康教育
  5. Google中国地图API应用
  6. 美国西点军校的育人之道
  7. 2021年煤炭生产经营单位(安全生产管理人员)考试题库及煤炭生产经营单位(安全生产管理人员)免费试题
  8. 012SpringBoot-Shiro(安全框架)
  9. 花式实现时间轴,样式由你来定!
  10. 通过CubeMX实现STM32的USB支持