Android AOP实现原理之字节码插桩(一)

参考

博客 Android AOP之字节码插桩

博客 Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)

由衷感谢以上博主分享的技术知识!

1.AOP的概念

AOP(面向切面编程)这个概念的提出主要是相对于OOP(面向对象编程)。OOP能够将项目划分为多个模块,但有些功能是各模块都需要的,例如性能监控、日志管理等,AOP便是一刀切入(切开并织入)多个模块,为这些模块提供功能,也为这些功能提供统一的管理。如下图:

《Android AOP实现原理之字节码插桩(一)》

2.Android中AOP的实现方式

Android中AOP的实现方式分两类:

  • 运行时切入

    • 集成Dexposed,Xposed框架(运行时hook某些关键方法)
    • Java API实现动态代理机制(基于反射,性能不佳)
  • 编译时切入

    • 集成AspactJ框架(特殊的插件或编译器来生成特殊的class文件)
    • 使用ASM,Javassit等字节码工具类来修改字节码(编译打包APK文件前修改class文件)

由于本篇想要讨论的是实现原理,因此不讨论如何使用第三方框架实现切入,仅讨论如何在APK文件生成前获取class文件并修改。这种方式局限性小,对程序运行性能几乎没影响。

3.Android编译流程

Google官方推荐使用Gradle构建Android项目,在Android Gradle构建流程中,会将源文件编译为class文件,再将class文件整合到dex文件中我们修改class文件的时机就在class文件编译完成后,dex文件整合之前,我们需要找到这样一个入口进行代码织入。打包流程如下图:

《Android AOP实现原理之字节码插桩(一)》

上图中dex步骤就是我们的入口,在Android Gradle Plugin 1.5.0 之前,我们需要hook dx.jar(将class文件整合到dex文件的过程)来获取织入入口。好在Android Gradle Plugin 1.5.0 以后,Google官方提供了Transform API用作字节码插桩的入口。因此本篇就不再赘述hook dx.jar方面的知识。

4.Gradle需知

Task

Gradle构建项目流程便是执行一个又一个task,包括官方提供的和第三方插件提供的,允许开发者灵活地构建项目。

Transfrom

Transfrom是Gradle 1.5.0 以后提供的一个API,是一个有固定运行时机的task,注册后便会运行在class文件整合到dex文件之前。

Input/output

每一个task都有input和output,input来自上一个task,output输出给下一个task。

Plugin

Plugin是插件,一个plugin中含有多个task,在build.gradle文件中这样依赖plugin:

apply plugin : 'package'

5.获取织入入口

新建plugin

  1. 新建一个library module,名字为BuildSrc,否则apply plugin时会提示找不到
  2. 删除module下除build.gradle外的所有文件
  3. 新建以下文件夹 src-main-groovy
  4. 修改build.gradle并同步:
apply plugin: 'groovy'

repositories { jcenter() }

dependencies { compile gradleApi() compile 'com.android.tools.build:gradle:1.5.0'//大于等于1.5.0就行 }
  1. 在groovy文件夹下新建包,之后的类都放下此包下。包名随意,如com.zyn.plugin
  2. 新建groovy类(新建file,并且以.groovy作为后缀),继承自org.gradle.api.Plugin:
package com.zyn.plugin

import org.gradle.api.Plugin;
import org.gradle.api.Project


public class MyPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        project.logger.error "========自定义Plugin========="
    }
}
  1. 在app module下的buiil.gradle中apply插件:
apply plugin: 'com.android.application'
apply plugin: com.zyn.plugin.MyPlugin
  1. 运行项目后可以在gradle console窗口看到:
Configuration on demand is an incubating feature.
:buildsrc:compileJava UP-TO-DATE
:buildsrc:compileGroovy
:buildsrc:processResources UP-TO-DATE
:buildsrc:classes
:buildsrc:jar
:buildsrc:assemble
:buildsrc:compileTestJava UP-TO-DATE
:buildsrc:compileTestGroovy UP-TO-DATE
:buildsrc:processTestResources UP-TO-DATE
:buildsrc:testClasses UP-TO-DATE
:buildsrc:test UP-TO-DATE
:buildsrc:check UP-TO-DATE
:buildsrc:build

========自定义Plugin=========
...

自定义Transfrom

新建一个groovy类继承com.android.build.api.transform.Transform

package com.zyn.plugin

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.gradle.api.Project

public class PreDexTransform extends Transform {

    Project project
    public PreDexTransform(Project project) {
        this.project = project
    }

    // Transfrom在Task列表中的名字
    // TransfromClassesWithPreDexForXXXX
    @Override
    String getName() {
        return "preDex"
    }

    // 指定input的类型
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    // 指定transfrom的作用范围
    @Override
    Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs,
                   Collection<TransformInput> referencedInputs,
                   TransformOutputProvider outputProvider, boolean isIncremental)
            throws IOException, TransformException, InterruptedException {

       // Transfrom的inputs有两种类型,一种是目录,一种是jar包,分别遍历
        inputs.each {TransformInput input ->

            input.directoryInputs.each {DirectoryInput directoryInput->

                //TODO 这里可以对input的文件做处理,比如代码注入!

                // 获取output目录
                def dest = outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)

                // 将input的目录复制到output指定目录
                FileUtils.copyDirectory(directoryInput.file, dest)
            }

            input.jarInputs.each {JarInput jarInput->

                //TODO 这里可以对input的文件做处理,比如代码注入!

                // 重命名输出文件(同目录copyFile会冲突)
                def jarName = jarInput.name
                def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if(jarName.endsWith(".jar")) {
                   jarName = jarName.substring(0,jarName.length()-4)
                }
                def dest = outputProvider.getContentLocation(jarName+md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }
        }
    }
}

如此就拿到了代码织入的入口,在上图TODO注释处可以处理input文件并输出到output中去

最后还需要修改MyPlugin的apply方法,添加注册Transfrom的逻辑:

@Override
public void apply(Project project) {
    project.logger.error "========自定义Plugin========="
    def android = project.extensions.findByType(AppExtension)
    android.registerTransform(new PreDexTransform(project))
}

这样就获取了代码织入的入口。

6.字节码处理方案

对于字节码的处理,有多个工具可以选择,常用的有ASM,Javassist,BCEL等,各有优劣,开发者可以根据项目需求选择:
– ASM优点是更高效,缺点是较难使用,API非常底层,贴近字节码层面,需要字节码知识及虚拟机相关知识
– Javassist、BCEL等工具可以更简单地操作字节码,但性能方面不如ASM

不同工具库生成同一个类的耗时比较,如下表:

FrameworkFirst timeLater times
Javassist2575.2
BCEL4735.5
ASM62.41.1

7.最后

此篇作为本人的学习记录,水平有限,如有谬误,欢迎指正

    原文作者:AOP
    原文地址: https://blog.csdn.net/baidu_20280065/article/details/70257735
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