Apk编译流程

Apk编译流程主要经过以下几步:
1、使用javac将java文件编译成class
2、使用dex工具将class打包成dex
3、使用apkbuilder工具将dex、资源文件打包成apk
4、使用jarsigner工具对apk签名

其实在编译过程中,google工程师留给了我们很多api用来添加自己的操作。如APT在编译时可以对代码进行处理,Transform在将class打包成dex中途,可以对class文件做自己的处理。

Sse java 官方demo javassist android_android

操作流程

一、创建工程、基础配置

1、新建Java Library工程

Sse java 官方demo javassist android_Sse java 官方demo_02

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

plugins {
    id 'java-library'
}

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

plugins {
    id 'groovy'
}

3、删除java目录,并在main中新建groovy目录。

Sse java 官方demo javassist android_gradle_03

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
}

如果报错,Build was configured to prefer settings repositories over project repositories but repository ‘Gradle Libs’ was added by unknown code;可以进入settings.gradle,将repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)注释掉。

二、插件开发

1、新建groovy类

新建MonitorPlugin,实现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、设置properties

在main目录下新建文件夹resources/META-INF/gradle-plugins,新建文件com.niiiico.monitor.properties(包名.properties)。

Sse java 官方demo javassist android_Sse java 官方demo_04

文件内容为:implementation-class=插件全路径,以此来表示插件的入口。

implementation-class=com.niiiico.monitor.MonitorPlugin

3、打包插件,并发布到本地仓库

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
}

// 将插件打包发布到本地
publishing {
    publications {
        // Creates a Maven publication called "monitor".
        monitor(MavenPublication) {
            // 表示是一个java插件,最终会打包成jar包
            from components.java

            groupId = 'com.niiiico.monitor'
            artifactId = 'monitor'
            version = '1.0'
        }
    }

    repositories {
        maven {
            // 发布地址
            url('../monitor-jar')
        }
    }
}

3.2、点击右上角的Sync now,在右上角的gradle->Tasks便能找到publish任务。

Sse java 官方demo javassist android_gradle_05

3.3、双击publish,可以在工程目录看到多了一个monitor-jar目录。打好的插件包便在这个目录下。

Sse java 官方demo javassist android_Sse java 官方demo_06

4、依赖插件

4.1、在项目的build.gradle中添加maven本地路径,并在dependencies添加插件依赖。classpath "groupId:artifactId:version"

Sse java 官方demo javassist android_gradle_07

// 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’;内容即我们在resources/META-INF/gradle-plugins下创建的文件名称。

Sse java 官方demo javassist android_java_08

4.3、点击sync,即可在build中看到如下打印,表示插件引入成功。

Sse java 官方demo javassist android_android studio_09

三、继承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/路径下生成新的文件夹
    // 用来存储本次transform操作的数据
    @Override
    String getName() {
        return "monitor"
    }

    // 接收什么类型的数据
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    // 接收数据的范围
    @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))注册自定义的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发布插件,然后运行app,发现apk无法运行。因为注册Transform后,系统会把我们的Transform插入编译打包流程,上一个节点会将编译好的class和jar等信息告诉我们,如果我们不进行任何处理,下一个节点便无法拿到这些信息,因此需要重写输入输出,将从上一个节点拿到的数据告诉下一个节点。

Sse java 官方demo javassist android_android studio_10

2.3、要将数据告诉下个节点,需要以下几步:
(1)遍历inputs目录,查询输入的文件
(2)查询输出文件路径
(3)将输入文件复制到下一个节点

重写transform函数

@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.复制到下一环节
            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.复制到下一环节
            FileUtils.copyDirectory(it.file, dest);
        }
    }
}

2.4、重新发布,点击安装,即可安装成功。此时,在build\intermediates\transforms\下可以发现,新增了monitor目录,这边是getName函数定义的名字。

Sse java 官方demo javassist android_android studio_11

四、Javassist修改class文件

1、通过ClassPool加载class文件

// 缓存class字节码对象的容器
def pool = ClassPool.getDefault()

def preFileName = it.file.absolutePath
// 加载路径下的class文件
pool.insertClassPath(preFileName)
// project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
pool.appendClassPath(project.android.bootClasspath[0].toString());
// 引入android.os.Bundle包,因为onCreate方法参数有Bundle
pool.importPackage("android.os.Bundle");

2、找到class文件

遍历系统传过来的class文件目录,找到class文件

// 找到需要处理的文件并处理
// fileName D:\workplace_github\JavassistDemo\app\build\intermediates\javac\debug\classes
private void findTargetAndSettle(File dir, String fileName) {
    if (dir.isDirectory()) {
        // 如果是目录,继续遍历
        dir.listFiles().each {
            findTargetAndSettle(it, fileName)
        }
    } else {
        def filePath = dir.absolutePath
        // 只处理class文件
        if (filePath.endsWith(".class")) {
            println "find class----->" + filePath
            // 修改文件
            modify(filePath, fileName)
        }
    }
}

3、过滤class

过滤系统生成的class,然后截取class全类名,通过ClassPool查找到CtClass 对象。

// 过滤class
private void filterClass(def filePath, String fileName) {
    // 过滤系统文件
    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对象,用来操作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() + "参数个数  " + method.getParameterTypes().length
        if ("onCreate".equals(method.getName())) {
            method.insertBefore("{ System.out.println(\"调用了" + method.getName() + "\");}")
        }
    }

    // 将修改的文件写出去
    ctClass.writeFile(fileName)
    ctClass.detach()
}

5、验证结果

点击publish重新打包插件,重新打包并运行apk。

Sse java 官方demo javassist android_Sse java 官方demo_12

查看build\intermediates\transforms\monitor\目录下的MainActivity.class文件,发现代码已经被修改。

Sse java 官方demo javassist android_java_13

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字节码对象的容器
    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.复制到下一环节
                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相关的所有类
                pool.appendClassPath(project.android.bootClasspath[0].toString());
                // 引入android.os.Bundle包,因为onCreate方法参数有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.复制到下一环节
                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()) {
            // 如果是目录,继续遍历
            dir.listFiles().each {
                findTargetAndSettle(it, fileName)
            }
        } else {
            def filePath = dir.absolutePath
            // 只处理class文件
            if (filePath.endsWith(".class")) {
                println "find class----->" + filePath
                // 修改文件
                filterClass(filePath, fileName)
            }
        }
    }

    // 过滤class
    private void filterClass(def filePath, String fileName) {
        // 过滤系统文件
        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对象,用来操作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() + "参数个数  " + method.getParameterTypes().length
            if ("onCreate".equals(method.getName())) {
                method.insertBefore("{ System.out.println(\"调用了" + method.getName() + "\");}")
            }
        }

        // 将修改的文件写出去
        ctClass.writeFile(fileName)
        ctClass.detach()
    }

    // 在app/build/intermediates/transforms/路径下生成新的文件夹
    // 用来存储本次transform操作的数据
    @Override
    String getName() {
        return "monitor"
    }

    // 接收什么类型的数据
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    // 接收数据的范围
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    // 一般不修改
    @Override
    boolean isIncremental() {
        return false
    }
}