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

反射的定義以及組成

關于反射,一般書上的定義是這樣的:JAVA反射機制是在運行狀態(tài)中,對于任意一個類,都能夠知道這個類的所有屬性和方法;對于任意一個對象,都能夠調用它的任意方法和屬性;這種動態(tài)獲取信息以及動態(tài)調用對象方法的功能稱為java語言的反射機制,這幾句解釋說明了反射的作用,動態(tài)的跟類進行交互,比如獲取隱藏屬性,修改屬性,獲取對象,創(chuàng)建對象或者方法等等,總之就一句話:

反射是一種具有與類進行動態(tài)交互能力的一種機制為什么要強調動態(tài)交互呢?因為一般情況下都是動態(tài)加載,也就是在運行的時候才會加載,而不是在編譯的時候,在需要的時候才進行加載獲取,或者說你可以在任何時候加載一個不存在的類到內存中,然后進行各種交互,或者獲取一個沒有公開的類的所有信息,換句話說,開發(fā)者可以隨時隨意的利用反射的這種機制動態(tài)進行一些特殊的事情。

反射的組成

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

1.java.lang.Class.java:類對象;

2.java.lang.reflect.Constructor.java:類的構造器對象;

3.java.lang.reflect.Method.java:類的方法對象;

4.java.lang.reflect.Field.java:類的屬性對象;

下面一張圖說明了關系:

反射機制.jpg

根據虛擬機的工作原理,一般情況下,類需要經過:加載->驗證->準備->解析->初始化->使用->卸載這個過程,如果需要反射的類沒有在內存中,那么首先會經過加載這個過程,并在在內存中生成一個class對象,有了這個class對象的引用,就可以發(fā)揮開發(fā)者的想象力,做自己想做的事情了。

反射的作用

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

● 需要訪問隱藏屬性或者調用方法改變程序原來的邏輯,這個在開發(fā)中很常見的,由于一些原因,系統(tǒng)并沒有開放一些接口出來,這個時候利用反射是一個有效的解決方法

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

●在開發(fā)中動態(tài)加載類,比如在Android中的動態(tài)加載解決65k問題等等,模塊化和插件化都離不開反射,離開了反射寸步難行。

反射的工作原理

我們知道,每個java文件最終都會被編譯成一個.class文件,這些Class對象承載了這個類的所有信息,包括父類、接口、構造函數(shù)、方法、屬性等,這些class文件在程序運行時會被ClassLoader加載到虛擬機中。當一個類被加載以后,Java虛擬機就會在內存中自動產生一個Class對象,而我們一般情況下用new來創(chuàng)建對象,實際上本質都是一樣的,只是這些底層原理對我們開發(fā)者透明罷了,我們前面說了,有了class對象的引用,就相當于有了Method,Field,Constructor的一切信息,在Java中,有了對象的引用就有了一切,剩下怎么發(fā)揮是開發(fā)者自己的想象力所能決定的了。

反射的簡單事例

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

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來修飾成員變量和方法 下面代碼用構造器,方法和屬性和靜態(tài)方法分別來獲取一下,

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,"小文","北京市海定區(qū)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);
        //調用靜態(tài)方法
        Method getTestMethod=clazz.getDeclaredMethod("getTest");
        getTestMethod.setAccessible(true);
        String result= (String) getTestMethod.invoke(null);
        System.out.println("調用靜態(tài)方法:"+result);
    }
}

結果如下:

反射練習.png

大家都看得懂,應該可以理解,有同學說不是有很多getDeclared和get的方法嗎, 實際上都差不多的,只不過用的范圍不一樣而已,getDeclared獲取的是僅限于本類的所有的不受訪問限制的,而get獲取的是包括父類的但僅限于public修飾符的,F(xiàn)ield和Method也是一樣的道理,這個大家注意一下就好,最后一個需要注意的是調用靜態(tài)方法和調用實例方法有點區(qū)別,調用實例方法一定需要一個類的實例,而調用靜態(tài)方法不需要實例的引用,其實這是JVM的在執(zhí)行方法上的有所區(qū)別,JVM在執(zhí)行方法的時候會創(chuàng)建一個堆棧,堆棧里面保存了局部變量表以及其他的一些必要的信息,其中局部變量表里面也包含了局部參數(shù),而局部參數(shù)里面保存了當前方法的形參,如果是調用實例方法的話,那么形參的第一個參數(shù)就是當前的類的引用了,而調用的是靜態(tài)方法的話,那么第一個參數(shù)是為null的,這一點無法通過任何手段去繞過,換句話說調用實例方法一定需要一個類的引用,關于這一點,讀者可以自己去查閱有關JVM的書籍。

