Javassist實(shí)現(xiàn)無(wú)侵入埋點(diǎn)

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文件做自己的處理。

Apk編譯流程

操作流程

一、創(chuàng)建工程、基礎(chǔ)配置

1、新建Java Library工程


新建工程

2、將monitor中build.gradle的plugins改成groovy

plugins {
    id 'java-library'
}

//------------------改成----------------------

plugins {
    id 'groovy'
}

3、刪除java目錄,并在main中新建groovy目錄。


新建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)。

com.niiiico.monitor.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ù)。


publish任務(wù)

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


monitor-jar目錄

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)。


Transform

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ù)定義的名字。


monitor目錄

四、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。

控制臺(tái)日志

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


MainActivity.class

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
    }
}

五、git工程地址

https://github.com/Timey729/JavassistDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容