APP流畅度优化做得再好,怎么防止同事在代码里面“下毒”再次劣化呢?(1),10天拿到字节跳动安卓岗位offer

public class Test {

private int m = 1;

public int add() {

int j = 2;

int k = m + j;

return k;

}

}

然后我们通过 javac Test.java -g来编译为Test.class,用文本编辑器打开如下:

可以看到是一堆十六进制数,但是其实这一堆十六进制数是按严格的结构拼接在一起的,按顺序分别是:魔数(cafe babe)、java版本号、常量池、访问权限标志、当前类索引、父类索引、接口索引、字段表、方法表、附加属性等十个部分,这些部分以十六进制的形式表达出来并紧凑的拼接在一起,就是上面看到的class字节码文件。

当然上面的十六进制文件显然不具备可阅读性,所以我们可以通过 javap -verbose Test来反编译,有兴趣的可以自己试一试,就可以看到上面说的十个部分,由于我们做字节码插桩一般和方法表关联比较大,所以我们下面着重看一下方法表,下面是反编译后的add()方法:

可以看到包括三部分:

**1. Code:**这里部分就是方法里的JVM指令操作码,也是最重要的一部分,因为我们方法里的逻辑实际上就是一条一条的指令操作码来完成的。

这里可以看到我们的add方法是通过9条指令操作码完成的。当然插桩重点操作的也是这一块,只要能修改指令,也就能操控任何代码了。

**2. LineNumberTable:**这个是表示行号表。是我们的java源码与指令行的行号对应。比如我们上面的add方法java源码里总共有三行,也就是上图中的line10、line11、line12,这三行对应的JVM指令行数。

有了这样的对应关系后,就可以实现比如Debug调试的功能,指令执行的时候,我们就可以定位到该指令对应的源码所在的位置。

**3. LocalVariableTable:**本地变量表,主要包括This和方法里的局部变量。从上图可以看到add方法里有this、j、k三个局部变量。

由于JVM指令集是基于栈的,上面我们已经了解到了add方法的逻辑编译为class文件后变成了9个指令操作码,下面我们简单看看这些指令操作码是如何配合操作数栈+本地变量表+常量池来执行add方法的逻辑的:

按顺序执行9条指令操作码:

0:把数字2入栈

1:将2赋值给本地变量表中的j

2、3:获取常量池中的m入栈

6:将本地变量表中的j入栈

7、8:将m和j相加,然后赋值给本地变量表中的k

9、10:将本地变量表中的k入栈,并return

好的,关于java字节码的暂时就简单介绍这些,主要是让我们基本了解字节码文件的结构,以及编译后代码时如何运行的。而ASM可以通过操作指令码来生成字节码或者插桩,当你可以利用ASM来接触到字节码,并且可以利用ASM的api来操控字节码时,就有很大的自由度来进行各种字节码的生成、修改、操作等等,也就能产生很强大的功能。

三、Gradle plugin + Transform

上面对于插桩框架的选择,我们通过对比最终选择了ASM,但是ASM只负责操作字节码,我们还需要通过自定义gradle plugin的形式来干预编译过程,在编译过程中获取到所有的class文件和jar包,然后遍历他们,利用ASM来修改字节码,达到插桩的目的。

那么干预编译的过程,我们的第一个念头可能就是,对class转为dex的任务进行hook,在class转为dex之前拿到所有的class文件,然后利用ASM对这些字节码文件进行插桩,然后再把处理过的字节码文件作为transformClassesWithDex任务的输入即可。

这种方案的好处是易于控制,我们明确的知道操作的字节码文件是最终的字节码,因为我们是在transformClassesWithDex任务的前一刻拿到字节码文件的。

缺点就是,如果项目开启了混淆,那么在transformClassesWithDex任务的前一刻拿到的字节码文件显然是经过了混淆了的,所以利用ASM操作字节码的时候还需要mapping文件进行配合才能找到正确的插桩点,这一点比较麻烦。

幸亏gradle还为我们提供了另一种干预编译转换过程的方法:Transform.其实我们稍微翻一下gradle编译过程的源码,就会发现一些我们熟知的功能都是通过Transform来实现的。

还有一点,就是关于混淆的问题,上面我们说了如果通过hook transformClassesWithDex任务的方式来实现插桩,开启混淆的情况下会出现问题,那么利用Transform的方式会不会有混淆的问题呢?

下面我们从gradle源码上面找一下答案:

我们从com.android.build.gradle.internal.TaskManager类里的createCompileTask()方法看起,显然这是一个创建编译任务的方法:

