Android 極簡反射教程及應用示例

_qisen發表於2016-07-14

原文連結:www.woaitqs.cc/android/201…

Java 反射簡介

Java 程式的執行需要相應的環境(Java Runtime Environment), 而這其中最有名的就是 JVM,JVM 提供了動態執行位元組碼的能力,除了 JVM 幫我們做連結、載入位元組碼相關的事情外,也通過反射提供給我們動態修改的能力。反射使得我們能夠在執行時,不知道類名、方法名的情況下檢視其中的介面、方法和欄位等資訊,另一方面,也可以動態呼叫方法、新建物件,甚至篡改欄位值。

那介紹了反射是幹嘛之後,反射能在實際的工作中發揮什麼作用嗎?Android 系統在設計的時候,出於安全和架構的考慮,利用了 Java 許可權相關的東西(private,package等等,以及 @hide 這個註解)使得我們無法訪問某些欄位,或者方法。但在實際開發過程中,這些隱藏的欄位或者方法卻能提供給我們非常想要的特性。在這種矛盾的情況下,反射就能滿足我們的需求,像是開啟隱藏關卡的一把鑰匙。

總結起來就是,反射提供了一種與 Class 檔案進行動態互動的機制。例如在下面的入口函式中,就可以看到 HashMapClass 裡所有的方法。

public class HashMapClass extends HashMap {

  public static void main(String[] args) {
    Method[] methods = HashMapClass.class.getMethods();

    for (Method method : methods) {
      System.out.println("method name is " + method.getName());
    }
  }

}複製程式碼

Class 類簡介

在進行接下來的反射教程中,首先應該瞭解 Class Object。Java 中所有的型別,包括 int、float 等基本型別,都有與之相關的 Class 物件。如果知道對應的 Class name,可以通過 Class.forName() 來構造相應的 Class 物件,如果沒有對應的 class,或者沒有載入進來,那麼會丟擲 ClassNotFoundException 物件。

Class 封裝了一個類所包含的資訊,主要的介面如下:

    try {
      Class mClass = Class.forName("java.lang.Object");

      // 不包含包名字首的名字
      String simpleName = mClass.getSimpleName();

      // 型別修飾符, private, protect, static etc.
      int modifiers = mClass.getModifiers();
      // Modifier 提供的一些用於判讀型別的靜態方法.
      Modifier.isPrivate(modifiers);

      // 父類的資訊
      Class superclass = mClass.getSuperclass();

      // 建構函式
      Constructor[] constructors = mClass.getConstructors();

      // 欄位型別
      Field[] fields = mClass.getFields();
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }複製程式碼

常用反射方法

下面列舉一些反射常見的應用場景,主要從 Student 這個類進行入手。

public class Student {

  private final String name;
private int grade = 1;

  public Student(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

  private int getGrade() {
    return grade;
  }

  private void goToSchool() {
    System.out.println(name + " go to school!");
  }
}複製程式碼

1)反射構建 Student 物件

try {
  Class studentClass = Student.class;

  // 引數型別為一個 String 的建構函式
  Constructor constructor = studentClass.getConstructor(new Class[]{String.class});

  // 例項化 student 物件
  Student student = (Student)constructor.newInstance("Li Lei");
  System.out.print(student.getName());
} catch (ReflectiveOperationException e) {
  e.printStackTrace();
}複製程式碼

2)反射修改私有變數

try {
  Student student = new Student("Han MeiMei");
  System.out.println("origin grade is " + student.getGrade());

  Class studentClass = Student.class;
  // 獲取宣告的 grade 欄位,這裡要注意 getField 和 getDeclaredField 的區別
  Field gradeField = studentClass.getDeclaredField("grade");

  // 如果是 private 或者 package 許可權的,一定要賦予其訪問許可權
  gradeField.setAccessible(true);

  // 修改 student 物件中的 Grade 欄位值
  gradeField.set(student, 2);
  System.out.println("after reflection grade is " + student.getGrade());

} catch (ReflectiveOperationException e) {
  e.printStackTrace();
}複製程式碼

