参考

博客 Android AOP之字节码插桩

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

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

1.AOP的概念

AOP(面向切面编程)这个概念的提出主要是相对于OOP(面向对象编程)。OOP能够将项目划分为多个模块,但有些功能是各模块都需要的,例如性能监控、日志管理等,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文件整合之前,我们需要找到这样一个入口进行代码织入。打包流程如下图:

上图中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.pluginimport org.gradle.api.Plugin;import org.gradle.api.Projectpublic 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.pluginimport com.android.build.api.transform.*import com.android.build.gradle.internal.pipeline.TransformManagerimport org.gradle.api.Projectpublic class PreDexTransform extends Transform {    Project project    public PreDexTransform(Project project) {        this.project = project    }    // Transfrom在Task列表中的名字    // TransfromClassesWithPreDexForXXXX    @Override    String getName() {        return "preDex"    }    // 指定input的类型    @Override    Set.ContentType> getInputTypes() {        return TransformManager.CONTENT_CLASS    }    // 指定transfrom的作用范围    @Override    Set.Scope> getScopes() {        return TransformManager.SCOPE_FULL_PROJECT    }    @Override    boolean isIncremental() {        return false    }    @Override    void transform(Context context, Collection inputs,                   Collection 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的逻辑:

@Overridepublic 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

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

Framework First time Later times
Javassist 257 5.2
BCEL 473 5.5
ASM 62.4 1.1

7.最后

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

更多相关文章

  1. 一款常用的 Squid 日志分析工具
  2. GitHub 标星 8K+!一款开源替代 ls 的工具你值得拥有!
  3. RHEL 6 下 DHCP+TFTP+FTP+PXE+Kickstart 实现无人值守安装
  4. Linux 环境下实战 Rsync 备份工具及配置 rsync+inotify 实时同步
  5. Android常用UI组件 - Button
  6. android 获取本地缓存文件大小,删除功能
  7. 一个android文本比对APP的实现(三)-设计模式在文件选择模块中的运用
  8. 一篇文章带你搞定 Android(安卓)项目的目录结构及如何修改应用的
  9. Android(安卓)开发艺术探索笔记(23)

随机推荐

  1. Android CalendarView 使用
  2. Android DOM解析XML
  3. Android自定义弹窗进度条
  4. ch029 Android service aidl
  5. Accessing internal data on Android dev
  6. Android(安卓)APK反编译
  7. 【工具类】如何通过代码安装一个apk文件
  8. Android 解析Html
  9. Android Bluetooth UUID
  10. Android Version