Java代理以及在Android中的一些簡單應用

stormWen發表於2017-12-12

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);
    }
}
複製程式碼

測試結果為:

Java代理以及在Android中的一些簡單應用

現在我們來弄一箇中介來幫我們買房,順便加上一些額外的功能,就是坑點我們的血汗錢,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);
    }
}
複製程式碼

執行結果如下:

Java代理以及在Android中的一些簡單應用

看到沒,中介幫我們跑腿買房,被坑了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代理以及在Android中的一些簡單應用
可以看到執行的結果跟靜態代理的是一樣的,順便提下,動態代理不僅在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檔案生成,我們簡單分析下原始碼:

Java代理以及在Android中的一些簡單應用
看到這裡的繼承關係,我想有些讀者已經看懂了一個動態代理的缺陷了,因為生成的類已經有一個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處理的代理傀儡而已,還是先拿出這個啟動圖看看:

Java代理以及在Android中的一些簡單應用
可以看到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);
複製程式碼

好了,我們看一下結果吧:

Java代理以及在Android中的一些簡單應用

看到結果了吧,已經成功檢測到被Hook的方法了,而具體如何執行就看具體的業務了。至此Java中的常用Hook手段:反射和動態代理就到此為止了,但實際上他們還有很多地方值得去使用,研究,只是限於篇幅,不在一一說明,以後如果有涉及到這方面的會再次提起的,大家有空可以研究原始碼,還是那句話,原始碼就是最好的學習資料

實際上Android上的很多服務都可以用類似的手段去處理,除了在Hook之外的應用外,在動態代理裡面也有廣泛的應用的,在以後寫效能優化的時候會提出來的,感謝大家閱讀,歡迎提出改進意見,不甚感謝。

相關文章