代理模式看這一篇就夠了~

語言: CN / TW / HK

前言

不知各位是否還記得這兩篇文章APP啟動流程解析Android Hook告訴你 如何啟動未註冊的Activity,這兩篇文章中使用的技術基礎都包含了 代理模式,其中在文章中也說道 “說到代理其實就是代理模式,關於什麼是代理模式以及動態代理和靜態代理的使用可以持續關注我,後面會單獨寫篇文章進行介紹。”

如今整整一年過去了,我還是曾經那個少年,沒有一絲絲改變。 這篇文章來了~

什麼是代理模式

說到設計模式,離我們特別遠,又特別近。

問許多工程師,設計模式用過哪些,相信很多人都會說 單例模式、工廠模式等等,但是很少有人提及橋接模式、門面模式、直譯器模式等 甚至都沒有聽說過。

代理模式 是 結構型模式之一,主要是將類和物件結合在一起解決特定的應用場景問題。對於Android工程師來說,我覺得了解並掌握代理模式是必要的,因為了解Android Hook、AMS代理等外掛化技術,是離不開代理模式的,這也是我一直覺得要有這篇文章的原因,如果你還不瞭解代理模式對Android開發者有什麼用途,可移步至前言的兩篇文章。

使用代理模式

代理模式簡單的說就是可以在不改被代理類程式碼的情況下,通過引入代理類來擴充套件功能。比如我們現在有一個登入註冊類LoginAndRegist.java 和一個登入方法 一個註冊方法。

public void login(String userName){
        System.out.println("我是登入方法");
    }

    public void reist(String userName){
        System.out.println("我是註冊方法");
    }
複製程式碼

同時我們新建一個Test類來測試方法

public class Test {

    public static void main(String[] args) {
        LoginAndRegist loginAndRegist = new LoginAndRegist();
        loginAndRegist.reist("huanglinqing");
        loginAndRegist.login("huanglinqing");
    }
}
複製程式碼

執行Test.main 列印如下所示:

我是註冊方法
我是登入方法

Process finished with exit code 0
複製程式碼

那麼我們現在有需求,為登入和註冊新增相關日誌,我們該怎麼來實現呢,你可能說了,這不簡單嗎,直接在login 和 regist方法中 直接再加兩行列印不就行了嗎?

可以是可以 但是隨著系統的龐大,你會越來越痛苦

第一 LoginAndRegist類不是你寫的,難道要讓各自負責人去修改自己的程式碼嗎

第二 新增日誌 是一個日誌系統 是一個獨立的系統,不應該和業務摻雜在一起

第三 如果要新增日誌的類 是jar包中的呢

第******

靜態代理

那麼我們該如何實現上面的功能呢,我們以 為登入註冊方法 新增 時間日誌為例,首先 有經驗的工程師在寫程式碼的時候,就應該知道,我們要遵循基於介面而非實現的設計原則,所以我們應把login 和 regist 抽取出來,讓LoginAndRegist 類 繼承蓋介面,如下所示:

public interface UserInter {

    /**
     * 登入
     * @param name name
     */
    void login(String name);

    /**
     * 註冊
     * @param name name
     */
    void regist(String name);
}
複製程式碼

public class LoginAndRegist implements UserInter {

    private static final String TAG = "Login";

    @Override
    public void login(String userName) {
        System.out.println("我是登入方法");
    }

    @Override
    public void regist(String name) {
        System.out.println("我是註冊方法");
    }

}
複製程式碼

為LoginAndRegist 類 建立代理類 LoginAndRegistProxy,讓代理類 實現 和 原始類同樣的介面,並呼叫原始類的方法 ,並在呼叫前 列印當前時間戳,程式碼如下所示:

public class LoginAndRegistProxy implements UserInter {

    private static final String TAG = "Login";

    private UserInter userInter;

    public LoginAndRegistProxy(UserInter userInter) {
        this.userInter = userInter;
    }

    @Override
    public void login(String userName) {
        System.out.println("呼叫登入介面的時間:" + System.currentTimeMillis());
        userInter.login(userName);
    }

    @Override
    public void regist(String name) {
        System.out.println("呼叫註冊介面的時間:" + System.currentTimeMillis());
        userInter.login(name);
    }

}
複製程式碼

在Test中修改呼叫方法為代理類呼叫:

 public static void main(String[] args) {
        LoginAndRegist loginAndRegist = new LoginAndRegist();
        LoginAndRegistProxy loginAndRegistProxy = new LoginAndRegistProxy(loginAndRegist);
        loginAndRegistProxy.regist("huanglinqing");
        loginAndRegistProxy.login("huanglinqing");
    }
