Qigsaw源码之Gradle插件解析

2024-02-09 06:58

本文主要是介绍Qigsaw源码之Gradle插件解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

hi,2021

Android App Bundle为Qigsaw的前置依赖知识点。

Android App Bundle 是Android新推出的一种官方发布格式.aab,可让您以更高效的方式开发和发布应用。借助 Android App Bundle,您可以更轻松地以更小的应用提供优质的使用体验,从而提升安装成功率并减少卸载量。转换过程轻松便捷。您无需重构代码即可开始获享较小应用的优势。改用这种格式后,您可以体验模块化应用开发和可自定义功能交付,并从中受益(PS:必须依赖于GooglePlay)。

qigsaw基于AAB实现,同时完全仿照AAB提供的play core library接口加载插件,开发查阅官方文档即可开始开发。如果有国际化需求的公司可以在国内版和国际版上无缝切换。同时Qigsaw实现0 hook,仅有少量私有 API 访问,保证其兼容性和稳定性。

Github:Qigsaw

本篇文章主要讲述Qigsaw相关的plugin

Qigsaw插件

主工程进行进行apply plugin: 'com.iqiyi.qigsaw.application'插件的依赖;

feature工程进行以下依赖:

apply plugin: 'com.android.dynamic-feature'
apply plugin: 'com.iqiyi.qigsaw.dynamicfeature'

gradle.properties文件中配置QIGSAW_BUILD=true,才会有feature包的一些信息生成。

com.iqiyi.qigsaw.application

com.iqiyi.qigsaw.application.properties文件内容为:

implementation-class=com.iqiyi.qigsaw.buildtool.gradle.QigsawAppBasePlugin

QigsawAppBasePlugin默认会注册一个 SplitComponentTransform,在开启QIGSAW_BUILD=true之后还会注册SplitResourcesLoaderTransform。通过 Transform实现对插件内容的AOP

QigsawAppBasePlugin除过注册两个Transform之外,为主要的是处理插件和基础包信息生成Qigsaw产物。

com.iqiyi.qigsaw.dynamicfeature

com.iqiyi.qigsaw.dynamicfeature.properties文件内容为:

implementation-class=com.iqiyi.qigsaw.buildtool.gradle.QigsawDynamicFeaturePlugin

QigsawDynamicFeaturePlugin在开启QIGSAW_BUILD=true之后会注册SplitResourcesLoaderTransform以及SplitLibraryLoaderTransform实现对插件内容的AOP

SplitResourcesLoaderTransform

主要是向ActivityServiceReceiver类中的getResources注入SplitInstallHelper.loadResources(this, super.getResources())

interface SplitComponentWeaver {/*** 链接目标*/String CLASS_WOVEN = "com/google/android/play/core/splitinstall/SplitInstallHelper"/*** 链接方法*/String METHOD_WOVEN = "loadResources"byte[] weave(InputStream inputStream)
}

相关注入类为:

class SplitResourcesLoaderInjector {WaitableExecutor waitableExecutor/*** 预埋的 Activity*/Set<String> activitiesSet<String> servicesSet<String> receiversSplitActivityWeaver activityWeaverSplitServiceWeaver serviceWeaverSplitReceiverWeaver receiverWeaver/**部分代码省略**/
}

其中基础包和插件的区别主要是注册的目标不同:

基础包只是读取build.gradle文件中的qigsawSplit.baseContainerActivities配置的Activity

而插件需要读取AndroidManifest.xml文件中的ActivityServiceReceiver

SplitInstallHelper.loadResources(this, super.getResources());的作用是将所有插件资源路径添加到AssetManager中,这样各个插件就可以访问所有的资源,关键实现代码如下:

static Method getAddAssetPathMethod() throws NoSuchMethodException {if (addAssetPathMethod == null) {addAssetPathMethod = HiddenApiReflection.findMethod(AssetManager.class, "addAssetPath", String.class);}return addAssetPathMethod;
}

SplitComponentTransform

Transform主要进行了两个操作 :

  • 读取各个插件apkManifest文件,创建ComponentInfo类并将将各个插件apkApplication,Activity,Service,Recevier记录在该类的字段中,字段名称以工程名+组件类型命名,值为各个插件apk包含的组件,如过包含多个用逗号隔开。
