Java代理設計模式的含義:
Java中的代理設計模式(Proxy),提供了對目標物件另外的訪問方式;即通過代理物件訪問目標物件.這樣做的好處是:可以在目標物件實現的基礎上,增強額外的功能操作,即擴充套件目標物件的功能.代理設計模式在現實生活中無處不在,舉個例子,一般明星都有個經紀人,這個經紀人的作用就是類似代理了,如果有誰想找明星,首先要通過經紀人,然後經紀人再轉告明星去做某些事情,這裡要注意的是:真正做事情的是明星本人,而不是經紀人,經紀人只是一箇中間的角色而已,本身並不會做事情,比如唱歌是明星來唱歌,而不是經紀人來唱歌,當然了,經紀人可以做些額外的事情,比如收取一定的服務費用,然後才允許你見明星之類,這個理解很重要,因為經紀人可以做些額外的事情,"這個額外的事情"就為程式中提供了很多擴充套件的功能,下面的例子可以看到這些額外的功能,再舉個例子,大家都買過房吧,一般情況下是由中介帶著你去,而這個中介也是類似代理人的作用,你也要明白,真正的買房人是你,你負責出錢,中介只是負責傳達你的要求而已,當然啦,一些額外的功能就是需要出點錢給中介,畢竟人家那麼辛苦,坑點茶水費還是要的,大家都懂,上次在買房也被坑過一些錢,後來想起了這個代理模式,也就理解了,哈哈,大家會發現,設計模式都是源於生活,在生活中處處都有設計模式的影子,我覺得學習設計模式一定要理解,理解,理解,重要的事情說三遍。
代理模式的分類
● 靜態代理,指的是在編譯的時候就已經存在了,需要定義介面或者父類,被代理物件與代理物件一起實現相同的介面或者是繼承相同父類,下面舉個例子,以買房為例子
首先定義一個購買的介面和方法
public interface Buy {
void buyHouse(long money);
}
複製程式碼
然後假設一個使用者去買房
public class User implements Buy {
@Override
public void buyHouse(long money) {
System.out.println("買房了,用了"+money+" 錢 ");
}
}
複製程式碼
我們先來執行一下:
public class ProxyClient {
public static void main(String[] args){
Buy buy=new User();
buy.buyHouse(1000000);
}
}
複製程式碼
測試結果為:
現在我們來弄一箇中介來幫我們買房,順便加上一些額外的功能,就是坑點我們的血汗錢,Fuck,萬惡的中介,開玩笑,程式碼如下
public class UserProxy implements Buy {
/**
*這個是真實物件,買房一定是真實物件來買的,中介只是跑腿的
*/
private Buy mBuy;
public UserProxy(Buy mBuy) {
this.mBuy = mBuy;
}
@Override
public void buyHouse(long money) {
long newMoney= (long) (money*0.99);
System.out.println("這裡坑點血汗錢,坑了我們:"+(money-newMoney)+"錢");
/**
* 這裡是我們出錢去買房,中介只是幫忙
*/
mBuy.buyHouse(newMoney);
}
}
public class ProxyClient {
public static void main(String[] args){
Buy buy=new User();
UserProxy proxy=new UserProxy(buy);
proxy.buyHouse(1000000);
}
}
複製程式碼
執行結果如下:
看到沒,中介幫我們跑腿買房,被坑了10000元,結果皆大歡喜,我們買到房了,而"額外功能""中介也賺了點辛苦費,大家都開心,這就是靜態代理的理解,可以看到,在編譯時就已經決定的了.
● 動態代理,顧名思義是動態的,Java中的動態一般指的是在執行時的狀態,是相對編譯時的靜態來區分,就是在執行時生成一個代理物件幫我們幹活,還是以買房為例子,如果靜態代理是我們在還沒有買房的時候(就是編譯的時候)預先找好的中介,那麼動態代理就是在買房過程中找的,注意:買房過程中說明是在買房這件事情的過程中,就是程式碼在執行的時候才找的一箇中介,Java中的動態代理要求必須實現一個介面,InvocationHandler,動態代理也必須有一個真實的物件,不管是什麼代理,只是幫忙傳達指令,最終還是必須有原來的物件去幹活的,下面是程式碼:
public class DynamiclProxy implements InvocationHandler {
//真正要買房的物件
private Buy mObject;
public DynamiclProxy(Buy mObject) {
this.mObject = mObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("buyHouse")){
//如果方法是買房的話,那麼先坑點錢
long money= (long) args[0];//取出第一個引數,因為我們是知道引數的
long newMoney= (long) (money*0.99);
System.out.println("坑了我們:"+(money-newMoney)+"錢");
args[0]=newMoney;//坑完再賦值給原來的引數
/**
* 呼叫真實物件去操作
*/
return method.invoke(mObject,args);
}
//如果有其他方法,也可以跟上面這樣判斷
return null;
}
}
複製程式碼
看到沒,都是需要一個真實物件的引用,然後在介面方法中做自己的邏輯,最後介面方法幫我們去實現自己的邏輯,我前面已經說過了,不管是什麼代理,都是真實的物件去做行為,代理只是幫忙做事情跑腿的, 測試程式碼:
public class ProxyClient {
public static void main(String[] args){
Buy buy=new User();
UserProxy proxy=new UserProxy(buy);
proxy.buyHouse(1000000);
System.out.println("動態代理測試");
Buy dynamicProxy= (Buy) Proxy.newProxyInstance(buy.getClass().getClassLoader(),
buy.getClass().getInterfaces(),new DynamiclProxy(buy));
dynamicProxy.buyHouse(1000000);
}
}
複製程式碼
下面是執行結果:
可以看到執行的結果跟靜態代理的是一樣的,順便提下,動態代理不僅在Java中有重要的作用,特別是AOP程式設計方面,更是在Android的外掛話發揮了不可或缺的作用,我們前面說過Java層的Hook一般有反射和動態代理2個方面,一般情況下是成對出現的,反射是負責找出隱藏的物件,而動態代理則是生成目標介面的代理物件,然後再由反射替換掉,一起完成有意思的事情,下面我們簡單來分析一下動態代理的內部原理實現:首先是生成class檔案,如下程式碼: public static void createProxyClassFile() {
String name = "ProxyClass";
byte[] data = ProxyGenerator.generateProxyClass(name, new Class[]{Buy.class});
try {
FileOutputStream out = new FileOutputStream(name + ".class");
out.write(data);
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
複製程式碼
在專案根目錄下面會有ProxyClass.class檔案生成,我們簡單分析下原始碼:
看到這裡的繼承關係,我想有些讀者已經看懂了一個動態代理的缺陷了,因為生成的類已經有一個Proxy父類了,因此註定了不是介面的不能使用動態代理,因為java的繼承機制所決定的,但通過第三方cglib可以實現,大家可以去試試,雖然有點遺憾,但不會影響其發揮的巨大作用,我們來看看是如何生成動態代理的子類,看方法:Proxy.newProxyInstance(),下面是程式碼: public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs);
/*
* Invoke its constructor with the designated invocation handler.
*/
try {
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
}
...省略了一些非關鍵程式碼
}
複製程式碼
可以看到,是通過反射建構函式來建立子類的,而建構函式裡面的引數constructorParams正是介面型別
/** parameter types of a proxy class constructor */
private static final Class<?>[] constructorParams =
{ InvocationHandler.class };
複製程式碼
下面我們分析一下是如何生成位元組碼的:
public static byte[] generateProxyClass(final String name,
Class[] interfaces)
{
ProxyGenerator gen = new ProxyGenerator(name, interfaces);
// 這裡是關鍵
final byte[] classFile = gen.generateClassFile();
if(saveGeneratedFiles) {
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
try {
int var1 = var0.lastIndexOf(46);
Path var2;
if(var1 > 0) {
Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar), new String[0]);
Files.createDirectories(var3, new FileAttribute[0]);
var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
} else {
var2 = Paths.get(var0 + ".class", new String[0]);
}
//這個是重點,寫在本地磁碟上
Files.write(var2, var4, new OpenOption[0]);
return null;
} catch (IOException var4x) {
throw new InternalError("I/O exception saving generated file: " + var4x);
}
}
});
// 返回代理類的位元組碼
return classFile;
}
//generateClassFile方法比較多,都是一些位元組碼的編寫
我們重點看下最終寫到哪裡去了
ByteArrayOutputStream var13 = new ByteArrayOutputStream();
DataOutputStream var14 = new DataOutputStream(var13);
try {
var14.writeInt(-889275714);
var14.writeShort(0);
var14.writeShort(49);
this.cp.write(var14);
var14.writeShort(this.accessFlags);
var14.writeShort(this.cp.getClass(dotToSlash(this.className)));
var14.writeShort(this.cp.getClass("java/lang/reflect/Proxy"));
var14.writeShort(this.interfaces.length);
Class[] var17 = this.interfaces;
int var18 = var17.length;
for(int var19 = 0; var19 < var18; ++var19) {
Class var22 = var17[var19];
var14.writeShort(this.cp.getClass(dotToSlash(var22.getName())));
}
var14.writeShort(this.fields.size());
var15 = this.fields.iterator();
while(var15.hasNext()) {
ProxyGenerator.FieldInfo var20 = (ProxyGenerator.FieldInfo)var15.next();
var20.write(var14);
}
var14.writeShort(this.methods.size());
var15 = this.methods.iterator();
while(var15.hasNext()) {
ProxyGenerator.MethodInfo var21 = (ProxyGenerator.MethodInfo)var15.next();
var21.write(var14);
}
var14.writeShort(0);
return var13.toByteArray();
} catch (IOException var9) {
throw new InternalError("unexpected I/O Exception", var9);
}
很明顯了,最終是寫在本地磁碟上來l
複製程式碼
現在我們知道了是寫到本地磁碟上的位元組碼,然後生成一個代理的物件,那麼是如何呼叫具體的方法呢,在剛才那個檔案ProxyClass.class告訴了我們,比如我們的介面方法:
public final void buyHouse(long var1) throws {
try {
super.h.invoke(this, m3, new Object[]{Long.valueOf(var1)});
} catch (RuntimeException | Error var4) {
throw var4;
} catch (Throwable var5) {
throw new UndeclaredThrowableException(var5);
}
}
複製程式碼
看到沒是h來呼叫的,那麼h從哪裡來的呢?建構函式,
//這個建構函式就是我們程式碼中的
new DynamiclProxy(buy))中buy就是這裡的引數var1,也就是真正需要代理的物件賦值給了父類Proxy.中的
protected InvocationHandler h;物件
public ProxyClass(InvocationHandler var1) throws {
super(var1);
}
這個類裡面的其他方法的執行都是這樣的流程走的,比如object類的HasCode方法等等
複製程式碼
看到這裡,我想基本明白了吧,動態代理就一句話:系統幫我們生成了位元組碼檔案儲存在本地並生成一個InvocationHandler的代理子類,然後通過我們傳進去的真實物件的引用,再幫忙呼叫各種介面方法,最終所有的方法都走
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
複製程式碼
從而我們可以在裡面根據實際情況做不同的業務處理,比如統計耗時,替換引數(這些就是所謂的額外的功能)等等,大家有時間可以去多看那些涉及到的原始碼就好。
● 除了上面提到的靜態代理和動態代理,還有一種代理稱為遠端代理的,這個在Android比較廣泛應用,特別是在IPC過程或者Binder機制中不可或缺,這種互動一般發生在不同的程式之間,所以一般稱為遠端代理模式。
代理模式在Android中的應用
我們前面說了,Java中代理模式一般三種,其中動態代理用的比較多,比較靈活,而且有時候由於介面是隱藏的,也不好用靜態代理,因此大多數時候都是用動態代理比較多,遠端代理一般在不同程式之間使用,這裡先簡單介紹一下遠端代理,比如我們熟悉的Activity的啟動過程,其實就隱藏了遠端代理的使用,由於APP本地程式和AMS(ActivityManagerService)程式分別屬於不同的程式,因此在APP程式內,所有的AMS的例項其實都是經過Binder驅動處理的代理而已,大家要明白,真正的例項只有一個的,就是在AMS程式以內,其他程式之外的都不過是經過Binder處理的代理傀儡而已,還是先拿出這個啟動圖看看:
可以看到APP程式和AMS程式之間可以相互呼叫,其實就是靠各自的遠端代理物件進行呼叫的,而不可能之間呼叫(程式隔離的存在) 就是APP本地程式有AMS的遠端代理ActivityManagerProxy,有了這個代理,就可以呼叫AMS的方法了,而AMS也一樣,有了ActivityThread的代理物件ApplicationThreadProxy,也可以呼叫APP本地程式的方法了,大家要明白,這些代理物件都是一個傀儡而已,只是Binder驅動處理之後的真實物件的引用,跟買房中介一樣的性質,實際上所有Binder機制中的所謂的獲取到的"遠端物件",都不過是遠端真實物件的代理物件,只不過這個過程是驅動處理,對我們透明而已,有興趣的同學可以去看看Native的原始碼,相信體會的更深.下面我們利用動態代理來有意義的事情,現在大家的專案中估計都有引入了好多個第三方的庫吧,大部分是遠端依賴的,有些引用庫會亂髮通知的,但是這些程式碼因為對我們不可見,為了方便對通知的統一管理,我們有必要對系統中的所有通知進行統一的控制,我們知道,通知是用NotificationManager來管理的,實際上這個不過是服務端物件在客戶端物件的一個代理物件的包裝, 也就是說最終的管理還是在遠端程式裡面,客戶端的作用只是包裝一下引數,通過Binder機制發到服務端進行處理而已,我們先看一下程式碼: private static INotificationManager sService;
/** @hide */
static public INotificationManager getService()
{
if (sService != null) {
return sService;
}
IBinder b = ServiceManager.getService("notification");
sService = INotificationManager.Stub.asInterface(b);
return sService;
}
複製程式碼
這是Binder機制的內容,首先ServiceManager通過 getService方法獲取了一個原生的裸的IBinder物件,然後通過AIDL機制的asInterface方法轉換成了本地的代理物件,而我們在通知中的所有的操作都是有這個sService發起的,當然了,sService也是什麼事情都幹不了,只是跑腿,包裝引數傳送給真正的遠端服務物件去做真正的事情,順便提一下,Android系統中的絕大多數服務都在以這樣形式而存在的,只有少數的比如AMS,PMS是以單列形式存在,因為AMS,PMS比較常用,按照常規的套路,先反射出sService欄位,然後我們利用動態代理生成一個偽造的sService物件替換掉,代替我們的工作,這樣所有的方法呼叫都會走動態代理的方法,這個我們前面已經說過了
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
複製程式碼
這樣的話,我們就可以通過選擇某些方法來做些自己想要的事情,比如判斷引數,然後選擇遮蔽之類,好了,我們寫一波程式碼先:
public static void hookNotificationManager(Context context) {
try {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Method method = notificationManager.getClass().getDeclaredMethod("getService");
method.setAccessible(true);
//獲取代理物件
final Object sService = method.invoke(notificationManager);
Log.d("[app]", "sService=" + sService);
Class<?> INotificationManagerClazz = Class.forName("android.app.INotificationManager");
Object proxy = Proxy.newProxyInstance(INotificationManagerClazz.getClassLoader(),
new Class[]{INotificationManagerClazz},new NotifictionProxy(sService));
//獲取原來的物件
Field mServiceField = notificationManager.getClass().getDeclaredField("sService");
mServiceField.setAccessible(true);
//替換
mServiceField.set(notificationManager, proxy);
Log.d("[app]", "Hook NoticeManager成功");
} catch (Exception e) {
e.printStackTrace();
}
}
public class NotifictionProxy implements InvocationHandler {
private Object mObject;
public NotifictionProxy(Object mObject) {
this.mObject = mObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d("[app]", "方法為:" + method.getName());
/**
* 做一些業務上的判斷
* 這裡以傳送通知為準,傳送通知最終的呼叫了enqueueNotificationWithTag
*/
if (method.getName().equals("enqueueNotificationWithTag")) {
//具體的邏輯
for (int i = 0; i < args.length; i++) {
if (args[i]!=null){
Log.d("[app]", "引數為:" + args[i].toString());
}
}
//做些其他事情,然後替換引數之類
return method.invoke(mObject, args);
}
return null;
}
}
複製程式碼
好了,我們在Application裡面的attachBaseContext()方法裡面注入就好,為什麼要在這裡注入呢,因為attachBaseContext()在四大元件中的方法是最先執行的,比ContentProvider的onCreate()方法都先執行,而ContrentProvider的onCreate()方法比Application的onCreate()都先執行,大家可以去測試一下,因此如果Hook的地方是涉及到ContentProvider的話,那麼最好在這個地方執行,我們在頁面傳送通知試試:,程式碼如下:
Intent intent=new Intent();
Notification build = new NotificationCompat.Builder(MotionActivity.this)
.setContentTitle("測試通知")
.setContentText("測試通知內容")
.setAutoCancel(true)
.setDefaults(Notification.DEFAULT_SOUND|Notification.DEFAULT_VIBRATE)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setSmallIcon(R.mipmap.ic_launcher)
.setWhen(System.currentTimeMillis())
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentIntent(PendingIntent.getService(MotionActivity.this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.build();
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify((int) (System.currentTimeMillis()/1000L), build);
複製程式碼
好了,我們看一下結果吧:
看到結果了吧,已經成功檢測到被Hook的方法了,而具體如何執行就看具體的業務了。至此Java中的常用Hook手段:反射和動態代理就到此為止了,但實際上他們還有很多地方值得去使用,研究,只是限於篇幅,不在一一說明,以後如果有涉及到這方面的會再次提起的,大家有空可以研究原始碼,還是那句話,原始碼就是最好的學習資料
實際上Android上的很多服務都可以用類似的手段去處理,除了在Hook之外的應用外,在動態代理裡面也有廣泛的應用的,在以後寫效能優化的時候會提出來的,感謝大家閱讀,歡迎提出改進意見,不甚感謝。