3)反射呼叫私有方法

try {
  Student student = new Student("Han MeiMei");

  // 獲取私有方法,同樣注意 getMethod 和 getDeclaredMethod 的區別
  Method goMethod = Student.class.getDeclaredMethod("goToSchool", null);
  // 賦予訪問許可權
  goMethod.setAccessible(true);

  // 呼叫 goToSchool 方法。
  goMethod.invoke(student, null);

} catch (ReflectiveOperationException e) {
  e.printStackTrace();
}複製程式碼

Android 反射應用示例

學以致用,現在我們來通過實際的例子,來看看如何利用 Java 的反射特性來完成一些牛逼的功能。設想我們想通過外掛的方式,來啟動一個未註冊的 Activity,這就會涉及到很多問題,其中之一就是如何賦予這些外掛 Activity 生命週期。這個例子就是通過反射的方式,來手動地進行 Activity 生命週期的通知。

1)瞭解並熟悉程式碼細節

要實現上述的功能,第一步就是要知道 Activity 的生命週期是如何運作的,要對程式碼細節有所瞭解。因為反射所操作的物件是具體的 Class 物件,如果不清楚原始碼細節,反射將無從說起。

篇幅所限,具體的原理又較為複雜,這裡列出連結 Android 外掛化原理解析——Activity生命週期管理, 有興趣的同學可以自行檢視,在這隻進行大體上的說明。

Activity 生命週期與 ActivityThread 息息相關,我們來進行各個突破,先看看 Activity 是怎麼啟動的。當需要啟動 Activity 時,ActivityManagerService 會通過 Binder 機制向 ActivityThread 傳送訊息,經過鏈式地呼叫後,會執行到scheduleLaunchActivity 這個方法,我們看看其內部的實現。

// we use token to identify this activity without having to send the
// activity itself back to the activity manager. (matters more with ipc)
@Override
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
        ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
        CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
        int procState, Bundle state, PersistableBundle persistentState,
        List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
        boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {

    updateProcessState(procState, false);

    ActivityClientRecord r = new ActivityClientRecord();

    r.token = token;
    r.ident = ident;
    r.intent = intent;
    r.referrer = referrer;
    r.voiceInteractor = voiceInteractor;
    r.activityInfo = info;
    r.compatInfo = compatInfo;
    r.state = state;
    r.persistentState = persistentState;

    r.pendingResults = pendingResults;
    r.pendingIntents = pendingNewIntents;

    r.startsNotResumed = notResumed;
    r.isForward = isForward;

    r.profilerInfo = profilerInfo;

    r.overrideConfig = overrideConfig;
    updatePendingConfiguration(curConfig);

    sendMessage(H.LAUNCH_ACTIVITY, r);
}複製程式碼

注意最後的 sendMessage 方法,說明內部是採用的 handler 機制來進行通訊的。在這篇文章中 Android 應用程式啟動流程 提及到當應用程式啟動後,會呼叫 ActivityThread 的 main 方法,並在這個方法中進行相應的訊息迴圈初始化,其後在主執行緒上的訊息傳遞都是通過 ActivityThread 中的 H 這個內部來進行初始化,這裡的 H 就是可能的突破口之一。

private class H extends Handler {
    public static final int LAUNCH_ACTIVITY         = 100;
    public static final int PAUSE_ACTIVITY          = 101;
    public static final int PAUSE_ACTIVITY_FINISHING= 102;
    public static final int STOP_ACTIVITY_SHOW      = 103;
    public static final int STOP_ACTIVITY_HIDE      = 104;
    public static final int SHOW_WINDOW             = 105;
    public static final int HIDE_WINDOW             = 106;
    public static final int RESUME_ACTIVITY         = 107;
    public static final int SEND_RESULT             = 108;
    public static final int DESTROY_ACTIVITY        = 109;
    public static final int BIND_APPLICATION        = 110;
    public static final int EXIT_APPLICATION        = 111;
    public static final int NEW_INTENT              = 112;
    public static final int RECEIVER                = 113;
    public static final int CREATE_SERVICE          = 114;
    public static final int SERVICE_ARGS            = 115;
    public static final int STOP_SERVICE            = 116;

