反射技術在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(熱修復),動態載入框架等等 一些基於外掛化的架構中。