Java反射以及在Android中的特殊應用

stormWen發表於2017-12-11

反射的定義以及組成

關於反射,一般書上的定義是這樣的:JAVA反射機制是在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意方法和屬性;這種動態獲取資訊以及動態呼叫物件方法的功能稱為java語言的反射機制,這幾句解釋說明了反射的作用,動態的跟類進行互動,比如獲取隱藏屬性,修改屬性,獲取物件,建立物件或者方法等等,總之就一句話:

反射是一種具有與類進行動態互動能力的一種機制 為什麼要強調動態互動呢?因為一般情況下都是動態載入,也就是在執行的時候才會載入,而不是在編譯的時候,在需要的時候才進行載入獲取,或者說你可以在任何時候載入一個不存在的類到記憶體中,然後進行各種互動,或者獲取一個沒有公開的類的所有資訊,換句話說,開發者可以隨時隨意的利用反射的這種機制動態進行一些特殊的事情。

反射的組成

由於反射最終也必須有類參與,因此反射的組成一般有下面幾個方面組成:

1.java.lang.Class.java:類物件;

2.java.lang.reflect.Constructor.java:類的構造器物件;

3.java.lang.reflect.Method.java:類的方法物件;

4.java.lang.reflect.Field.java:類的屬性物件;

下面一張圖說明了關係:

Java反射以及在Android中的特殊應用

根據虛擬機器的工作原理,一般情況下,類需要經過:載入->驗證->準備->解析->初始化->使用->解除安裝這個過程,如果需要反射的類沒有在記憶體中,那麼首先會經過載入這個過程,並在在記憶體中生成一個class物件,有了這個class物件的引用,就可以發揮開發者的想象力,做自己想做的事情了。

反射的作用

前面只是說了反射是一種具有與Java類進行動態互動能力的一種機制,在Java和Android開發中,一般情況下下面幾種場景會用到反射機制.

● 需要訪問隱藏屬性或者呼叫方法改變程式原來的邏輯,這個在開發中很常見的,由於一些原因,系統並沒有開放一些介面出來,這個時候利用反射是一個有效的解決方法

● 自定義註解,註解就是在執行時利用反射機制來獲取的。

●在開發中動態載入類,比如在Android中的動態載入解決65k問題等等,模組化和外掛化都離不開反射,離開了反射寸步難行。

反射的工作原理

我們知道,每個java檔案最終都會被編譯成一個.class檔案,這些Class物件承載了這個類的所有資訊,包括父類、介面、建構函式、方法、屬性等,這些class檔案在程式執行時會被ClassLoader載入到虛擬機器中。當一個類被載入以後,Java虛擬機器就會在記憶體中自動產生一個Class物件,而我們一般情況下用new來建立物件,實際上本質都是一樣的,只是這些底層原理對我們開發者透明罷了,我們前面說了,有了class物件的引用,就相當於有了Method,Field,Constructor的一切資訊,在Java中,有了物件的引用就有了一切,剩下怎麼發揮是開發者自己的想象力所能決定的了。

反射的簡單事例

前面說了這麼多理論,下面簡單實踐一下

public class Student {
    private int age;//年齡
    private String name;//姓名
    private String address;//地址
     private static String sTest;
    public Student() {
         throw new IllegalAccessError("Access to default Constructor Error!");
    }

    private Student(int age, String name, String address) {
        this.age = age;
        this.name = name;
        this.address = address;
         sTest = "測試反射";
    }

    private int getAge() {
        return age;
    }
    
    private void setAge(int age) {
        this.age = age;
    }

    private String getName() {
        return name;
    }

    private void setName(String name) {
        this.name = name;
    }

    private String getAddress() {
        return address;
    }

    private void setAddress(String address) {
        this.address = address;
    }
    private static String getTest() {
        return sTest;
    }
}
複製程式碼

在這裡為了練習,刻意用了private來修飾成員變數和方法 下面程式碼用構造器,方法和屬性和靜態方法分別來獲取一下,

public class StudentClient {
    public static void main(String[] args) throws Exception{
        Class<?> clazz=Class.forName("ClassLoader.Student");
        Constructor constructors=clazz.getDeclaredConstructor(int.class,String.class,String.class);
        constructors.setAccessible(true);
        //利用構造器生成物件
        Object mStudent=constructors.newInstance(27,"小文","北京市海定區XX號");
        System.out.println(mStudent.toString());
        //獲取隱藏的int屬性
        Field mAgeField=clazz.getDeclaredField("age");
        mAgeField.setAccessible(true);
        int age= (int) mAgeField.get(mStudent);
        System.out.println("年齡為:"+age);
        //呼叫隱藏的方法
        Method getAddressMethod=clazz.getDeclaredMethod("getAge");
        getAddressMethod.setAccessible(true);
        int newage= (int) getAddressMethod.invoke(mStudent);
        System.out.println("年齡為:"+newage);
        //呼叫靜態方法
        Method getTestMethod=clazz.getDeclaredMethod("getTest");
        getTestMethod.setAccessible(true);
        String result= (String) getTestMethod.invoke(null);
        System.out.println("呼叫靜態方法:"+result);
    }
}
複製程式碼