    //......

    public void handleMessage(Message msg){
        // ...
    }
}複製程式碼

既然發現通訊是通過 H 這個 Handler 來完成的,那麼再看看 Handler 的實現原理,這裡也有一篇文章供參考, Android Handler機制全解析 。Handler 在內部維護著一個 callback 物件,當有訊息發生時,會通過這個 callback 往外傳送訊息。

/**
 * Handle system messages here.
 */
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}複製程式碼

如果能夠替換 callback 變數,這樣訊息就可以傳遞到替換後的 callback 裡了。這樣是否能達到我們的目的?

2) 驗證是否滿足反射條件

如果我們反射的物件,擁有多個例項,那麼我們就需要在不同的地方進行處理,顯然這樣會額外增加實現的複雜度,因而反射儘量在 單例或者靜態 例項上完成,程式碼的複雜度能提升不少。

在前面提到了通過替換 callback 的方式,這樣是否可行,我們來驗證下。首先 H 是放置在 ActivityThread 這個類裡面的,而 ActivityThread 執行的執行緒就是主執行緒,我們知道每一個應用都擁有一個主執行緒,因而這裡的 ActivityThread 只存在一份,進而也可以保證 H 的唯一性。

另一方面,ActivityThread 在內部也維護了 currentActivityThread 這個變數,雖然由於 API 的訪問限制,不能直接訪問,但也同樣可以由反射拿到。

至此,可以證明這種方式理論上是可以成功的。

3)實施程式碼細節,並在合適的地方進行程式碼注入

首先,我們自定義出自定義的 callback 物件,這個 callback 作為 H 中 callback 的代理,這裡需要注意的是 msg 的定義要和底層保持一致,程式碼如下:

public class LaunchCallback implements Handler.Callback {

  public static final int LAUNCH_ACTIVITY = 100;

  private Handler.Callback originCallback;

  public LaunchCallback(Handler.Callback originCallback) {
    this.originCallback = originCallback;
  }

  @Override
  public boolean handleMessage(Message msg) {

    if (msg.what == LAUNCH_ACTIVITY) {
      Toast.makeText(
          VApp.getApp().getApplicationContext(),
          "activity is going to launch! ", Toast.LENGTH_SHORT).show();
    }

    return originCallback.handleMessage(msg);
  }

}複製程式碼

通過前文提及的反射方法,將執行的 callback 替換為自定義的 callback,程式碼如下:

public class InjectTool {

  public static void dexInject() {
    try {

      // 通過反射呼叫 ActivityThread 的靜態方法, 獲取 currentActivityThread
      Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
      Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
      currentActivityThreadMethod.setAccessible(true);
      Object currentActivityThread = currentActivityThreadMethod.invoke(null);

      // 獲取 currentActivityThread 這個示例中的 mH
      Field handlerField = activityThreadClass.getDeclaredField("mH");
      handlerField.setAccessible(true);
      Handler handler = (Handler) handlerField.get(currentActivityThread);

      // 修改 mH 中的 callback 欄位
      Field callbackField = Handler.class.getDeclaredField("mCallback");
      callbackField.setAccessible(true);
      Handler.Callback callback = (Handler.Callback) callbackField.get(handler);

      callbackField.set(handler, new LaunchCallback(callback));
    } catch (IllegalArgumentException | NoSuchMethodException | IllegalAccessException
        | InvocationTargetException | ClassNotFoundException | NoSuchFieldException e) {
      e.printStackTrace();
    }
  }

}複製程式碼

在 Application 中進行注入,完成修改的落地

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    InjectTool.dexInject();
}複製程式碼

實際執行的效果,如圖所示:

activity launch

activity 其他的生命週期也可以同樣處理,這裡就不再贅述了。

4)反射使用總結

在通過反射實現相關功能的時候,第一件事情就是認真地閱讀原始碼,理清其中的脈絡,其後找尋其中的突破點,這些點一般為 static 方法或者單例物件,最後才是程式碼落地。

相關文章