protected void createCompileTask(@NonNull VariantScope variantScope) {

//创建一个将java文件编译为class文件的任务

JavaCompile javacTask = createJavacTask(variantScope);

addJavacClassesStream(variantScope);

setJavaCompilerTask(javacTask, variantScope);

//创建一些在编译为class文件后执行的额外任务,比如一些Transform等

createPostCompilationTasks(variantScope);

}

接下来我们看看createPostCompilationTasks()方法,这个方法比较长,下面只保留重要的几个代码:

public void createPostCompilationTasks(@NonNull final VariantScope variantScope) {

、、、、、、

TransformManager transformManager = variantScope.getTransformManager();

、、、、、

// ----- External Transforms 这个就是我们自定义注册进来的Transform-----

// apply all the external transforms.

List customTransforms = extension.getTransforms();

List<List> customTransformsDependencies = extension.getTransformsDependencies();

、、、、、、

、、、、、、

// ----- Minify next 这个就是混淆代码的Transform-----

CodeShrinker shrinker = maybeCreateJavaCodeShrinkerTransform(variantScope);

、、、、、、

、、、、、、

}

其实这个方法里有很多其他Transform,这里都省略了,我们重点只看我们自定义注册的Transform和混淆代码的Transform,从上面的代码上我们自定义的Transform是在混淆Transform之前添加进TransformManager,所以执行的时候我们自定义的Transform也会在混淆之前执行的,也就是说我们利用自定义Transform的方式对代码进行插桩是不受混淆影响的。

所以我们最终确定的方案就是 Gradle plugin + Transform +ASM的技术方案。下面我们正式说说利用该技术方案进行具体实现。

3、具体实现


这里具体实现只挑重点实现步骤讲,详细的可以看具体源码,文章结尾提供了项目的github地址。

一、自定义gradle plugin

关于如何创建一个自定义gradle plugin的项目,这边就不细说了,可以网上搜索,或者直接看MethodTraceMan项目的源码也行,自定义gradle plgin继承自Plugin类,入口是apply方法,我们的apply方法里很简单,就是创建一个自定义扩展配置,然后就是注册一下我们自定义的Transform:

@Override

void apply(Project project) {

project.extensions.create(“traceMan”, TraceManConfig)

def android = project.extensions.getByType(AppExtension)

android.registerTransform(new TraceManTransform(project))

}

二、自定义Transform实现

这里我们创建了一个名叫traceMan的扩展,这样我们可以再使用这个plugin的时候进行一些配置,比如配置插桩的范围,配置是否开启插桩等,这样我们就可以根据自己的需要来配置。

接下来我们看一下TraceManTransform的实现:

public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {

println ‘[MethodTraceMan]: transform()’

def traceManConfig = project.traceMan

String output = traceManConfig.output

if (output == null || output.isEmpty()) {

traceManConfig.output = project.getBuildDir().getAbsolutePath() + File.separator + “traceman_output”

}

if (traceManConfig.open) {

//读取配置

Config traceConfig = initConfig()

traceConfig.parseTraceConfigFile()

Collection inputs = transformInvocation.inputs

TransformOutputProvider outputProvider = transformInvocation.outputProvider

if (outputProvider != null) {

outputProvider.deleteAll()

}

//遍历,分为class文件变量和jar包的遍历

inputs.each { TransformInput input ->

input.directoryInputs.each { DirectoryInput directoryInput ->

traceSrcFiles(directoryInput, outputProvider, traceConfig)

}

input.jarInputs.each { JarInput jarInput ->

traceJarFiles(jarInput, outputProvider, traceConfig)

}

}

}

}

三、利用ASM进行插桩

接下来看看遍历class文件后如何利用ASM的访问者模式进行插桩:

static void traceSrcFiles(DirectoryInput directoryInput, TransformOutputProvider outputProvider, Config traceConfig) {

if (directoryInput.file.isDirectory()) {

directoryInput.file.eachFileRecurse { File file ->

def name = file.name

//根据配置的插桩范围决定要对某个class文件进行处理

if (traceConfig.isNeedTraceClass(name)) {

//利用ASM的api对class文件进行访问

ClassReader classReader = new ClassReader(file.bytes)

ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)

ClassVisitor cv = new TraceClassVisitor(Opcodes.ASM5, classWriter, traceConfig)

classReader.accept(cv, EXPAND_FRAMES)

byte[] code = classWriter.toByteArray()

FileOutputStream fos = new FileOutputStream(

file.parentFile.absolutePath + File.separator + name)

fos.write(code)

fos.close()

}

}

}

//处理完输出给下一任务作为输入

def dest = outputProvider.getContentLocation(directoryInput.name,

directoryInput.contentTypes, directoryInput.scopes,

Format.DIRECTORY)

FileUtils.copyDirectory(directoryInput.file, dest)

}

可以看到,最终是TraceClassVisitor类里对class文件进行处理的,我们看一下

