1. 程式人生 > >反射技術在android中的應用

反射技術在android中的應用

動態語言:

一般認為在程式執行時,允許改變程式結構或變數型別,這種語言稱為動態語言。從這個觀點看,Perl,Python,Ruby是動態語言,C++,Java,C#不是動態語言。儘管這樣,JAVA有著一個非常突出的動態相關機制:反射(Reflection)。運用反射我們可以於執行時載入、探知、使用編譯期間完全未知的classes。換句話說,Java程式可以載入在執行時才得知名稱的class,獲悉其完整構造方法,並生成其物件實體、或對其屬性設值、或喚起其成員方法。

反射:

要讓Java程式能夠執行,就得讓Java類被Java虛擬機器載入。Java類如果不被Java虛擬機器載入就不能正常執行。正常情況下,我們執行的所有的程式在編譯期時候就已經把那個類被載入了。 Java的反射機制是在編譯時並不確定是哪個類被載入了,而是在程式執行的時候才載入。使用的是在編譯期並不知道的類。這樣的編譯特點就是java反射。

反射的作用:

如果有AB兩個程式設計師合作,A在寫程式的時需要使用B所寫的類,但B並沒完成他所寫的類。那麼A的程式碼是不能通過編譯的。此時,利用Java反射的機制,就可以讓A在沒有得到B所寫的類的時候,來使自身的程式碼通過編譯。

反射的實質:

反射就是把Java類中的各種存在給解析成相應的Java類。要正確使用Java反射機制就得使用Class(C大寫) 這個類。它是Java反射機制的起源。當一個類被載入以後,Java虛擬機器就會自動產生一個Class物件。通過這個Class物件我們就能獲得載入到虛擬機器當中這個Class物件對應的方法、成員以及構造方法的宣告和定義等資訊。

反射機制的優點與缺點:

為什麼要用反射機制?直接建立物件不就可以了嗎,這就涉及到了動態與靜態的概念:
靜態編譯:在編譯時確定型別,繫結物件,即通過。
動態編譯:執行時確定型別,繫結物件。動態編譯最大限度發揮了java的靈活性,體現了多型的應用,降低類之間的藕合性。
一句話,反射機制的優點就是可以實現動態建立物件和編譯,體現出很大的靈活性,特別是在J2EE的開發中它的靈活性就表現的十分明顯。比如,一個大型的軟體,不可能一次就把把它設計的很完美,當這個程式編譯後,釋出了,當發現需要更新某些功能時,我們不可能要使用者把以前的解除安裝,再重新安裝新的版本,假如這樣的話,這個軟體肯定是沒有多少人用的。採用靜態的話,需要把整個程式重新編譯一次才可以實現功能的更新,而採用反射機制的話,它就可以不用安裝,只需要在執行時才動態的建立和編譯,就可以實現該功能。它的缺點是對效能有影響。使用反射基本上是一種解釋操作,我們可以告訴JVM,我們希望做什麼並且它滿足我們的要求。這類操作總是慢於只直接執行相同的操作。

Android FrameWork中的反射:

一個類中的每個成員都可以用相應的反射API的一個例項物件來表示——反射機制。
瞭解這些,那我們就知道了,我們可以利用反射機制在Java程式中,動態的去呼叫一些protected甚至是private的方法或類,這樣可以很大程度上滿足我們的一些比較特殊需求。例如Activity的啟動過程中Activity的物件的建立。

以下程式碼位於ActivityThread中:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

        。。。。。。
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            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);
            }
        } 
        。。。。。。

上面程式碼可知Activity在建立物件的時候呼叫了mInstrumentation.newActivity();
以下程式碼位於Instrumentation中:

    public Activity newActivity(ClassLoader cl, String className,
            Intent intent)
            throws InstantiationException, IllegalAccessException,
            ClassNotFoundException {
            //這裡的className就是在manifest中註冊的Activity name.
        return (Activity)cl.loadClass(className).newInstance();
    }

最終在newActivity()裡返回的是利用cl.loadClass返回的Activity物件。可知,Activity物件的建立是通過反射完成的。java程式可以動態載入類定義,而這個動態載入的機制就是通過ClassLoader來實現的,所以可想而知ClassLoader的重要性如何。

ClassLoader和DexClassLoader

上面說到JAVA的動態載入的機制就是通過ClassLoader來實現的,ClassLoader也是實現反射的基石。ClassLoader是JAVA提供的一個類,顧名思義,它就是用來載入Class檔案到JVM,以供程式使用的。