當然了,反射的作用絕不止這些,在數(shù)組,泛型,設計模式等方面依然發(fā)揮了巨大的作用,但原理并沒有脫離上面說的,讀者可以多查看相關源碼學習,源碼就是最好的學習資源。

反射在Android框架層的應用

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

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

Activity.png

圖畫的有些粗糙,大家將就看吧,從上面可以看到,Activity從本地到遠程AMS以后,遠程AMS只是做了權限以及屬性的檢查,然后再回到本地進程,這才開始真正的創(chuàng)建和檢查,我們才代碼來分析一下,涉及到的類有Handler以及ActivityThread和Instrumentation類,首先從遠端進程回到本地進程之后,系統(tǒng)的Handler類H會發(fā)送一個消息: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里面終于創(chuàng)建了Activity了,進入performLaunchActivity里面看看有一段非常核心的代碼:

Activity activity = null;
        try {
        //
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            //通過mInstrumentation.newActivity()方法創(chuàng)建了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);
            }
        }

我們現(xiàn)在已經知道了Activity的創(chuàng)建了,是由Instrumentation的newActivity()方法實現(xiàn),我們看一下方法:

 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其實也是一個普通對象,也是由反射創(chuàng)建的,只不過由于加入了生命周期方法,才有組件這個活生生的對象存在, 所以說Android中反射無處不在,分析完了啟動和創(chuàng)建的過程,回到剛才那個需求來說,如何監(jiān)控Activity的啟動和創(chuàng)建呢?讀者可以先自己想一下,首先啟動是由Handler來發(fā)送消息,具體的在里面handlerMessage方法實現(xiàn)的, 這也是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類里面的處理接口,這樣就可以監(jiān)控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);
        //這里設置了我們自己實現(xiàn)了接口的CallBack對象
        mCallBack.set(handler, new UserHandler(handler));
    }
public class UserHandler  implements Callback {
    //這個100一般情況下最好也反射獲取,當然了你也可以直接寫死,跟系統(tǒng)的保持一致就好了
    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的啟動監(jiān)控就這樣了,一般寫在application里面的attachBaseContext()方法里面,因為這個方法時機最早。 好了,下面來說說Activity的創(chuàng)建的監(jiān)控,前面我們知道了,Instrumentation的newActivity方法負責創(chuàng)建了Activity,那么突破口也就是在這里了,創(chuàng)建為我們自定義的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);
        //替換掉原來的,就是把系統(tǒng)的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;
    }

    //重寫創(chuàng)建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還有其他方法可以重寫,大家可以去試一試,下面是運行的結果:

Handler.png

看到沒,監(jiān)控啟動和創(chuàng)建都實現(xiàn)了,其實這里面也有好多擴展的,比如啟動Activity的時候,Instrumentation一樣是可以監(jiān)控的,你懂的,再次重寫方法,然后實現(xiàn)自己的邏輯,另外,small插件化框架就是Hook了Instrumentation來動態(tài)加載Activity的,大家有興趣可以去看看,除了以上方法,還有很多方法可以用類似的手段去實現(xiàn),大家一定要多練習,好記性不如爛筆頭就是這個道理。

使用反射需要注意的地方

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

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

●大家都知道的Android開源代碼引起的兼容性的問題,這是Android系統(tǒng)開源的最大的問題,特別是那些第三方的ROM,要慎用。

●如果大量使用反射,在代碼上需要優(yōu)化封裝,不然不好管理,寫代碼不僅僅是實現(xiàn)功能,還有維護性和可讀性方法也需要加強,demo中可以直接這樣粗糙些,在項目中還是需要好好組織封裝下的。

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

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容