TraceClassVisitor:

class TraceClassVisitor(api: Int, cv: ClassVisitor?, var traceConfig: Config) : ClassVisitor(api, cv) {

private var className: String? = null

private var isABSClass = false

private var isBeatClass = false

private var isConfigTraceClass = false

override fun visit(

version: Int,

access: Int,

name: String?,

signature: String?,

superName: String?,

interfaces: Array?

) {

super.visit(version, access, name, signature, superName, interfaces)

this.className = name

//抽象方法或者接口

if (access and Opcodes.ACC_ABSTRACT > 0 || access and Opcodes.ACC_INTERFACE > 0) {

this.isABSClass = true

}

//插桩代码所属类

val resultClassName = name?.replace(“.”, “/”)

if (resultClassName == traceConfig.mBeatClass) {

this.isBeatClass = true

}

//是否是配置的需要插桩的类

name?.let { className ->

isConfigTraceClass = traceConfig.isConfigTraceClass(className)

}

}

override fun visitMethod(

access: Int,

name: String?,

desc: String?,

signature: String?,

exceptions: Array?

): MethodVisitor {

val isConstructor = MethodFilter.isConstructor(name)

//抽象方法、构造方法、不是插桩范围内的方法,则不进行插桩

return if (isABSClass || isBeatClass || !isConfigTraceClass || isConstructor) {

super.visitMethod(access, name, desc, signature, exceptions)

} else {

//TraceMethodVisitor中对方法进行插桩

val mv = cv.visitMethod(access, name, desc, signature, exceptions)

TraceMethodVisitor(api, mv, access, name, desc, className, traceConfig)

}

}

}

再来看看TraceMethodVisitor:

override fun onMethodEnter() {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

《设计思想解读开源框架》

第一章、 热修复设计

  • 第一节、 AOT/JIT & dexopt 与 dex2oat

  • 第二节、 热修复设计之 CLASS_ISPREVERIFIED 问题

  • 第三节、热修复设计之热修复原理

  • 第四节、Tinker 的集成与使用(自动补丁包生成)

    第二章、 插件化框架设计

  • 第一节、 Class 文件与 Dex 文件的结构解读

  • 第二节、 Android 资源加载机制详解

  • 第三节、 四大组件调用原理

  • 第四节、 so 文件加载机制

  • 第五节、 Android 系统服务实现原理

    第三章、 组件化框架设计

  • 第一节、阿里巴巴开源路由框——ARouter 原理分析

  • 第二节、APT 编译时期自动生成代码&动态类加载

  • 第三节、 Java SPI 机制

  • 第四节、 AOP&IOC

  • 第五节、 手写组件化架构

    第四章、图片加载框架

  • 第一节、图片加载框架选型

  • 第二节、Glide 原理分析

  • 第三节、手写图片加载框架实战

    第五章、网络访问框架设计

  • 第一节、网络通信必备基础

  • 第二节、OkHttp 源码解读

  • 第三节、Retrofit 源码解析

    第六章、 RXJava 响应式编程框架设计

  • 第一节、链式调用

  • 第二节、 扩展的观察者模式

  • 第三节、事件变换设计

  • 第四节、Scheduler 线程控制

    第七章、 IOC 架构设计

  • 第一节、 依赖注入与控制反转

  • 第二节、ButterKnife 原理上篇、中篇、下篇

  • 第三节、Dagger 架构设计核心解密

    第八章、 Android 架构组件 Jetpack

  • 第一节、 LiveData 原理

  • 第二节、 Navigation 如何解决 tabLayout 问题

  • 第三节、 ViewModel 如何感知 View 生命周期及内核原理

  • 第四节、 Room 架构方式方法

  • 第五节、 dataBinding 为什么能够支持 MVVM

  • 第六节、 WorkManager 内核揭秘

  • 第七节、 Lifecycles 生命周期


    本文包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

注入与控制反转**

  • 第二节、ButterKnife 原理上篇、中篇、下篇

  • 第三节、Dagger 架构设计核心解密

    [外链图片转存中…(img-AZz6kur5-1712451684886)]

    第八章、 Android 架构组件 Jetpack

  • 第一节、 LiveData 原理

  • 第二节、 Navigation 如何解决 tabLayout 问题

  • 第三节、 ViewModel 如何感知 View 生命周期及内核原理

  • 第四节、 Room 架构方式方法

  • 第五节、 dataBinding 为什么能够支持 MVVM

  • 第六节、 WorkManager 内核揭秘

  • 第七节、 Lifecycles 生命周期

    [外链图片转存中…(img-y6wyzcVK-1712451684886)]
    本文包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
    [外链图片转存中…(img-DS21LbXQ-1712451684887)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值