但是問題來了,ClassLoader載入檔案到JVM,但是Android是基於DVM的,用ClassLoader載入檔案進DVM肯定是不行的。於是Android提供了另外一套載入機制,分別為 dalvik.system.DexClassLoader 和 dalvik.system.PathClassLoader,區別在於 PathClassLoader 不能直接從 zip 包中得到 dex,因此只支援直接操作 dex 檔案或者已經安裝過的 apk(因為安裝過的 apk 在 cache 中存在快取的 dex 檔案)。而 DexClassLoader 可以載入外部的 apk、jar 或 dex檔案,並且會在指定的 outpath 路徑存放其 dex 檔案。

ClassLoader在JAVA中的應用

下面利用反射來呼叫另一個類中的方法

//定義一個測試類,用來被反射呼叫
package com.izzy;
public class Test {
    private String s;
    //構造方法
    public Test(String s) {
        this.s = s;
    }
    //定義一個方法,用來輸出,構造方法中傳遞進來的引數
    public void display() {
        System.out.println(s);
    }

}
package com.izzy;
import java.lang.reflect.Constructor;

public class Client {

    public static void main(String[] s) {
        try {
        //首先拿到系統ClassLoader,並載入Class,返回的是一個Class物件clazz 
            Class clazz = ClassLoader.getSystemClassLoader().loadClass(
                    "com.izzy.Test");
        //通過clazz 拿到構造方法並轉換成物件
            Constructor constructor = clazz.getConstructor(String.class);
            Object obj = constructor.newInstance("I AM IZZY");
        //通過clazz 拿到成員方法
            Method method = clazz.getMethod("display", null);
            method.invoke(obj, null);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

這裡寫圖片描述

DexClassLoader在Android中的應用

以一個例子來說明DexClassLoader用法(本例採用兩個已安裝的Apk),現在有兩個Apk:Share和Test,利用Test來呼叫Share 裡面的方法。

首先在Share apk中定義Share類,其中有一個display()方法提供給遠端呼叫

public class Share {
    public void display(String s) {
        Log.e("IZZY", s);
    }

}

接著在manifest檔案中配置action和category,方便呼叫這找到

  <activity
            android:name=".MainActivity"
            android:theme="@android:style/Theme.Light.NoTitleBar">
            <intent-filter>
                <action android:name="com.IZZY"/>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

接下來就是在Test Apk中編寫呼叫程式碼了

 public void getFromRemote() {
        Intent intent = new Intent("com.IZZY");
        PackageManager pm = getPackageManager();
        List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, 0);

        ResolveInfo resolveInfo = resolveInfos.get(0);
        ActivityInfo activityInfo = resolveInfo.activityInfo;
        //拿到目標類的包名
        String packageName = activityInfo.packageName;

        //拿到目標類所在的apk或者jar存放的路徑
        String dexPath = activityInfo.applicationInfo.sourceDir;
        //該路徑為拿到目標類dex檔案存放在呼叫者裡的路徑
        String dexOutputDir = getApplicationInfo().dataDir;
        //拿到目標類所使用的C/C++庫存放路徑
        String nativeLibraryDir = activityInfo.applicationInfo.nativeLibraryDir;
        //拿到類裝載器
        ClassLoader classLoader = getClassLoader();

        //DexClassLoader引數分別對應以上四個引數
        DexClassLoader dcl = new DexClassLoader(dexPath,dexOutputDir,nativeLibraryDir,classLoader);
        try {
            //裝載目標類
            Class<?> clazz = dcl.loadClass(packageName + ".Share");
            //拿到構造器並例項化物件
            Constructor<?> constructor = clazz.getConstructor();
            Object o = constructor.newInstance();
            //拿到成員方法
            Method display = clazz.getMethod("display", String.class);
            display.invoke(o, "I AM IZZY");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

在該呼叫方法中首先利用Intent查詢到目標activityInfo,然後利用查詢到的activityInfo得到目標Apk的包名,目標Apk所在的apk或者jar存放的路徑dexPath,目標Apk所使用的C/C++庫存放路徑nativeLibraryDir。然後利用這些引數例項化DexClassLoader載入器。之後反射呼叫目標類中的方法。
(ps:此處使用的目標Apk是已經安裝過的,因此採用Intent查詢來拿到dexPath和nativeLibraryDir,如果是未安裝過的jar包或Apk,則直接傳入該jar包活Apk的檔案存放路徑和C/C++庫存放路徑)。


執行結果:
這裡寫圖片描述
從結果看,呼叫者Apk拿到了目標Apk的方法併成功執行。

DexClassLoaderde 在Android中的使用場景

上面是是使用的已經安裝過的Apk,如果採用未安裝過的jar包或者Apk,則例項化DexClassLoader的時候把相應路徑改為需要載入的jar包或者Apk路徑亦可拿到結果。這就使得DexClassLoaderde可以應用在HotFix(熱修復),動態載入框架等等 一些基於外掛化的架構中。