//com.iqiyi.android.qigsaw.core.extension.ComponentInfo
public class ComponentInfo {public static final String native_ACTIVITIES = "com.iqiyi.qigsaw.sample.ccode.NativeSampleActivity";public static final String java_ACTIVITIES = "com.iqiyi.qigsaw.sample.java.JavaSampleActivity";public static final String java_APPLICATION = "com.iqiyi.qigsaw.sample.java.JavaSampleApplication";
}
  • 为每个provider创建代理类 类名为String providerClassName=providerName+"Decorated"+splitName,其中providerName为原始provider类名,splitName为插件apk对应的名称,并且该类继承SplitContentProvider
public class JavaContentProvider_Decorated_java extends SplitContentProvider {}

provider_deccorated.png

为啥这么做呢?

因为在app启动时provider的执行时机是比较靠前的,
Application->attachBaseContext ==>ContentProvider->onCreate ==>Application->onCreate ==>Activity->onCreate在这个过程中我们的插件apk并没有加载进来,一定会报ClassNotFound。所以我们将插件apkprovider生成一个代理类,然后替换掉,如果插件没有加载进来,代理类什么也不执行就可以了。很好的解决了我们的问题。

SplitLibraryLoaderTransform

SplitLibraryLoaderTransform类进行的操作是向dynamic-feature构建apk的过程中,创建以 "com.iqiyi.android.qigsaw.core.splitlib." + project.name + "SplitLibraryLoader"的类。

// com.iqiyi.android.qigsaw.core.splitlib.assetsSplitLibraryLoader
// com.iqiyi.android.qigsaw.core.splitlib.javaSplitLibraryLoader
// com.iqiyi.android.qigsaw.core.splitlib.nativeSplitLibraryLoader
package com.iqiyi.android.qigsaw.core.splitlib;
public class javaSplitLibraryLoader {public void loadSplitLibrary(String str) {System.loadLibrary(str);}
}

这个类的作用是啥呢?

下面我们来解释一下,你会发现很有趣的。

  • Qigsaw是基于对于com.google.android.play.core对外暴露的方法,进行了自定义实现。因为aab目前只能对google play上发布应用起作用,所以开发者重新实现了一套com.google.android.play.core包名的第三方库,这样就可以做到在国内市场,与国外应用市场无缝迁移。
  • Qigsaw提供两种加载方式加载插件apk,单Classloader和多Classloader模式,单Classloader涉及私有api访问,而多Classloader不涉及私有api访问。

该类的存在就是为了解决多Classloader模式下的so加载问题
System.loadLibrary(str);该方法会使用调用方的classloader从中获取so信息并加载。

//java.lang.System.java
@CallerSensitive
public static void loadLibrary(String libname) {Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}

由于多Classloader模式下,每个插件都要各自的Classloader,sodex都在各自的Classloader中记录,所以在多Classloader模式下, System.loadLibrary应由插件apk各自的Classloader调用。具体实现可参考SplitLibraryLoaderHelper类。

//com.iqiyi.android.qigsaw.core.splitload.SplitLibraryLoaderHelper.java
private static boolean loadSplitLibrary0(ClassLoader classLoader, String splitName, String name) {try {Class<?> splitLoaderCl = classLoader.loadClass("com.iqiyi.android.qigsaw.core.splitlib." + splitName + "SplitLibraryLoader");Object splitLoader = splitLoaderCl.newInstance();Method method = HiddenApiReflection.findMethod(splitLoaderCl, "loadSplitLibrary", String.class);method.invoke(splitLoader, name);return true;} catch (Throwable ignored) {}return false;
}

Qigsaw编译解析

Qigsaw打包流程

qigsaw_plugin_flow_chart.png

copySplitManifestDebug

实现feature包下生成的AndroidManifest.xml文件的拷贝。

目标文件和地址:featureName/build/intermediates/merged_manifests/debug/AndroidManifest.xml

拷贝后的地址:app/build/intermediates/qigsaw/split-outputs/manifests/debug

拷贝后的文件名:$featureName.xml

ProcessTaskDependenciesBetweenBaseAndSplitsWithQigsaw

触发copySplitManifestDebug任务,将feature包生成的产物和数据输出到qigsawProcessDebugManifest任务中。

extractTargetFilesFromOldApk

app_debug.apk解压 将assets/目录下所有内容释放到app/build/intermediates/qigsaw/old-apk/target-files/xxx

qigsawProcessDebugManifest

SplitComponentTransform创建的$ContentProviderName_Decorated_$featureName继承SplitContentProvider代替原有的Provider

