概述
該部分基礎(chǔ)知識在Gradle學(xué)習(xí)-----Gradle自定義插件及Replugin源碼解析之replugin-host-gradle涉及欣鳖,不再重復(fù)累述
源碼分析
ReClassTransform
入口相關(guān)類源碼如下
public class ReClassTransform extends Transform {
private Project project
private def globalScope
/* 需要處理的 jar 包 */
def includeJars = [] as Set
def map = [:]
public ReClassTransform(Project p) {
this.project = p
AppPlugin appPlugin = project.plugins.getPlugin(AppPlugin)
// taskManager 在 2.1.3 中為 protected 訪問類型的,在之后的版本為 private 訪問類型的,
// 使用反射訪問
def taskManager = BasePlugin.metaClass.getProperty(appPlugin, "taskManager")
this.globalScope = taskManager.globalScope;
}
@Override
String getName() {
return '___ReClass___'
}
@Override
void transform(Context context,
Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider,
boolean isIncremental) throws IOException, TransformException, InterruptedException {
welcome()
/* 讀取用戶配置 */
ReClassConfig config = project.extensions.getByName('repluginPluginConfig')
File rootLocation = null
try {
rootLocation = outputProvider.rootLocation
println ">>> rootLocation: throw----------->rootLocation=" + (rootLocation == null ? "null" :rootLocation)
} catch (Throwable e) {
//android gradle plugin 3.0.0+ 修改了私有變量桑嘶,將其移動到了IntermediateFolderUtils中去
rootLocation = outputProvider.folderUtils.getRootFolder()
println ">>> rootLocation: throw----------->"
}
if (rootLocation == null) {
throw new GradleException("can't get transform root location")
}
println ">>> rootLocation: ${rootLocation}"
// Compatible with path separators for window and Linux, and fit split param based on 'Pattern.quote'
def variantDir = rootLocation.absolutePath.split(getName() + Pattern.quote(File.separator))[1]
println ">>> variantDir: ${variantDir}"
CommonData.appModule = config.appModule
CommonData.ignoredActivities = config.ignoredActivities
def injectors = includedInjectors(config, variantDir)
if (injectors.isEmpty()) {
copyResult(inputs, outputProvider) // 跳過 reclass
} else {
doTransform(inputs, outputProvider, config, injectors) // 執(zhí)行 reclass
}
}
...//省略
}
查看入口transform
方法
2.1 project.extensions.getByName('repluginPluginConfig')
讀取用戶在replugin插件項目的build.gradle中配置的參數(shù)拧咳,在下面的代碼中會用到干厚,比如獲取需要忽略的注入器ignoredInjectors
礁蔗、需要忽略替換的ActivityignoredActivities
壤短、自定義的代碼注入器customInjectors
等设拟。
2.2 includedInjectors
返回用戶未忽略的注入器,代碼如下
/**
* 返回用戶未忽略的注入器的集合
*/
def includedInjectors(def cfg, String variantDir) {//product\debug
def injectors = []
Injectors.values().each {
//設(shè)置project
it.injector.setProject(project)
//設(shè)置variant關(guān)鍵dir
it.injector.setVariantDir(variantDir)
if (!(it.nickName in cfg.ignoredInjectors)) {
injectors << it.nickName
}
}
injectors
}
Injectors
為枚舉,其中定義了5種IClassInjector
注入器的封裝久脯,被封裝的分別對應(yīng)操作對應(yīng)組件及resource,源碼如下
public enum Injectors {
LOADER_ACTIVITY_CHECK_INJECTOR('LoaderActivityInjector', new LoaderActivityInjector(), '替換 Activity 為 LoaderActivity'),//替換插件中的Activity的繼承相關(guān)代碼 為 replugin-plugin-library 中的XXPluginActivity父類
LOCAL_BROADCAST_INJECTOR('LocalBroadcastInjector', new LocalBroadcastInjector(), '替換 LocalBroadcast 調(diào)用'),//替換插件中的LocalBroadcastManager調(diào)用代碼 為 插件庫的調(diào)用代碼纳胧。
PROVIDER_INJECTOR('ProviderInjector', new ProviderInjector(), '替換 Provider 調(diào)用'),//替換 插件中的 ContentResolver 調(diào)用代碼 為 插件庫的調(diào)用代碼
PROVIDER_INJECTOR2('ProviderInjector2', new ProviderInjector2(), '替換 ContentProviderClient 調(diào)用'),//替換 插件中的 ContentProviderClient 調(diào)用代碼 為 插件庫的調(diào)用代碼
GET_IDENTIFIER_INJECTOR('GetIdentifierInjector', new GetIdentifierInjector(), '替換 Resource.getIdentifier 調(diào)用') //替換 插件中的 Resource.getIdentifier 調(diào)用代碼的參數(shù) 為 動態(tài)適配的參數(shù)
IClassInjector injector
String nickName
String desc
Injectors(String nickName, IClassInjector injector, String desc) {
this.injector = injector
this.nickName = nickName
this.desc = desc;
}
}
2.3 getInputTypes()
指明當前Trasfrom要處理的數(shù)據(jù)類型,可選類型包括CONTENT_CLASS
(代表要處理的數(shù)據(jù)是編譯過的Java代碼,而這些數(shù)據(jù)的容器可以是jar包也可以是文件夾)帘撰,CONTENT_JARS
(包括編譯過的Java代碼和標準的Java資源)跑慕,CONTENT_RESOURCES,CONTENT_NATIVE_LIBS
等骡和。在replugin-plugin-gradle中是使用Transform來做代碼插樁,所以選用CONTENT_CLASS
類型相赁。
2.4doTransform
方法是執(zhí)行reclass的關(guān)鍵,代碼如下
* 執(zhí)行 Transform
*/
def doTransform(Collection<TransformInput> inputs,
TransformOutputProvider outputProvider,
Object config,
def injectors) {
/* 初始化 ClassPool */
Object pool = initClassPool(inputs)
/* 進行注入操作 */
Util.newSection()
Injectors.values().each {
if (it.nickName in injectors) {
println ">>> Do: ${it.nickName}"
// 將 NickName 的第 0 個字符轉(zhuǎn)換成小寫,用作對應(yīng)配置的名稱
def configPre = Util.lowerCaseAtIndex(it.nickName, 0)
doInject(inputs, pool, it.injector, config.properties["${configPre}Config"])
} else {
println ">>> Skip: ${it.nickName}"
}
}
if (config.customInjectors != null) {
config.customInjectors.each {
doInject(inputs, pool, it)
}
}
/* 重打包 */
repackage()
/* 拷貝 class 和 jar 包 */
copyResult(inputs, outputProvider)
Util.newSection()
}
2.4.1 initClassPool
添加編譯時需要引用的到類到 ClassPool慰于。解壓對應(yīng)的jar的路徑及名字钮科,將解壓后再重新打包的jar的路徑及名字作為值,jar的名字作為key存放在map
中婆赠。includeJars
存放的是 android.jar的目錄绵脯、其它各種引用到的jar進行解壓后目錄佳励、及class所在目錄。
/**
* 初始化 ClassPool
*/
def initClassPool(Collection<TransformInput> inputs) {
Util.newSection()
def pool = new ClassPool(true)
// 添加編譯時需要引用的到類到 ClassPool, 同時記錄要修改的 jar 到 includeJars
Util.getClassPaths(project, globalScope, inputs, includeJars, map).each {
println " $it"
pool.insertClassPath(it)
}
pool
}
Util.newSection()
為畫一條直線長度為50個--蛆挫,代碼如下
def static newSection() {
50.times {
print '--'
}
println()
}
2.4.2
CtMethod:是一個class文件中的方法的抽象表示赃承。一個CtMethod對象表示一個方法。(Javassit 庫API)
CtClass:是一個class文件的抽象表示悴侵。一個CtClass(compile-time class)對象可以用來處理一個class文件瞧剖。(Javassit 庫API)
ClassPool:是一個CtClass對象的容器類。(Javassit 庫API)
.class文件:.class文件是一種存儲Java字節(jié)碼的二進制文件可免,里面包含一個Java類或者接口抓于。
遍歷Injectors
枚舉中值,與用戶未忽略的注入器injectors
對比浇借,如果未被忽略則執(zhí)行doInject
否則打印跳過信息
2.4.3 doInject
即執(zhí)行注入操作捉撮,分別對目錄下的每個class文件執(zhí)行handleDir
及對每個jar文件執(zhí)行handleJar
源碼如下
/**
* 執(zhí)行注入操作
*/
def doInject(Collection<TransformInput> inputs, ClassPool pool,
IClassInjector injector, Object config) {
try {
inputs.each { TransformInput input ->
input.directoryInputs.each {
handleDir(pool, it, injector, config)
}
input.jarInputs.each {
handleJar(pool, it, injector, config)
}
}
} catch (Throwable t) {
println t.toString()
}
}
2.4.4
handleDir
即調(diào)用各個注入器的injectClass方法,handleJar
為通過jar名稱獲取map
中存放的對應(yīng)解壓目錄妇垢,然后調(diào)用各個注入器的injectClass方法
/**
* 處理目錄中的 class 文件
*/
def handleDir(ClassPool pool, DirectoryInput input, IClassInjector injector, Object config) {
println ">>> Handle Dir: ${input.file.absolutePath}"
injector.injectClass(pool, input.file.absolutePath, config)
}
/**
* 處理 jar
*/
def handleJar(ClassPool pool, JarInput input, IClassInjector injector, Object config) {
File jar = input.file
if (jar.absolutePath in includeJars) {
println ">>> Handle Jar: ${jar.absolutePath}"
String dirAfterUnzip = map.get(jar.getParent() + File.separatorChar + jar.getName()).replace('.jar', '')
injector.injectClass(pool, dirAfterUnzip, config)
}
}
2.4.5 各種注入器的injectClass
方法巾遭,針對class目錄,遍歷下面class文件進行處理闯估,以LoaderActivityInjector
作為例子分析灼舍,源碼如下
@Override
def injectClass(ClassPool pool, String dir, Map config) {
init()
/* 遍歷程序中聲明的所有 Activity */
//每次都new一下,否則多個variant一起構(gòu)建時只會獲取到首個manifest
new ManifestAPI().getActivities(project, variantDir).each {
// 處理沒有被忽略的 Activity
if (!(it in CommonData.ignoredActivities)) {
handleActivity(pool, it, dir)
}
}
}
def private init() {
/* 延遲初始化 loaderActivityRules */
// todo 從配置中讀取睬愤,而不是寫死在代碼中
if (loaderActivityRules == null) {
def buildSrcPath = project.project(':buildsrc').projectDir.absolutePath
def loaderConfigPath = String.join(File.separator, buildSrcPath, 'res', LOADER_PROP_FILE)
loaderActivityRules = new Properties()
new File(loaderConfigPath).withInputStream {
loaderActivityRules.load(it)
}
println '\n>>> Activity Rules:'
loaderActivityRules.each {
println it
}
println()
}
}
2.4.6 handleActivity
代碼如下,主要作用就是更改Activity的最頂層父類片仿,對應(yīng)關(guān)系為loaderActivityRules
數(shù)組。
private def handleActivity(ClassPool pool, String activity, String classesDir) {
def clsFilePath = classesDir + File.separatorChar + activity.replaceAll('\\.', '/') + '.class'
if (!new File(clsFilePath).exists()) {
return
}
println ">>> Handle $activity &clsFilePath=-----------> $clsFilePath"
def stream, ctCls
try {
stream = new FileInputStream(clsFilePath)
ctCls = pool.makeClass(stream);
/*
// 打印當前 Activity 的所有父類
CtClass tmpSuper = ctCls.superclass
while (tmpSuper != null) {
println(tmpSuper.name)
tmpSuper = tmpSuper.superclass
}
*/
// ctCls 之前的父類
def originSuperCls = ctCls.superclass
/* 從當前 Activity 往上回溯尤辱,直到找到需要替換的 Activity */
def superCls = originSuperCls
while (superCls != null && !(superCls.name in loaderActivityRules.keySet())) {
// println ">>> 向上查找 $superCls.name"
ctCls = superCls
superCls = ctCls.superclass
}
// 如果 ctCls 已經(jīng)是 LoaderActivity砂豌,則不修改
if (ctCls.name in loaderActivityRules.values()) {
// println " 跳過 ${ctCls.getName()}"
return
}
/* 找到需要替換的 Activity, 修改 Activity 的父類為 LoaderActivity */
if (superCls != null) {
def targetSuperClsName = loaderActivityRules.get(superCls.name)
// println " ${ctCls.getName()} 的父類 $superCls.name 需要替換為 ${targetSuperClsName}"
CtClass targetSuperCls = pool.get(targetSuperClsName)
if (ctCls.isFrozen()) {
ctCls.defrost()
}
ctCls.setSuperclass(targetSuperCls)
// 修改聲明的父類后,還需要方法中所有的 super 調(diào)用光督。
ctCls.getDeclaredMethods().each { outerMethod ->
outerMethod.instrument(new ExprEditor() {
@Override
void edit(MethodCall call) throws CannotCompileException {
if (call.isSuper()) {
if (call.getMethod().getReturnType().getName() == 'void') {
call.replace('{super.' + call.getMethodName() + '($$);}')
} else {
call.replace('{$_ = super.' + call.getMethodName() + '($$);}')
}
}
}
})
}
ctCls.writeFile(CommonData.getClassPath(ctCls.name))
println " Replace ${ctCls.name}'s SuperClass ${superCls.name} to ${targetSuperCls.name}"
}
} catch (Throwable t) {
println " [Warning] --> ${t.toString()}"
} finally {
if (ctCls != null) {
ctCls.detach()
}
if (stream != null) {
stream.close()
}
}
}
2.4.7將解壓修改后的jar重新打包阳距,然后刪除解壓目錄,只留jar结借。源碼如下
/**
* 將解壓的 class 文件重新打包筐摘,然后刪除 class 文件
*/
def repackage() {
Util.newSection()
println '>>> Repackage...'
includeJars.each {
File jar = new File(it)
String JarAfterzip = map.get(jar.getParent() + File.separatorChar + jar.getName())
String dirAfterUnzip = JarAfterzip.replace('.jar', '')
// println ">>> 壓縮目錄 $dirAfterUnzip"
Util.zipDir(dirAfterUnzip, JarAfterzip)
// println ">>> 刪除目錄 $dirAfterUnzip"
FileUtils.deleteDirectory(new File(dirAfterUnzip))
}
}
2.4.8最后將處理好的class和jar賦值到輸出目錄,jar改名避免重名船老,交給下個task處理咖熟,至此Transform結(jié)束。
/**
* 拷貝處理結(jié)果
*/
def copyResult(def inputs, def outputs) {
// Util.newSection()
inputs.each { TransformInput input ->
input.directoryInputs.each { DirectoryInput dirInput ->
copyDir(outputs, dirInput)
}
input.jarInputs.each { JarInput jarInput ->
copyJar(outputs, jarInput)
}
}
}
/**
* 拷貝目錄
*/
def copyDir(TransformOutputProvider output, DirectoryInput input) {
File dest = output.getContentLocation(input.name, input.contentTypes, input.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(input.file, dest)
// println ">>> 拷貝目錄 ${input.file.absolutePath} 到 ${dest.absolutePath}"
}
/**
* 拷貝 Jar
*/
def copyJar(TransformOutputProvider output, JarInput input) {
File jar = input.file
String jarPath = map.get(jar.absolutePath);
if (jarPath != null) {
jar = new File(jarPath)
}
String destName = input.name
def hexName = DigestUtils.md5Hex(jar.absolutePath)
if (destName.endsWith('.jar')) {
destName = destName.substring(0, destName.length() - 4)
}
File dest = output.getContentLocation(destName + '_' + hexName, input.contentTypes, input.scopes, Format.JAR)
FileUtils.copyFile(jar, dest)
/*
def path = jar.absolutePath
if (path in CommonData.includeJars) {
println ">>> 拷貝Jar ${path} 到 ${dest.absolutePath}"
}
*/
}