結果如下:

Java反射以及在Android中的特殊應用
大家都看得懂,應該可以理解,有同學說不是有很多getDeclared***和get***的方法嗎, 實際上都差不多的,只不過用的範圍不一樣而已,getDeclared***獲取的是僅限於本類的所有的不受訪問限制的,而get***獲取的是包括父類的但僅限於public修飾符的,Field和Method也是一樣的道理,這個大家注意一下就好,最後一個需要注意的是呼叫靜態方法和呼叫例項方法有點區別,呼叫例項方法一定需要一個類的例項,而呼叫靜態方法不需要例項的引用,其實這是JVM的在執行方法上的有所區別,JVM在執行方法的時候會建立一個堆疊,堆疊裡面儲存了區域性變數表以及其他的一些必要的資訊,其中區域性變數表裡面也包含了區域性引數,而區域性引數裡面儲存了當前方法的形參,如果是呼叫例項方法的話,那麼形參的第一個引數就是當前的類的引用了,而呼叫的是靜態方法的話,那麼第一個引數是為null的,這一點無法通過任何手段去繞過,換句話說呼叫例項方法一定需要一個類的引用,關於這一點,讀者可以自己去查閱有關JVM的書籍。

當然了,反射的作用絕不止這些,在陣列,泛型,設計模式等方面依然發揮了巨大的作用,但原理並沒有脫離上面說的,讀者可以多檢視相關原始碼學習,原始碼就是最好的學習資源。

反射在Android框架層的應用

這是本文需要重點說明的,眾所周知,Android中的FrameWork是用Java語言編寫的,自然離不開一些反射的影子,而利用反射更是可以達到我們一些常規方法難於達到的目的,再者反射也是Java層中進行Hook的重要手段,目前的外掛化更是大量利用反射。

首先提出需求:如何監控Activity的建立和啟動過程? 有同學說了,我在Activity裡面重寫生命週期方法不就可以了嗎?實際上這個是達不到需求的,因為很簡單,這些生命週期方法的呼叫是在建立和啟動之後很久的事情了,裡面的生命週期方法相對於整個Activity來說是比較後面的事情,要想解決這個問題,必須要知道Activity是怎麼來,中間經過了哪個流程,最後去了哪裡,只有明白了這些,才能知道在哪個階段做哪些事情,我們知道,Activity的啟動是一個IPC過程,也就是Binder機制,裡面經過了本地程式->AMS程式-->再回到本地程式,下面是例項圖:

Java反射以及在Android中的特殊應用

圖畫的有些粗糙,大家將就看吧,從上面可以看到,Activity從本地到遠端AMS以後,遠端AMS只是做了許可權以及屬性的檢查,然後再回到本地程式,這才開始真正的建立和檢查,我們才程式碼來分析一下,涉及到的類有Handler以及ActivityThread和Instrumentation類,首先從遠端程式回到本地程式之後,系統的Handler類H會傳送一個訊息:LAUNCH_ACTIVITY,程式碼如下:省略了一些非必要程式碼,不然篇幅太長,下面的程式碼都是在ActivityThread.java裡面的

  public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } 
複製程式碼

隨後呼叫了handleLaunchActivity方法,handleLaunchActivity方法裡面又呼叫了 performLaunchActivity方法,程式碼如下:

  private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
        // If we are getting ready to gc after going to the background, well
        // we are back active so skip it.
        unscheduleGcIdler();
        mSomeActivitiesChanged = true;

        if (r.profilerInfo != null) {
            mProfiler.setProfiler(r.profilerInfo);
            mProfiler.startProfiling();
        }

        // Make sure we are running with the most recent config.
        handleConfigurationChanged(null, null);

        if (localLOGV) Slog.v(
            TAG, "Handling launch of " + r);

        // Initialize before creating the activity
        WindowManagerGlobal.initialize();

        Activity a = performLaunchActivity(r, customIntent);
        
        ....
        }
複製程式碼

在performLaunchActivity裡面終於建立了Activity了,進入performLaunchActivity裡面看看有一段非常核心的程式碼:

Activity activity = null;
        try {
        //
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            //通過mInstrumentation.newActivity()方法建立了Activity,mInstrumentation是Instrumentation類的例項
            ,物件的類為:Instrumentation.java
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }
複製程式碼

我們現在已經知道了Activity的建立了,是由Instrumentation的newActivity()方法實現,我們看一下方法:

 public Activity newActivity(Class<?> clazz, Context context, 
            IBinder token, Application application, Intent intent, ActivityInfo info, 
            CharSequence title, Activity parent, String id,
            Object lastNonConfigurationInstance) throws InstantiationException, 
            IllegalAccessException {
        Activity activity = (Activity)clazz.newInstance();
        ActivityThread aThread = null;
        activity.attach(context, aThread, this, token, 0, application, intent,
                info, title, parent, id,
                (Activity.NonConfigurationInstances)lastNonConfigurationInstance,
                new Configuration(), null, null, null);
        return activity;
    }
複製程式碼

