Apk編譯流程
Apk編譯流程主要經(jīng)過(guò)以下幾步:
1、使用javac將java文件編譯成class
2、使用dex工具將class打包成dex
3、使用apkbuilder工具將dex、資源文件打包成apk
4、使用jarsigner工具對(duì)apk簽名
其實(shí)在編譯過(guò)程中,google工程師留給了我們很多api用來(lái)添加自己的操作。如APT在編譯時(shí)可以對(duì)代碼進(jìn)行處理,Transform在將class打包成dex中途,可以對(duì)class文件做自己的處理。

操作流程
一、創(chuàng)建工程、基礎(chǔ)配置
1、新建Java Library工程

2、將monitor中build.gradle的plugins改成groovy
plugins {
id 'java-library'
}
//------------------改成----------------------
plugins {
id 'groovy'
}
3、刪除java目錄,并在main中新建groovy目錄。

4、在monitor的build.gradle中添加依賴
plugins {
id 'groovy'
id 'maven-publish'
}
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation "com.android.tools.build:gradle:3.1.3"
implementation "org.javassist:javassist:3.20.0-GA"
}
java {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
如果報(bào)錯(cuò),Build was configured to prefer settings repositories over project repositories but repository 'Gradle Libs' was added by unknown code;可以進(jìn)入settings.gradle,將repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)注釋掉。
二、插件開(kāi)發(fā)
1、新建groovy類
新建MonitorPlugin,實(shí)現(xiàn)Plugin接口,泛型為Project。
package com.niiiico.monitor
import org.gradle.api.Plugin
import org.gradle.api.Project;
public class MonitorPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
println "hello plugin"
}
}
2、設(shè)置properties
在main目錄下新建文件夾resources/META-INF/gradle-plugins,新建文件com.niiiico.monitor.properties(包名.properties)。

文件內(nèi)容為:implementation-class=插件全路徑,以此來(lái)表示插件的入口。
implementation-class=com.niiiico.monitor.MonitorPlugin
3、打包插件,并發(fā)布到本地倉(cāng)庫(kù)
3.1、在monitor的build.gradle添加publishing代碼:
plugins {
id 'groovy'
id 'maven-publish'
}
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation "com.android.tools.build:gradle:3.1.3"
implementation "org.javassist:javassist:3.20.0-GA"
}
java {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
// 將插件打包發(fā)布到本地
publishing {
publications {
// Creates a Maven publication called "monitor".
monitor(MavenPublication) {
// 表示是一個(gè)java插件,最終會(huì)打包成jar包
from components.java
groupId = 'com.niiiico.monitor'
artifactId = 'monitor'
version = '1.0'
}
}
repositories {
maven {
// 發(fā)布地址
url('../monitor-jar')
}
}
}
3.2、點(diǎn)擊右上角的Sync now,在右上角的gradle->Tasks便能找到publish任務(wù)。

3.3、雙擊publish,可以在工程目錄看到多了一個(gè)monitor-jar目錄。打好的插件包便在這個(gè)目錄下。

4、依賴插件
4.1、在項(xiàng)目的build.gradle中添加maven本地路徑,并在dependencies添加插件依賴。classpath "groupId:artifactId:version"

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
maven {
url('monitor-jar')
}
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.3"
classpath "com.niiiico.monitor:monitor:1.0"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
4.2、在app的build.gradle中添加插件依賴:apply plugin: 'com.niiiico.monitor';內(nèi)容即我們?cè)?strong>resources/META-INF/gradle-plugins下創(chuàng)建的文件名稱。

4.3、點(diǎn)擊sync,即可在build中看到如下打印,表示插件引入成功。

三、繼承Transform
1、新建MonitorTransform繼承自Transform
package com.niiiico.monitor
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.gradle.internal.pipeline.TransformManager;
public class MonitorTransform extends Transform {
def project
MonitorTransform(Project project) {
this.project = project
}
// 在app/build/intermediates/transforms/路徑下生成新的文件夾
// 用來(lái)存儲(chǔ)本次transform操作的數(shù)據(jù)
@Override
String getName() {
return "monitor"
}
// 接收什么類型的數(shù)據(jù)
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
// 接收數(shù)據(jù)的范圍
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
// 一般不修改
@Override
boolean isIncremental() {
return false
}
}
2、重寫輸入輸出
2.1、在MonitorPlugin的apply方法中使用project.android.registerTransform(new MonitorTransform(project))注冊(cè)自定義的Transform。
package com.niiiico.monitor
import org.gradle.api.Plugin
import org.gradle.api.Project;
public class MonitorPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.android.registerTransform(new MonitorTransform(project))
}
}
2.2、重新使用publish發(fā)布插件,然后運(yùn)行app,發(fā)現(xiàn)apk無(wú)法運(yùn)行。因?yàn)樽?cè)Transform后,系統(tǒng)會(huì)把我們的Transform插入編譯打包流程,上一個(gè)節(jié)點(diǎn)會(huì)將編譯好的class和jar等信息告訴我們,如果我們不進(jìn)行任何處理,下一個(gè)節(jié)點(diǎn)便無(wú)法拿到這些信息,因此需要重寫輸入輸出,將從上一個(gè)節(jié)點(diǎn)拿到的數(shù)據(jù)告訴下一個(gè)節(jié)點(diǎn)。