複製程式碼

執行結果 如下所示:

呼叫註冊介面的時間:1596793891141
我是登入方法
呼叫登入介面的時間:1596793891141
我是登入方法

Process finished with exit code 0
複製程式碼

這樣呢,我們就實現了不改變原始的情況下,為類方法新增日誌的功能,但是呢,這種方法存在的問題 我們上面也提到了

第一 如果原始類 沒有實現介面怎麼辦

第二 如果原始類的原始碼 我們獲取不到怎麼辦

對於原始類 並沒有實現介面,並且我們無法修改的情況下,這種我們稱為對外部類的擴充套件,外部類的擴充套件我們一般使用繼承的方式去擴充套件,這種方式我們就不解釋了。

第三 如果為每個類都新增代理類,會增加大量的檔案

第三個問題才是我們實際開發中需要首要解決的問題,所以 為了解決靜態代理檔案過多的的問題,我們需要使用動態代理。

動態代理

動態代理簡單的說 就是我們不需要事先為某個原始類編寫代理類,而是在執行的時候,動態的建立代理類,然後將原始類替換為代理類。動態代理的魅力在Android中真的是非常非常大,如果你還不瞭解,一定要回頭看我前言中提到的兩篇文章。而在java中動態代理的基礎是反射,如果你還不瞭解反射技術,請移步至我的這篇文章Java反射技術詳解

動態代理,我們主要依賴的是newProxyInstance方法,該方法返回的是指定介面代理類的例項。

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
複製程式碼

第一個引數loader 指的是目標物件class對應的classLoader

第二個引數interfaces是設定為物件class所實現的介面型別,第一個引數和第二個引數其實在業務上都是固定的,在這裡就是UserInter對應的的classLoader和介面型別。

我們主要來看第三個引數 InvocationHandler,它是一個實現了InvocationHandler介面的類物件

首先我們來定義一個MyInvocationHandler實現InvocationHandler

public class MyInvocationHandler implements java.lang.reflect.InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       return null;
    }
}
複製程式碼

這裡實現的invoke方法就是動態代理的核心,此外我們需要將代理類傳進來,並在invoke方法中執行代理方法

public class MyInvocationHandler implements java.lang.reflect.InvocationHandler {

    private Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("開始執行前:" + method.getName());
        Object object = method.invoke(target, args);
        System.out.println("執行結束:" + method.getName());
        return object;
    }
}
複製程式碼

當外部代理類呼叫某個方法的時候,其實就是在呼叫invoke中的method.invoke方法,args就是執行對應方法所需的引數,我們這裡 在方法前後分別加上日誌。

上面我們已經說了,動態代理是通過newProxyInstance方法建立的,我們來看Test中如何使用

  public static void main(String[] args) {
        UserInter loginAndRegist = new LoginAndRegist();
        UserInter loginAndRegistProxy = (UserInter) Proxy.newProxyInstance(loginAndRegist.getClass().getClassLoader(),
                loginAndRegist.getClass().getInterfaces(),new MyInvocationHandler(loginAndRegist));
        loginAndRegistProxy.regist("huang_動態代理");
        loginAndRegistProxy.login("huang_靜態代理");
    }
複製程式碼

newProxyInstance中的引數我們在上面已經說明了,執行結果如下所示:

開始執行前:regist
我是註冊方法
執行結束:regist
開始執行前:login
我是登入方法
執行結束:login

Process finished with exit code 0
複製程式碼

如此我們就通過動態代理,給所有類的方法 統一新增日誌了。

在Android中我們用Proxy.newProxyInstance生成的物件,直接替換掉原來的物件,這個技術就是聽起來很高大上的Hook技術。

此外Spring AOP 底層的實現原理就是基於動態代理。

代理模式還有哪些應用場景

如果我們想要很好的應用代理模式,我們需要了解代理模式的應用場景有哪些

業務系統非功能性需求

在業務系統中一些非功能性需求,比如:監控、統計、鑑權、事務、日誌等。我們將這些附加功能與業務功能解耦,放到代理類中統一處理,這樣可以與業務解耦並且做到職責明確劃分。

除此之外代理模式還在RPC技術、Android Hook、外掛化等技術領域有著廣泛的應用。

現在你是否對代理模式有清晰的瞭解了呢?