【AOP】一個輕量級Android AOP框架Lancet介紹

注:本文來自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模式中, 比如你只想代理掉某一個包名下所有的目標操作. 或者你在代理所有的網絡請求時,不想代理掉自己發起的請求. 使用NameRegexTargetClass , 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

通過類查找.

  1. @TargetClassvalue 是一個類的全稱.
  2. Scope.SELF 代表僅匹配 value 指定的目標類.
  3. Scope.DIRECT 代表匹配 value 指定類的直接子類.
  4. Scope.All 代表匹配 value 指定類的所有子類.
  5. Scope.LEAF 代表匹配 value 指定類的最終子類.衆所周知java是單繼承,所以繼承關係是樹形結構,所以這裏代表了指定類爲頂點的繼承樹的所有葉子節點.

@ImplementedInterface

通過接口查找. 情況比通過類查找稍複雜一些.

  1. @ImplementedInterfacevalue 可以填寫多個接口的全名.
  2. Scope.SELF : 代表直接實現所有指定接口的類.
  3. Scope.DIRECT : 代表直接實現所有指定接口,以及指定接口的子接口的類.
  4. Scope.ALL: 代表 Scope.DIRECT 指定的所有類及他們的所有子類.
  5. 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

我們可以通過 OriginThis 與目標類進行一些交互.

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

  1. 內部類應該命名爲 package.outer_class$inner_class
  2. SDK 開發者不需要 apply 插件, 只需要 provided me.ele:lancet-base:x.y.z
  3. 儘管我們支持增量編譯. 但當我們使用 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

注:該框架目前尚有許多問題,在項目中建議慎用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章