2.3、要將數(shù)據(jù)告訴下個(gè)節(jié)點(diǎn),需要以下幾步:
(1)遍歷inputs目錄,查詢輸入的文件
(2)查詢輸出文件路徑
(3)將輸入文件復(fù)制到下一個(gè)節(jié)點(diǎn)
重寫transform函數(shù)
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOExcepti
super.transform(transformInvocation)
println "--------------------transform-------------------"
// 1、查詢輸入,遍歷inputs目錄
transformInvocation.inputs.each {
// 1.1 jar包目錄
it.jarInputs.each {
// 2.查詢輸出
def dest = transformInvocation.outputProvider.getContentLocation(
it.name,
it.contentTypes,
it.scopes,
Format.JAR)
println "jar dest----->" + dest
// 3.復(fù)制到下一環(huán)節(jié)
FileUtils.copyFile(it.file, dest);
}
// 1.2 class目錄
it.directoryInputs.each {
// 2.查詢輸出
def dest = transformInvocation.outputProvider.getContentLocation(
it.name,
it.contentTypes,
it.scopes,
Format.DIRECTORY)
println "class dest----->" + dest
// 3.復(fù)制到下一環(huán)節(jié)
FileUtils.copyDirectory(it.file, dest);
}
}
}
2.4、重新發(fā)布,點(diǎn)擊安裝,即可安裝成功。此時(shí),在build\intermediates\transforms\下可以發(fā)現(xiàn),新增了monitor目錄,這邊是getName函數(shù)定義的名字。

四、Javassist修改class文件
1、通過(guò)ClassPool加載class文件
// 緩存class字節(jié)碼對(duì)象的容器
def pool = ClassPool.getDefault()
def preFileName = it.file.absolutePath
// 加載路徑下的class文件
pool.insertClassPath(preFileName)
// project.android.bootClasspath 加入android.jar,不然找不到android相關(guān)的所有類
pool.appendClassPath(project.android.bootClasspath[0].toString());
// 引入android.os.Bundle包,因?yàn)閛nCreate方法參數(shù)有Bundle
pool.importPackage("android.os.Bundle");
2、找到class文件
遍歷系統(tǒng)傳過(guò)來(lái)的class文件目錄,找到class文件
// 找到需要處理的文件并處理
// fileName D:\workplace_github\JavassistDemo\app\build\intermediates\javac\debug\classes
private void findTargetAndSettle(File dir, String fileName) {
if (dir.isDirectory()) {
// 如果是目錄,繼續(xù)遍歷
dir.listFiles().each {
findTargetAndSettle(it, fileName)
}
} else {
def filePath = dir.absolutePath
// 只處理class文件
if (filePath.endsWith(".class")) {
println "find class----->" + filePath
// 修改文件
modify(filePath, fileName)
}
}
}
3、過(guò)濾class
過(guò)濾系統(tǒng)生成的class,然后截取class全類名,通過(guò)ClassPool查找到CtClass 對(duì)象。
// 過(guò)濾class
private void filterClass(def filePath, String fileName) {
// 過(guò)濾系統(tǒng)文件
if (filePath.contains('R$')
|| filePath.contains('R.class')
|| filePath.contains("BuildConfig.class")) {
return
}
// 獲取className
def className = filePath.replace(fileName, "")
.replace("\\", ".")
.replace("/", ".")
.replace(".class", "")
.substring(1)
println "find className----->" + className
// 獲取CtClass對(duì)象,用來(lái)操作class
CtClass ctClass = pool.get(className)
addCode(ctClass, fileName)
}
4、修改代碼并寫入文件
// 添加代碼
private void addCode(CtClass ctClass, String fileName) {
// 解凍
ctClass.defrost()
CtMethod[] methods = ctClass.getDeclaredMethods()
for (method in methods) {
println "method " + method.getName() + "參數(shù)個(gè)數(shù) " + method.getParameterTypes().length
if ("onCreate".equals(method.getName())) {
method.insertBefore("{ System.out.println(\"調(diào)用了" + method.getName() + "\");}")
}
}
// 將修改的文件寫出去
ctClass.writeFile(fileName)
ctClass.detach()
}
5、驗(yàn)證結(jié)果
點(diǎn)擊publish重新打包插件,重新打包并運(yùn)行apk。

