字節碼插樁輕松掌握 [復制鏈接]

2019-8-15 10:33
BlueManlove 閱讀:1013 評論:0 贊:0
Tag:  插樁

1 什么是插樁?

聽到關于“插樁”的詞語,第一眼覺得會很高深,那到底什么是插樁呢?用通俗的話來講,插樁就是將一段代碼通過某種策略插入到另一段代碼,或替換另一段代碼。這里的代碼可以分為源碼和字節碼,而我們所說的插樁一般指字節碼插樁。

圖1是Android開發者常見的一張圖,我們編寫的源碼(.java)通過javac編譯成字節碼(.class),然后通過dx/d8編譯成dex文件。

我們下面要講的插樁,就是在.class轉為.dex之前,修改.class文件從而達到修改或替換代碼的目的。

那有人肯定會有這樣的疑問?既然插樁是插入或替換代碼,那為何我不自己直接插入或替換呢?為何還要用這么“復雜”的工具?別著急,第二個問題將會給你答案。

2 插樁的應用場景有哪些?

技術是服務于業務的,一個無法推進業務進步的技術并不值得我們學習。在上面,我們對插樁的理解是:插入,替換代碼。那么,結合這個核心主線我們來挖掘插樁能被應用的場景有哪些?

  • 代碼插入

我們所熟悉的ButterKnife,Dagger這些常用的框架,也是在編譯期間生成了代碼,簡化了程序員的操作。假設有這么一個需求,要監控某些或者所有方法的執行耗時?你會怎么做呢?如果你監控的方法只有十幾個或者幾十個,那么也許通過程序員自身的編碼就能輕松解決;但是如果監控的方法達到百千甚至萬級別,你還通過編碼來解決?那么程序員存在的價值在哪里?面對這樣的重復勞動問題,最先想到的就應該是自動化,也就是我們今天所講的插樁。通過插樁,我們掃描每一個class文件,并針對特定規則進行字節碼修改從而達到監控每個方法耗時的目的。關于如何實現這樣的需求,后面我會詳細講述。

  • 代碼替換

如果遇到這么一個需求,需要將項目中所有使用某個方法(如Dialog.show())的地方替換成自己包裝的方法(MyDialog.show()),那么你該如何解決呢?有人會說,直接使用快捷鍵就能全局替換。那么有兩個問題

1 如果有其他類定義了show()方法,并被調用了,直接使用快捷鍵是否會被錯誤替換?

2 如果其他引用包使用了該方法,你怎么替換呢?

沒關系,插樁同樣可以解決你的問題。

綜合上面所說的兩點,其實很多業務場景都使用了插樁技術,比如無痕埋點,性能監控等。

3 掌握插樁應該具備的基礎知識有哪些?

上面講了插樁的應用場景,是否現在想躍躍欲試呢?別著急,想掌握好插樁技術,練就扎實的插樁功底,我們是需要具備一些基礎知識的。

  • 熟練掌握字節碼相關技術??蓞⒖?一文讓你明白Java字節碼
  • Gradle自定義插件,直接參考官網 Writing Custom plugins
  • 如果你想運用在Android項目中,那么還需要掌握Transform API,
  • 這是android在將class轉成dex之前給我們預留的一個接口,在該接口中我們可以通過插件形式來修改class文件。
  • 字節碼修改工具。如AspectJ,ASM,javasisst。這里我推薦使用ASM,關于ASM相關知識,在下一章我給大家簡單介紹。同樣大家可以參考 Asm官方文檔
  • groovy語言基礎
  • 如果你具備了上面5塊知識,那么恭喜你,會很順利的完成字節碼插樁技術了。下面,我通過實戰一個很簡單的例子,帶領大家一起領略插樁的風采。

4 使用ASM進行字節碼插樁

1 什么是ASM?

ASM是生成和轉換已編譯的Java類工具,就是我們插樁需要使用的工具。

2 兩種API?

ASM提供了兩種API來生成和轉換已編譯類,一個是核心API,以基于事件形式來表示類;另一個是樹API,以基于對象形式來表示類。

3 基于事件形式

我們通過上面的基礎知識,了解到類的結構,類包含字段,方法,指令等;基于事件的API把類看作是一系列事件來表示,每一個類的事件表示一個類的元素。類似解析XML的SAX

4 基于對象形式

基于對象的API將類表示成一棵對象樹,每個對象表示類的一部分。類似解析XML的DOM

5 優缺點比較

事件形式 對象形式 內存占用 少 多 實現難度 難 易 通過上面表格,我們清楚的了解到:

  • 事件API內存占用少于對象API,因為事件API不需要在內存中創建和存儲對象樹
  • 事件API實現難度比對象API大,因為事件API在任意時刻類中只有一個元素可使用,但是對象API能獲得整個類。
  • 那么接下來,我們就通過比較容易實現的對象API入手,一起完成上面的需求。
  • 我們Android的構建工具是Gradle,因此我們結合transform和Gradle插件方式來完成該需求,接下來我們來看看gradle官方提供的3種插件形式
  • 6 Gradle插件的3種形式

插件形式 說明 Build script 直接在build script中寫插件代碼,不可復用 buildSrc 獨立項目結構,只能在本構建體系中復用,無法提供給其他項目 Standalone 獨立項目結構,發布到倉庫,可以復用 由于我們是demo,并不需要共享給其他項目,因此采用buildSrc方式即可,但是正常項目中都采用Standalone形式。

5 插樁實踐

目標 : 刪除所有以test開頭的方法

