其實Transform API
在一個android工程的打包流程中做用很是大, 像是咱們熟知的混淆處理, 類文件轉dex文件的處理, 都是經過Transform API
去完成的. 本篇內容主要圍繞Transform
作展開:html
Transform API
的使用及原理ASM
使用技巧Transform API
在應用工程上的使用摸索自從1.5.0-beta1
版本開始, android gradle插件就包含了一個Transform API
, 它容許第三方插件在編譯後的類文件轉換爲dex文件以前作處理操做. 而使用Transform API
, 咱們徹底能夠不用去關注相關task的生成與執行流程, 它讓咱們能夠只聚焦在如何對輸入的類文件進行處理java
Transform的註冊和使用很是易懂, 在咱們自定義的plugin內, 咱們能夠經過android.registerTransform(theTransform)
或者android.registerTransform(theTransform, dependencies).
就能夠進行註冊.android
class DemoPlugin: Plugin<Project> {
override fun apply(target: Project) {
val android = target.extensions.findByType(BaseExtension::class.java)
android?.registerTransform(DemoTransform())
}
}
複製代碼
而咱們自定義的Transform
繼承於com.android.build.api.transform.Transform
, 具體咱們能夠看javaDoc, 如下代碼是比較常見的transform處理模板git
class DemoTransform: Transform() {
/** * transform 名字 */
override fun getName(): String = "DemoTransform"
/** * 輸入文件的類型 * 可供咱們去處理的有兩種類型, 分別是編譯後的java代碼, 以及資源文件(非res下文件, 而是assests內的資源) */
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> = TransformManager.CONTENT_CLASS
/** * 是否支持增量 * 若是支持增量執行, 則變化輸入內容可能包含 修改/刪除/添加 文件的列表 */
override fun isIncremental(): Boolean = false
/** * 指定做用範圍 */
override fun getScopes(): MutableSet<in QualifiedContent.Scope> = TransformManager.SCOPE_FULL_PROJECT
/** * transform的執行主函數 */
override fun transform(transformInvocation: TransformInvocation?) {
transformInvocation?.inputs?.forEach {
// 輸入源爲文件夾類型
it.directoryInputs.forEach {directoryInput->
with(directoryInput){
// TODO 針對文件夾進行字節碼操做
val dest = transformInvocation.outputProvider.getContentLocation(
name,
contentTypes,
scopes,
Format.DIRECTORY
)
file.copyTo(dest)
}
}
// 輸入源爲jar包類型
it.jarInputs.forEach { jarInput->
with(jarInput){
// TODO 針對Jar文件進行相關處理
val dest = transformInvocation.outputProvider.getContentLocation(
name,
contentTypes,
scopes,
Format.JAR
)
file.copyTo(dest)
}
}
}
}
}
複製代碼
每個Transform
都聲明它的做用域, 做用對象以及具體的操做以及操做後輸出的內容.github
經過Transform#getScopes
方法咱們能夠聲明自定義的transform的做用域, 指定做用域包括以下幾種算法
QualifiedContent.Scope | |
---|---|
EXTERNAL_LIBRARIES | 只包含外部庫 |
PROJECT | 只做用於project自己內容 |
PROVIDED_ONLY | 支持compileOnly的遠程依賴 |
SUB_PROJECTS | 子模塊內容 |
TESTED_CODE | 當前變體測試的代碼以及包括測試的依賴項 |
經過Transform#getInputTypes
咱們能夠聲明其的做用對象, 咱們能夠指定的做用對象只包括兩種api
QualifiedContent.ContentType | |
---|---|
CLASSES | Java代碼編譯後的內容, 包括文件夾以及Jar包內的編譯後的類文件 |
RESOURCES | 基於資源獲取到的內容 |
TransformManager
整合了部分經常使用的Scope以及Content集合, 若是是application
註冊的transform, 一般狀況下, 咱們通常指定TransformManager.SCOPE_FULL_PROJECT
;若是是library
註冊的transform, 咱們只能指定TransformManager.PROJECT_ONLY
, 咱們能夠在LibraryTaskManager#createTasksForVariantScope
中看到相關的限制報錯代碼瀏覽器
Sets.SetView<? super Scope> difference =
Sets.difference(transform.getScopes(), TransformManager.PROJECT_ONLY);
if (!difference.isEmpty()) {
String scopes = difference.toString();
globalScope
.getAndroidBuilder()
.getIssueReporter()
.reportError(
Type.GENERIC,
new EvalIssueException(
String.format(
"Transforms with scopes '%s' cannot be applied to library projects.",
scopes)));
}
複製代碼
而做用對象咱們主要經常使用到的是TransformManager.CONTENT_CLASS
網絡
咱們經過實現Transform#transform
方法來處理咱們的中間轉換過程, 而中間相關信息都是經過TransformInvocation
對象來傳遞app
public interface TransformInvocation {
/** * transform的上下文 */
@NonNull
Context getContext();
/** * 返回transform的輸入源 */
@NonNull
Collection<TransformInput> getInputs();
/** * 返回引用型輸入源 */
@NonNull Collection<TransformInput> getReferencedInputs();
/** * 額外輸入源 */
@NonNull Collection<SecondaryInput> getSecondaryInputs();
/** * 輸出源 */
@Nullable
TransformOutputProvider getOutputProvider();
/** * 是否增量 */
boolean isIncremental();
}
複製代碼
關於輸入源, 咱們能夠大體分爲消費型和引用型和額外的輸入源
消費型
就是咱們須要進行transform操做的, 這類對象在處理後咱們必須指定輸出傳給下一級, 咱們主要經過getInputs()
獲取進行消費的輸入源, 而在進行變換後, 咱們也必須經過設置getInputTypes()
和getScopes()
來指定輸出源傳輸給下個transform.getReferencedScopes()
指定咱們的引用型輸入源的做用域後, 咱們能夠經過TransformInvocation#getReferencedInputs()
獲取引用型輸入源ProGuardTransform
中, 就會指定建立mapping.txt傳給下一級; 一樣像是DexMergerTransform
, 若是打開了multiDex
功能, 則會將maindexlist.txt文件傳給下一級咱們已經大體瞭解它是如何使用的, 如今看下他的原理(本篇源碼基於gradle插件3.3.2
版本)在去年AppPlugin源碼解析中, 咱們粗略瞭解了android的com.android.application
以及com.android.library
兩個插件都繼承於BasePlugin
, 而他們的主要執行順序能夠分爲三個步驟
在BaseExtension
內部維護了一個transforms
集合對象, android.registerTransform(theTransform)
實際上就是將咱們自定義的transform實例新增到這個列表對象中. 在3.3.2
的源碼中, 也能夠這樣理解. 在BasePlugin#createAndroidTasks
中, 咱們經過VariantManager#createAndroidTasks
建立各個變體的相關編譯任務, 最終經過TaskManager#createTasksForVariantScope
(application
插件最終實現方法在TaskManager#createPostCompilationTasks
中, 而library
插件最終實現方法在LibraryTaskManager#createTasksForVariantScope
中)方法中獲取BaseExtension
中維護的transforms
對象, 經過TransformManager#addTransform
將對應的transform對象轉換爲task, 註冊在TaskFactory
中.這裏關於一系列Transform Task
的執行流程, 咱們能夠選擇看下application
內的相關transform流程, 因爲篇幅緣由, 能夠自行去看相關源碼, 這裏的transform task流程分別是從Desugar->MergeJavaRes->自定義的transform->MergeClasses->Shrinker(包括ResourcesShrinker和DexSplitter和Proguard)->MultiDex->BundleMultiDex->Dex->ResourcesShrinker->DexSplitter, 由此調用鏈, 咱們也能夠看出在處理類文件的時候, 是不須要去考慮混淆的處理的.
TransformManager
管理了項目對應變體的全部Transform
對象, 它的內部維護了一個TransformStream
集合對象streams
, 每當新增一個transform, 對應的transform會消費掉對應的流, 然後將處理後的流添加會streams
內
public class TransformManager extends FilterableStreamCollection{
private final List<TransformStream> streams = Lists.newArrayList();
}
複製代碼
咱們能夠看下它的核心方法addTransform
@NonNull
public <T extends Transform> Optional<TaskProvider<TransformTask>> addTransform(
@NonNull TaskFactory taskFactory,
@NonNull TransformVariantScope scope,
@NonNull T transform,
@Nullable PreConfigAction preConfigAction,
@Nullable TaskConfigAction<TransformTask> configAction,
@Nullable TaskProviderCallback<TransformTask> providerCallback) {
...
List<TransformStream> inputStreams = Lists.newArrayList();
// transform task的命名規則定義
String taskName = scope.getTaskName(getTaskNamePrefix(transform));
// 獲取引用型流
List<TransformStream> referencedStreams = grabReferencedStreams(transform);
// 找到輸入流, 並計算經過transform的輸出流
IntermediateStream outputStream = findTransformStreams(
transform,
scope,
inputStreams,
taskName,
scope.getGlobalScope().getBuildDir());
// 省略代碼是用來校驗輸入流和引用流是否爲空, 理論上不可能爲空, 若是爲空, 則說明中間有個transform的轉換處理有問題
...
transforms.add(transform);
// transform task的建立
return Optional.of(
taskFactory.register(
new TransformTask.CreationAction<>(
scope.getFullVariantName(),
taskName,
transform,
inputStreams,
referencedStreams,
outputStream,
recorder),
preConfigAction,
configAction,
providerCallback));
}
複製代碼
在TransformManager
中添加一個Transform
管理, 流程可分爲如下幾步
static String getTaskNamePrefix(@NonNull Transform transform) {
StringBuilder sb = new StringBuilder(100);
sb.append("transform");
sb.append(
transform
.getInputTypes()
.stream()
.map(
inputType ->
CaseFormat.UPPER_UNDERSCORE.to(
CaseFormat.UPPER_CAMEL, inputType.name()))
.sorted() // Keep the order stable.
.collect(Collectors.joining("And")));
sb.append("With");
StringHelper.appendCapitalized(sb, transform.getName());
sb.append("For");
return sb.toString();
}
複製代碼
從上面代碼, 咱們能夠看到新建的transform task的命名規則能夠理解爲transform${inputType1.name}And${inputType2.name}With${transform.name}For${variantName}
, 對應的咱們也能夠經過已生成的transform task來驗證
streams
做用域和做用類型的交集來獲取對應的流, 將其定義爲咱們須要的引用型流
private List<TransformStream> grabReferencedStreams(@NonNull Transform transform) {
Set<? super Scope> requestedScopes = transform.getReferencedScopes();
...
List<TransformStream> streamMatches = Lists.newArrayListWithExpectedSize(streams.size());
Set<ContentType> requestedTypes = transform.getInputTypes();
for (TransformStream stream : streams) {
Set<ContentType> availableTypes = stream.getContentTypes();
Set<? super Scope> availableScopes = stream.getScopes();
Set<ContentType> commonTypes = Sets.intersection(requestedTypes,
availableTypes);
Set<? super Scope> commonScopes = Sets.intersection(requestedScopes, availableScopes);
if (!commonTypes.isEmpty() && !commonScopes.isEmpty()) {
streamMatches.add(stream);
}
}
return streamMatches;
}
複製代碼
private IntermediateStream findTransformStreams( @NonNull Transform transform, @NonNull TransformVariantScope scope, @NonNull List<TransformStream> inputStreams, @NonNull String taskName, @NonNull File buildDir) {
Set<? super Scope> requestedScopes = transform.getScopes();
...
Set<ContentType> requestedTypes = transform.getInputTypes();
// 獲取消費型輸入流
// 並將streams中移除對應的消費型輸入流
consumeStreams(requestedScopes, requestedTypes, inputStreams);
// 建立輸出流
Set<ContentType> outputTypes = transform.getOutputTypes();
// 建立輸出流轉換的文件相關路徑
File outRootFolder =
FileUtils.join(
buildDir,
StringHelper.toStrings(
AndroidProject.FD_INTERMEDIATES,
FD_TRANSFORMS,
transform.getName(),
scope.getDirectorySegments()));
// 輸出流的建立
IntermediateStream outputStream =
IntermediateStream.builder(
project,
transform.getName() + "-" + scope.getFullVariantName(),
taskName)
.addContentTypes(outputTypes)
.addScopes(requestedScopes)
.setRootLocation(outRootFolder)
.build();
streams.add(outputStream);
return outputStream;
}
複製代碼
如何觸發到咱們實現的Transform#transform
方法, 就在TransformTask
對應的TaskAction中執行
void transform(final IncrementalTaskInputs incrementalTaskInputs) throws IOException, TransformException, InterruptedException {
final ReferenceHolder<List<TransformInput>> consumedInputs = ReferenceHolder.empty();
final ReferenceHolder<List<TransformInput>> referencedInputs = ReferenceHolder.empty();
final ReferenceHolder<Boolean> isIncremental = ReferenceHolder.empty();
final ReferenceHolder<Collection<SecondaryInput>> changedSecondaryInputs =
ReferenceHolder.empty();
isIncremental.setValue(transform.isIncremental() && incrementalTaskInputs.isIncremental());
GradleTransformExecution preExecutionInfo =
GradleTransformExecution.newBuilder()
.setType(AnalyticsUtil.getTransformType(transform.getClass()).getNumber())
.setIsIncremental(isIncremental.getValue())
.build();
// 一些增量模式下的處理, 包括在增量模式下, 判斷輸入流(引用型和消費型)的變化
...
GradleTransformExecution executionInfo =
preExecutionInfo.toBuilder().setIsIncremental(isIncremental.getValue()).build();
...
transform.transform(
new TransformInvocationBuilder(TransformTask.this)
.addInputs(consumedInputs.getValue())
.addReferencedInputs(referencedInputs.getValue())
.addSecondaryInputs(changedSecondaryInputs.getValue())
.addOutputProvider(
outputStream != null
? outputStream.asOutput(
isIncremental.getValue())
: null)
.setIncrementalMode(isIncremental.getValue())
.build());
if (outputStream != null) {
outputStream.save();
}
}
複製代碼
經過上文的介紹, 咱們如今應該知道了自定義的Transform執行的時序, 位置, 以及相關原理. 那麼, 咱們如今已經拿到了編譯後的全部字節碼, 咱們要怎麼去處理呢? 咱們能夠了解下ASM
想要處理字節碼, 常見的框架有AspectJ, Javasist, ASM. 關於框架的選型網上相關的文章仍是比較多的, 從處理速度以及內存佔用率上, ASM明顯優於其餘兩個框架.本篇主要着眼於ASM的使用.
ASM
是一個通用的Java字節碼操做和分析框架。它能夠用於修改現有類或直接以二進制形式動態生成類. ASM
提供了一些常見的字節碼轉換和分析算法,能夠從中構建自定義複雜轉換和代碼分析工具. ASM庫提供了兩個用於生成和轉換編譯類的API:Core API
提供基於事件的類表示,而Tree API
提供基於對象的表示。因爲基於事件的API(Core API)不須要在內存中存儲一個表示該類的對象數, 因此從執行速度和內存佔用上來講, 它比基於對象的API(Tree API)更優.而後從使用場景上來講, 基於事件的API使用會比基於對象的API使用更爲困難, 譬如當咱們須要針對某個對象進行調整的時候.因爲一個類只能被一種API管理, 因此咱們應該要區分場景選取使用對應的API
ASM的使用須要必定的學習成本, 咱們能夠經過使用ASM Bytecode Outline
插件輔助瞭解, 對應插件在AS中的插件瀏覽器就能夠找到
@RouteModule
public class ASMTest {
}
複製代碼
Transform API
在應用工程方面的摸索使用Transform API
在組件化工程中有不少應用方向, 目前咱們項目中在自開發的路由框架中, 經過其去作了模塊的自動化靜態註冊, 同時考慮到路由經過協議文檔維護的不肯定性(頁面路由地址的維護不及時致使對應開發沒法及時更新對應代碼), 咱們作了路由的常量管理, 首先經過掃描整個工程項目代碼收集路由信息, 創建符合必定規則的路由原始基礎信息文件, 經過variant#registerJavaGeneratingTask
註冊 經過對應原始信息文件生成對應常量Java文件下沉在基礎通用組件中的task, 這樣上層依賴於這個基礎組件的項目均可以經過直接調用常量來使用路由.在各組件代碼隔離的狀況下, 能夠經過由組件aar傳遞原始信息文件, 仍然走上面的步驟生成對應的常量表, 而存在的類重複的問題, 經過自定義Transform
處理合並
在應用工程中, 咱們一般有關於網絡監控,應用性能檢測(包括頁面加載時間, 甚至包括各個方法調用所耗時間, 可能存在超過閾值須要警告)的需求, 這些需求咱們都不可能嵌入在業務代碼中, 都是能夠基於Transform API
進行處理. 而針對於埋點, 咱們也能夠經過Transform
實現自動化埋點的功能, 經過ASM Core
和ASM Tree
將盡量多的字段信息造成記錄傳遞, 這裏有些咱們項目中已經實現了, 有一些則是咱們須要去優化或者去實現的.
關於結合Transform+ASM
的使用, 我寫了個一個小Demo, 包括瞭如何處理支持增量功能時的轉換, 如何使用ASM Core Api
和ASM Tree Api
, 作了必定的封裝, 能夠參閱