查看build\intermediates\transforms\monitor\目錄下的MainActivity.class文件,發(fā)現(xiàn)代碼已經(jīng)被修改。

6、MonitorTransform全部代碼
package com.niiiico.monitor
import com.android.build.api.transform.Format
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.gradle.api.Project;
public class MonitorTransform extends Transform {
def project
// 緩存class字節(jié)碼對(duì)象的容器
def pool = ClassPool.getDefault()
MonitorTransform(Project project) {
this.project = project
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println "--------------------transform-------------------"
// 1、查詢輸入,遍歷inputs目錄
transformInvocation.inputs.each {
// 1.1 jar包目錄
it.jarInputs.each {
// 2.查詢輸出
def destDir = transformInvocation.outputProvider.getContentLocation(
it.name,
it.contentTypes,
it.scopes,
Format.JAR)
println "jar destDir----->" + destDir
// 3.復(fù)制到下一環(huán)節(jié)
FileUtils.copyFile(it.file, destDir);
}
// 1.2 class目錄
it.directoryInputs.each {
def preFileName = it.file.absolutePath
// 加載路徑下的class文件
pool.insertClassPath(preFileName)
// project.android.bootClasspath 加入android.jar,不然找不到android相關(guān)的所有類
pool.appendClassPath(project.android.bootClasspath[0].toString());
// 引入android.os.Bundle包,因?yàn)閛nCreate方法參數(shù)有Bundle
pool.importPackage("android.os.Bundle");
println "========directoryInputs======== " + preFileName
findTargetAndSettle(it.file, preFileName)
// 2.查詢輸出
def destDir = transformInvocation.outputProvider.getContentLocation(
it.name,
it.contentTypes,
it.scopes,
Format.DIRECTORY)
println "class destDir----->" + destDir
// 3.復(fù)制到下一環(huán)節(jié)
FileUtils.copyDirectory(it.file, destDir);
}
}
}
// 找到需要處理的文件并處理
// fileName D:\workplace_github\JavassistDemo\app\build\intermediates\javac\debug\classes
private void findTargetAndSettle(File dir, String fileName) {
if (dir.isDirectory()) {
// 如果是目錄,繼續(xù)遍歷
dir.listFiles().each {
findTargetAndSettle(it, fileName)
}
} else {
def filePath = dir.absolutePath
// 只處理class文件
if (filePath.endsWith(".class")) {
println "find class----->" + filePath
// 修改文件
filterClass(filePath, fileName)
}
}
}
// 過(guò)濾class
private void filterClass(def filePath, String fileName) {
// 過(guò)濾系統(tǒng)文件
if (filePath.contains('R$')
|| filePath.contains('R.class')
|| filePath.contains("BuildConfig.class")) {
return
}
// 獲取className
def className = filePath.replace(fileName, "")
.replace("\\", ".")
.replace("/", ".")
.replace(".class", "")
.substring(1)
println "find className----->" + className
// 獲取CtClass對(duì)象,用來(lái)操作class
CtClass ctClass = pool.get(className)
addCode(ctClass, fileName)
}
// 添加代碼
private void addCode(CtClass ctClass, String fileName) {
// 解凍
ctClass.defrost()
CtMethod[] methods = ctClass.getDeclaredMethods()
for (method in methods) {
println "method " + method.getName() + "參數(shù)個(gè)數(shù) " + method.getParameterTypes().length
if ("onCreate".equals(method.getName())) {
method.insertBefore("{ System.out.println(\"調(diào)用了" + method.getName() + "\");}")
}
}
// 將修改的文件寫出去
ctClass.writeFile(fileName)
ctClass.detach()
}
// 在app/build/intermediates/transforms/路徑下生成新的文件夾
// 用來(lái)存儲(chǔ)本次transform操作的數(shù)據(jù)
@Override
String getName() {
return "monitor"
}
// 接收什么類型的數(shù)據(jù)
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
// 接收數(shù)據(jù)的范圍
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
// 一般不修改
@Override
boolean isIncremental() {
return false
}
}