因为Provider 在应用启动的时候就需要加载,避免这个时候feature包没有下载下来,先加载一个代理的Provider

provider.png

generateDebugQigsawConfig

生成以下文件:

@Keep
public final class QigsawConfig {public static final String DEFAULT_SPLIT_INFO_VERSION = "1.0.0_1.0.0";public static final String[] DYNAMIC_FEATURES = {"java", "assets", "native"};public static final String QIGSAW_ID = "1.0.0_c40ab5d";public static final boolean QIGSAW_MODE = Boolean.parseBoolean("true");public static final String VERSION_NAME = "1.0.0";
}

QIGSAW_ID回先获取基础包的id,如果没有那么为当前的QigsawId

processSplitApkDebug

每个feature都需要执行的任务,分别处理自己的的apk并生成对应的json文件。

  • feature包的apk文件解压到app/build/intermediates/qigsaw/split-outputs/unzip/debug/$featureName文件;

  • 遍历解压apk中的lib文件目录,找到支持的ABI;

  • 如果有lib文件有so文件,那么在该目录生成一个AndroidManifest.xml文件;

    • lib文件和生成的AndroidManifest.xml压缩为protoAbiApk;

    • 利用aapt2工具将 protoAbiApkbinaryAbiApk中;

    • binaryAbiApk进行签名生成app/build/intermediates/qigsaw/split-outputs/apks/debug/$feature-$abi.apk;

    • 生成SplitInfo.SplitApkData数据;

      {"abi": "x86","url": "assets://qigsaw/native-x86.zip","md5": "03a29962b87c6ed2a7961b6dbe45f532","size": 8539
      }
      
  • 遍历解压apk除过lib之前的文件目录,压缩为$fearure-master-unsigned.apk。签名生成app/build/intermediates/qigsaw/split-outputs/apks/debug/$feature-master.apk;

  • 生成SplitInfo.SplitApkData数据;

    {"abi": "master","url": "assets://qigsaw/native-master.zip","md5": "3b89066aeaf7d2c2a59b4f3a10fef345","size": 12824
    }
    
  • 更具lib文件下的数据生成SplitInfo.SplitLibData数据;

    {"abi": "arm64-v8a","jniLibs": [{"name": "libhello-jni.so","md5": "2938d8b40825e82715422dbdba479e4f","size": 5896}]
    }
    
  • 最后生成每个featureSplitInfo数据,写入/app/build/intermediates/qigsaw/split-outputs/split-info/debug/$featureName.json文件;

    public class SplitInfo implements Cloneable, GroovyObject {private String splitName;//feature包名称private boolean builtIn;//!onDemand||!releaseSplitApk(releaseSplitApk是gradle中配置项)private boolean onDemand;//取自AndroidManifest.xml中的onDemandprivate String applicationName;//feature应用名private String version;//feature包中的versionname@versioncodeprivate int minSdkVersion;//feature最低版本private int dexNumber;//feature包中的dex数量private Set<String> dependencies;//feature包的依赖;private Set<String> workProcesses;//feature包AndroidManifest.xml中的Activity、Service、Receiver、provider配置的进程;private List<SplitInfo.SplitApkData> apkData;//SplitInfo.SplitApkData数据private List<SplitInfo.SplitLibData> libData;//SplitInfo.SplitLibData数据
    }
    