接下來我們來完成一個非常小的需求,刪除所有以test開頭的方法。為什么說這是一個小需求,因為這并不涉及指令的操作,所有操作通過方法名完成即可。通過完成這個demo,只是拋磚引玉。如若后期需要,可以逐步深入到指令級別替換。

接下來的步驟就是創建demo的過程

  • 1 新建buildSrc目錄,用來存放源代碼位置。針對不同語言可以新建不同目錄。

如上圖所示的是buildSrc的結構。

  • 2 在buildSrc的gradle文件中我們需要配置如下代碼
apply plugin: 'groovy'
dependencies {
compile gradleApi()//在使用自定義插件時候,一定要引用org.gradle.api.Plugin
compile 'com.android.tools.build:gradle:3.3.2'//使用自定義transform時候,需要引用com.android.build.api.transform.Transform
compile 'org.ow2.asm:asm:6.0'
compile 'commons-io:commons-io:2.6'
}
repositories {
mavenCentral()
jcenter()
google()
}
  • 3 重寫Transform API

在groovy目錄下新建一個groovy類并繼承Transform,注意導包com.android.build.api.transform,并實現抽象方法和transform方法,如下

class MyTransform extends Transform {
Project project
MyTransform(Project project) {
this.project = project
}
@Override
String getName() {
return "MyTransform"
}
//設置輸入類型,我們是針對class文件處理
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
//設置輸入范圍,我們選擇整個項目
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return true
}
//重點就是該方法,我們需要將修改字節碼的邏輯就從這里開始
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
inputs.each {
TransformInput input ->
input.getJarInputs().each {
//處理jar文件,代碼太多,這里暫時不貼
}
input.getDirectoryInputs().each {
//處理目錄文件,這里的ASMHelper.transformClass()是修改字節碼邏輯
def destDir = transformInvocation.outputProvider.getContentLocation(
"${dir.name}_transformed",
dir.contentTypes,
dir.scopes,
Format.DIRECTORY)
if (dir.file) {
def modifiedRecord = [:]
dir.file.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) {
File classFile ->
def className = classFile.absolutePath.replace(dir.getFile().getAbsolutePath(), "")
if (!ASMHelper.filter(className)) {
def transformedClass = ASMHelper.transformClass(classFile, dir.file, transformInvocation.context.temporaryDir)
modifiedRecord[(className)] = transformedClass
}
}
FileUtils.copyDirectory(dir.file, destDir)
modifiedRecord.each { name, file ->
def targetFile = new File(destDir.absolutePath, name)
if (targetFile.exists()) {
targetFile.delete()
}
FileUtils.copyFile(file, targetFile)
}
modifiedRecord.clear()
}
}
}
}
  • 4 實現字節碼修改邏輯

Transform我們已經定義完成,接下來就要針對讀入的字節碼進行修改。我們采用對象API進行解析class文件。一共就是3個步驟:

1 將輸入流轉化為ClassNode

2 處理ClassNode,這里就是我們的業務邏輯所在

3 將ClassNode轉為字節數組輸出

當然還有其他文件的IO操作,這里因為篇幅限制未貼出,如若需要demo,可以私信。

static byte[] modifyClass(InputStream inputStream) {
ClassNode classNode = new ClassNode(Opcodes.ASM5)
ClassReader classReader = new ClassReader(inputStream)
//1 將讀入的字節轉為classNode
classReader.accept(classNode, 0)
//2 對classNode的處理邏輯
Iterator<MethodNode> iterator = classNode.methods.iterator();
while (iterator.hasNext()) {
MethodNode node = iterator.next()
if (node.name.startsWith("test")) {
iterator.remove()
}
}
ClassWriter classWriter = new ClassWriter(0)
//3 將classNode轉為字節數組
classNode.accept(classWriter)
return classWriter.toByteArray()
}
  • 5 插件化

上面我們完成了字節碼修改邏輯以及定義Transform,但是并沒有完成插件的定義。結合Transform API我們了解到,需要將我們自定義的Transform注冊到插件中,如下

class MyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.android.registerTransform(new MyTransform(project))
}
}
  • 6 提供可對外使用的插件

插件完成了,但是怎么才能對外使用呢?上面我們說到,我們采取3種插件形式之一的buildSrc。我們上文中創建了plugin.properties文件。只需要在該文件中編輯實現類即可

implementation-class=MyPlugin
  • 7 應用方應用插件

在應用方的gradle文件中做如下配置

apply plugin: 'plugin'

上面代碼我們注意到,plugin這個插件和plugin.properties的文件名是一樣的。是的,應用方應用的插件名和我們定義的properties文件名保持一致。

  • 8 結果展示

源代碼如下,經過我們插件處理之后,編譯后的字節碼應該沒有了testDemo方法。

public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(android.R.layout.activity_list_item);
}
public void testDemo() {
System.out.println("demo test");
}
}

那么,處理后的字節碼在哪呢?在$project/build/intermediates/transforms/MyTransform/...MyTransform是我自定義Transform的類名,下面有debug和release包。繼續下去大家應該能找到對應的類。

上圖我們看到,已經沒有的testDemo方法。成功!


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

在柳州学什么小吃赚钱 广东11选5单双大小稳赚 专家免费推荐股票 六肖公式计算方法公开 最大的股票论坛 期货配资交易 幸运5分彩开奖结果图 时时彩计划稳定版app下载 福建体彩网31选7论坛 安徽快3基本二码遗漏 今日安徽快三走势图