看到沒,作為四大元件的Activity其實也是一個普通物件,也是由反射建立的,只不過由於加入了生命週期方法,才有元件這個活生生的物件存在, 所以說Android中反射無處不在,分析完了啟動和建立的過程,回到剛才那個需求來說,如何監控Activity的啟動和建立呢?讀者可以先自己想一下,首先啟動是由Handler來傳送訊息,具體的在裡面handlerMessage方法實現的, 這也是Handler裡面的處理程式碼的順序,如下程式碼:

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

大不了我們自己弄一個自定義的Handler.Callback介面,然後替換掉那個H類裡面的處理介面,這樣就可以監控Activity的啟動了,好方法,我們來寫一下程式碼:

public static void hookHandler(Context context) throws Exception {
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThreadMethod.setAccessible(true);
        //獲取主執行緒物件
        Object activityThread = currentActivityThreadMethod.invoke(null);
        //獲取mH欄位
        Field mH = activityThreadClass.getDeclaredField("mH");
        mH.setAccessible(true);
        //獲取Handler
        Handler handler = (Handler) mH.get(activityThread);
        //獲取原始的mCallBack欄位
        Field mCallBack = Handler.class.getDeclaredField("mCallback");
        mCallBack.setAccessible(true);
        //這裡設定了我們自己實現了介面的CallBack物件
        mCallBack.set(handler, new UserHandler(handler));
    }
複製程式碼
public class UserHandler  implements Callback {
    //這個100一般情況下最好也反射獲取,當然了你也可以直接寫死,跟系統的保持一致就好了
    public static final int LAUNCH_ACTIVITY = 100;
    private Handler origin;
    public UserHandler( Handler mHandler) {
        this.origin = mHandler;
    }

    @Override
    public boolean handleMessage(Message msg) {
        if (msg.what == LAUNCH_ACTIVITY) {
        //這樣每次啟動的時候可以做些額外的事情
            Log.d("[app]","做你想要的事情");
        }
        origin.handleMessage(msg);
        return false;
    }
}
複製程式碼

好了,Activity的啟動監控就這樣了,一般寫在application裡面的attachBaseContext()方法裡面,因為這個方法時機最早。 好了,下面來說說Activity的建立的監控,前面我們知道了,Instrumentation的newActivity方法負責建立了Activity,那麼突破口也就是在這裡了,建立為我們自定義的Instrumentation,然後反射替換掉就好,同時重寫newActivity方法,可以做些事情,比如記錄時間之類,下面是程式碼:

public static void hookInstrumentation() throws Exception{
        Class<?> activityThread=Class.forName("android.app.ActivityThread");
        Method currentActivityThread=activityThread.getDeclaredMethod("currentActivityThread");
        currentActivityThread.setAccessible(true);
        //獲取主執行緒物件
        Object activityThreadObject=currentActivityThread.invoke(null);

        //獲取Instrumentation欄位
        Field mInstrumentation=activityThread.getDeclaredField("mInstrumentation");
        mInstrumentation.setAccessible(true);
        Instrumentation instrumentation= (Instrumentation) mInstrumentation.get(activityThreadObject);
        CustomInstrumentation customInstrumentation=new CustomInstrumentation(instrumentation);
        //替換掉原來的,就是把系統的instrumentation替換為自己的Instrumentation物件
        mInstrumentation.set(activityThreadObject,CustomInstrumentation);
        Log.d("[app]","Hook Instrumentation成功");

    }
複製程式碼
public class CustomInstrumentation  extends Instrumentation{
    private Instrumentation base;

    public CustomInstrumentation(Instrumentation base) {
        this.base = base;
    }

    //重寫建立Activity的方法
    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        Log.d("[app]","you are hook!,做自己想要的事情");
        Log.d("[app]","className="+className+" intent="+intent);
        return super.newActivity(cl, className, intent);
    }
}
複製程式碼

同樣在application的attachBaseContext注入就好,當然了,Instrumentation還有其他方法可以重寫,大家可以去試一試,下面是執行的結果:

Java反射以及在Android中的特殊應用
看到沒,監控啟動和建立都實現了,其實這裡面也有好多擴充套件的,比如啟動Activity的時候,Instrumentation一樣是可以監控的,你懂的,再次重寫方法,然後實現自己的邏輯,另外,small外掛化框架就是Hook了Instrumentation來動態載入Activity的,大家有興趣可以去看看,除了以上方法,還有很多方法可以用類似的手段去實現,大家一定要多練習,好記性不如爛筆頭就是這個道理。

使用反射需要注意的地方

從前面可以看出,使用反射非常方便,而且在一些特定的場合下可以實現特別的需求,但是使用反射也是需要注意一下幾點的:

●反射最好是使用public修飾符的,其他修飾符有一定的相容性風險,比如這個版本有,另外的版本可能沒有

●大家都知道的Android開原始碼引起的相容性的問題,這是Android系統開源的最大的問題,特別是那些第三方的ROM,要慎用。

●如果大量使用反射,在程式碼上需要優化封裝,不然不好管理,寫程式碼不僅僅是實現功能,還有維護性和可讀性方法也需要加強,demo中可以直接這樣粗糙些,在專案中還是需要好好組織封裝下的。

今天的文章就寫到這裡,感謝大家閱讀。

相關文章