注:本文來自github的lancet框架的官方文檔,僅做學習轉載
來源:餓了麼github
原文地址:https://github.com/eleme/lancet/edit/master/README_zh.md
Lancet 是一個輕量級Android AOP框架。
- 編譯速度快, 並且支持增量編譯.
- 簡潔的 API, 幾行 Java 代碼完成注入需求.
- 沒有任何多餘代碼插入 apk.
- 支持用於 SDK, 可以在SDK編寫注入代碼來修改依賴SDK的App.
開始使用
安裝
在根目錄的 build.gradle
添加:
dependencies{
classpath 'me.ele:lancet-plugin:1.0.5'
}
在 app 目錄的’build.gradle’ 添加:
apply plugin: 'me.ele.lancet'
dependencies {
provided 'me.ele:lancet-base:1.0.5'
}
示例
Lancet 使用註解來指定代碼織入的規則與位置。
首先看看基礎API使用:
@Proxy("i")
@TargetClass("android.util.Log")
public static int anyName(String tag, String msg){
msg = msg + "lancet";
return (int) Origin.call();
}
這裏有幾個關鍵點:
@TargetClass
指定了將要被織入代碼目標類android.util.Log
.@Proxy
指定了將要被織入代碼目標方法i
.- 織入方式爲
Proxy
(將在後面介紹). Origin.call()
代表了Log.i()
這個目標方法.
所以這個示例Hook方法的作用就是 將代碼裏出現的所有 Log.i(tag,msg)
代碼替換爲Log.i(tag,msg + "lancet")
代碼織入方式
@Proxy
public @interface Proxy {
String value();
}
@Proxy
將使用新的方法替換代碼裏存在的原有的目標方法.
比如代碼裏有10個地方調用了 Dog.bark()
, 代理這個方法後,所有的10個地方的代碼會變爲_Lancet.xxxx.bark()
. 而在這個新方法中會執行你在Hook方法中所寫的代碼.
@Proxy
通常用與對系統 API 的劫持。因爲雖然我們不能注入代碼到系統提供的庫之中,但我們可以劫持掉所有調用系統API的地方。
@NameRegex
@NameRegex 用來限制範圍操作的作用域. 僅用於Proxy
模式中, 比如你只想代理掉某一個包名下所有的目標操作. 或者你在代理所有的網絡請求時,不想代理掉自己發起的請求. 使用NameRegex
對 TargetClass
, ImplementedInterface
篩選出的class再進行一次匹配.
@Insert
public @interface Insert {
String value();
boolean mayCreateSuper() default false;
}
@Insert
將新代碼插入到目標方法原有代碼前後。
@Insert
常用於操作App與library的類,並且可以通過This
操作目標類的私有屬性與方法(下文將會介紹)。
@Insert
當目標方法不存在時,還可以使用mayCreateSuper
參數來創建目標方法。
比如下面將代碼注入每一個Activity的onStop
生命週期
@TargetClass(value = "android.support.v7.app.AppCompatActivity", scope = Scope.LEAF)
@Insert(value = "onStop", mayCreateSuper = true)
protected void onStop(){
System.out.println("hello world");
Origin.callVoid();
}
Scope
將在後文介紹,這裏的意爲目標是 AppCompatActivity
的所有最終子類。
如果一個類 MyActivity extends AppcompatActivity
沒有重寫 onStop
會自動創建onStop
方法,而Origin
在這裏就代表了super.onStop()
, 最後就是這樣的效果:
protected void onStop() {
System.out.println("hello world");
super.onStop();
}
Note:public/protected/private 修飾符會完全照搬 Hook 方法的修飾符。
匹配目標類
public @interface TargetClass {
String value();
Scope scope() default Scope.SELF;
}
public @interface ImplementedInterface {
String[] value();
Scope scope() default Scope.SELF;
}
public enum Scope {
SELF,
DIRECT,
ALL,
LEAF
}
很多情況,我們不會僅匹配一個類,會有注入某各類所有子類,或者實現某個接口的所有類等需求。所以通過 TargetClass
, ImplementedInterface
2個註解及 Scope
進行目標類匹配。
@TargetClass
通過類查找.
@TargetClass
的value
是一個類的全稱.- Scope.SELF 代表僅匹配
value
指定的目標類. - Scope.DIRECT 代表匹配
value
指定類的直接子類. - Scope.All 代表匹配
value
指定類的所有子類. - Scope.LEAF 代表匹配
value
指定類的最終子類.衆所周知java是單繼承,所以繼承關係是樹形結構,所以這裏代表了指定類爲頂點的繼承樹的所有葉子節點.
@ImplementedInterface
通過接口查找. 情況比通過類查找稍複雜一些.
@ImplementedInterface
的value
可以填寫多個接口的全名.- Scope.SELF : 代表直接實現所有指定接口的類.
- Scope.DIRECT : 代表直接實現所有指定接口,以及指定接口的子接口的類.
- Scope.ALL: 代表
Scope.DIRECT
指定的所有類及他們的所有子類. - Scope.LEAF: 代表
Scope.ALL
指定的森林結構中的所有葉節點.
如下圖:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-sxFYfM0K-1593136319046)(media/14948409810841/scope.png)]
當我們使用@ImplementedInterface(value = "I", scope = ...)
時, 目標類如下:
- Scope.SELF -> A
- Scope.DIRECT -> A C
- Scope.ALL -> A B C D
- Scope.LEAF -> B D
匹配目標方法
雖然在 Proxy
, Insert
中我們指定了方法名, 但識別方法必須要更細緻的信息. 我們會直接使用 Hook 方法的修飾符,參數類型來匹配方法.
所以一定要保持 Hook 方法的 public/protected/private
static
信息與目標方法一致,參數類型,返回類型與目標方法一致.
返回類型可以用 Object 代替.
方法名不限. 異常聲明也不限.
但有時候我們並沒有權限聲明目標類. 這時候怎麼辦?
@ClassOf
可以使用 ClassOf
註解來替代對類的直接 import.
比如下面這個例子:
public class A {
protected int execute(B b){
return b.call();
}
private class B {
int call() {
return 0;
}
}
}
@TargetClass("com.dieyidezui.demo.A")
@Insert("execute")
public int hookExecute(@ClassOf("com.dieyidezui.demo.A$B") Object o) {
System.out.println(o);
return (int) Origin.call();
}
ClassOf
的 value 一定要按照 **(package_name.)(outer_class_name$)inner_class_name([]...)
**的模板.
比如:
- java.lang.Object
- java.lang.Integer[][]
- A[]
- A$B
API
我們可以通過 Origin
與 This
與目標類進行一些交互.
Origin
Origin
用來調用原目標方法. 可以被多次調用.
Origin.call()
用來調用有返回值的方法.
Origin.callVoid()
用來調用沒有返回值的方法.
另外,如果你有捕捉異常的需求.可以使用
Origin.call/callThrowOne/callThrowTwo/callThrowThree()
Origin.callVoid/callVoidThrowOne/callVoidThrowTwo/callVoidThrowThree()
For example:
@TargetClass("java.io.InputStream")
@Proxy("read")
public int read(byte[] bytes) throws IOException {
try {
return (int) Origin.<IOException>callThrowOne();
} catch (IOException e) {
e.printStackTrace();
throw e;
}
}
This
僅用於Insert
方式的非靜態方法的Hook中.(暫時)
get()
返回目標方法被調用的實例化對象.
putField & getField
你可以直接存取目標類的所有屬性,無論是 protected
or private
.
另外,如果這個屬性不存在,我們還會自動創建這個屬性. Exciting!
自動裝箱拆箱肯定也支持了.
一些已知的缺陷:
Proxy
不能使用This
- 你不能存取你父類的屬性. 當你嘗試存取父類屬性時,我們還是會創建新的屬性.
For example:
package me.ele;
public class Main {
private int a = 1;
public void nothing(){
}
public int getA(){
return a;
}
}
@TargetClass("me.ele.Main")
@Insert("nothing")
public void testThis() {
Log.e("debug", This.get().getClass().getName());
This.putField(3, "a");
Origin.callVoid();
}
Tips
- 內部類應該命名爲
package.outer_class$inner_class
- SDK 開發者不需要
apply
插件, 只需要provided me.ele:lancet-base:x.y.z
- 儘管我們支持增量編譯. 但當我們使用
Scope.LEAF、Scope.ALL
覆蓋的類有變動 或者修改 Hook 類時, 本次編譯將會變成全量編譯.
License
Licensed under the Apache License, Version 2.0 (the “License”);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
注:本文來自github的lancet框架的官方文檔,僅做學習轉載
來源:餓了麼github
原文地址:https://github.com/eleme/lancet/edit/master/README_zh.md
注:該框架目前尚有許多問題,在項目中建議慎用