qigsawAssembleDebug

  • build/intermediates/qigsaw/split-outputs/split-info/debug中的每个feature包生成的json合并;
  • 合并之后的文件与基础包中的Qigsaw配置文件进行对比,生成新的增量Qigsaw配置文件;
    • 对比规则是verisonName相等的时候对比split.version,有一个不同就表示有更新;
    • 如果有更新,那么QigsawId为基础包的QigsawId,并分析和修改split信息;
      • 修改split信息的时候,相同的splitName对比split.version。如果相同那么split使用基础包的split信息,如果不同那么该splitbuiltIn=falseonDemand=true。并将有更新的split做记录(updatesplits字段值)。此时updateMode值为VERSION_CHANGED=1
      • 没有任何修改,那么updateMode值为VERSION_NO_CHANGED=2
      • 如果没有基础包,那么updateMode值为DEFAULT=0
  • 分别判断如果feature包的builtInfalse
    • 判断是否有上传服务,如有有那么上传feature包。上传成功后将对应的url地址修改为可下载的http地址。如果地址为空,或者不是http开头会跑异常。
    • 如果没有实现上传服务那么builtIn置为true
  • 格式化split内容,写到build/intermediates/qigsaw/split-details/debug文件目录下。
  • updateMode值写到build/intermediates/qigsaw/split-details/debug/_update_record_.json文件。
  • 如果updateMode值为VERSION_NO_CHANGED,那么将intermediates/qigsaw/old-apk/target-files/debug/assets/qigsaw/qigsaw_*.json文件拷贝到app/build/intermediates/merged_assets/debug/out/qigsaw/qigsaw_*.json;
    • 否则将app/build/intermediates/qigsaw/split-details/debug/qigsaw_*.json文件拷贝到app/build/intermediates/merged_assets/debug/out/qigsaw/qigsaw_*.json;
  • app/build/intermediates/qigsaw/split-details/debug/base.app.cpu.abilist.properties写入支持的abi,并将其拷贝到app/build/intermediates/merged_assets/debug/out/下面;
  • 遍历feature生成的splitinfo信息,如果builtIntrue;
    • 如果updateMode值为DEFAULT=0,将将app/build/intermediates/qigsaw/split-outputs/apks/debug/*.apk拷贝到app/build/intermediates/merged_assets/debug/out/qigsaw/*.zip
    • 如果updateMode值为DEFAULT!=0,判断该feature是否是在updateSplits中;
      • 如果是那么将app/build/intermediates/qigsaw/split-outputs/apks/debug/*.apk拷贝到app/build/intermediates/merged_assets/debug/out/qigsaw/*.zip
      • 如果不是将app/build/intermediates/qigsaw/old-apk/target-files/debug/assets/qigsaw/*.zip拷贝到app/build/intermediates/merged_assets/debug/out/qigsaw/*.zip

产物

Qigsaw配置文件

{"qigsawId": "1.0.0_c40ab5d","appVersionName": "1.0.0","updateSplits": ["java"],"splits": [{"splitName": "java","builtIn": true,"onDemand": false,"applicationName": "com.iqiyi.qigsaw.sample.java.JavaSampleApplication","version": "1.1@1","minSdkVersion": 14,"dexNumber": 2,"workProcesses": [""],"apkData": [{"abi": "master","url": "assets://qigsaw/java-master.zip","md5": "658bc419a9d3c7812a36e61f6c5be4c4","size": 12822}]}{"splitName": "native","builtIn": true,"onDemand": true,"version": "1.0@1","minSdkVersion": 14,"dexNumber": 2,"apkData": [{"abi": "arm64-v8a","url": "assets://qigsaw/native-arm64-v8a.zip","md5": "b01ad63db38a4ec5fad3284c573a02d3","size": 8545},{"abi": "master","url": "assets://qigsaw/native-master.zip","md5": "3c41745a16a31e967cde8247009463f1","size": 12824}],"libData": [{"abi": "arm64-v8a","jniLibs": [{"name": "libhello-jni.so","md5": "2938d8b40825e82715422dbdba479e4f","size": 5896}]}]}]
}

Qigsaw加载的压缩包

app-debug.png

下期研究知识点

  • 混淆相关使用操作;
  • Tinker热修改相关使用操作;

文章到这里就全部讲述完啦,若有其他需要交流的可以留言哦

想阅读作者的更多文章,可以查看我的公共号:
振兴书城

这篇关于Qigsaw源码之Gradle插件解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/693463

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

OWASP十大安全漏洞解析

OWASP(开放式Web应用程序安全项目)发布的“十大安全漏洞”列表是Web应用程序安全领域的权威指南,它总结了Web应用程序中最常见、最危险的安全隐患。以下是对OWASP十大安全漏洞的详细解析: 1. 注入漏洞(Injection) 描述:攻击者通过在应用程序的输入数据中插入恶意代码,从而控制应用程序的行为。常见的注入类型包括SQL注入、OS命令注入、LDAP注入等。 影响:可能导致数据泄

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

CSP 2023 提高级第一轮 CSP-S 2023初试题 完善程序第二题解析 未完

一、题目阅读 (最大值之和)给定整数序列 a0,⋯,an−1,求该序列所有非空连续子序列的最大值之和。上述参数满足 1≤n≤105 和 1≤ai≤108。 一个序列的非空连续子序列可以用两个下标 ll 和 rr(其中0≤l≤r<n0≤l≤r<n)表示,对应的序列为 al,al+1,⋯,ar​。两个非空连续子序列不同,当且仅当下标不同。 例如,当原序列为 [1,2,1,2] 时,要计算子序列 [