動態代理分析與仿Retrofit實踐

0_D0LicO5QLRBAfpsz.jpeg

咱們一直都在使用Retroift,都知道它的核心是動態代理。例如在以前的文章重溫Retrofit源碼,笑看協程實現中也簡單說起到動態代理(來填以前挖的坑...)。java

咳咳,你們不要關注原由,仍是要回歸當前的內容。android

此次主要是來分析一下動態代理的做用與實現原理。既然都已經分析了原理,最後天然也要動手仿照Retrofit來簡單實現一個Demogit

經過最後的Demo實現,相信動態代理你也基本沒什麼問題了。github

靜態代理

既然說到動態代理,天然少不了靜態代理。那麼靜態代理究竟是什麼呢?咱們仍是經過一個簡單的場景來了解。算法

假設有一個Bird接口來表明鳥的一些特性,例如fly飛行特性編程

interface Bird {
    fun fly()
}

如今分別有麻雀、老鷹等動物,由於它們都是鳥類,因此都會實現Bird接口,內部實現本身的fly邏輯。api

// 麻雀
class Sparrow : Bird {
    override fun fly() {
        println("Sparrow: is fly.")
        Thread.sleep(1000)
    }
}
// 老鷹
class Eagle : Bird {
    override fun fly() {
        println("Eagle: is fly.")
        Thread.sleep(2000)
    }
}

麻雀與老鷹的飛行能力都實現了,如今有個需求:須要分別統計麻雀與老鷹飛行的時長。數組

你會怎麼作呢?相信在咱們剛學習編程的時候都會想到的是:這還不簡單直接在麻雀與老鷹的fly方法中分別統計就能夠了。網絡

若是實現的鳥類種類很少的話,這種實現不會有太大的問題,可是一旦實現的鳥類種類不少,那麼這種方法重複作的邏輯將會不少,由於咱們要到每一種鳥類的fly方法中都去添加統計時長的邏輯。架構

因此爲了解決這種無心義的重複邏輯,咱們能夠經過一個ProxyBird來代理實現時長的統計。

class BirdProxy(private val bird: Bird) : Bird {
    override fun fly() {
        println("BirdProxy: fly start.")
        val start = System.currentTimeMillis() / 1000
        bird.fly()
        println("BirdProxy: fly end and cost time => ${System.currentTimeMillis() / 1000 - start}s")
    }
}

ProxyBird實現了Bird接口,同時接受了外部傳進來的實現Bird接口的對象。當調用ProxyBirdfly方法時,間接調用了傳進來的對象的fly方法,同時還進行來時長的統計。

class Main {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            ProxyBird(Sparrow()).fly()
            println()
            ProxyBird(Eagle()).fly()
        }

    }
}

最後輸出以下:

ProxyBird: fly start.
Sparrow: is fly.
ProxyBird: fly end and cost time => 1s
 
ProxyBird: fly start.
Eagle: is fly.
ProxyBird: fly end and cost time => 2s

上面這種模式就是靜態代理,可能有許多讀者都已經不知覺的使用到了這種方法,只是本身沒有意識到這是靜態代理。

那它的好處是什麼呢?

經過上面的例子,很天然的可以體會到靜態代理主要幫咱們解決的問題是:

  1. 減小重複邏輯的編寫,提供統一的便捷處理入口。
  2. 封裝實現細節。

動態代理

既然已經有了靜態代理,爲何又要來一個動態代理呢?

任何東西的產生都是有它的必要性的,都是爲了解決前者不能解決的問題。

因此動態代理就是來解決靜態代理所不能解決的問題,亦或者是它的缺點。

假設咱們如今要爲Bird新增一種特性:chirp鳥叫。

那麼基於前面的靜態代理,須要作些什麼改變呢?

  1. 修改Bird接口,新增chirp方法。
  2. 分別修改SparrowEagle,爲它們新增chirp的具體實現。
  3. 修改ProxyBird,實現chirp代理方法。

一、3還好,尤爲是2,一旦實現Bird接口的鳥類種類不少的話,將會很是繁瑣,這時就真的是牽一髮動全身了。

這仍是改動現有的Bird接口,可能你還須要新增另一種接口,例如Fish魚,實現有關魚的特性。

這時又要從新生成一個新的代理ProxyFish來管理有關魚的代理。

因此從這一點,咱們能夠發現靜態代理的機動性不好,對於那些實現了以後不怎麼改變的功能,能夠考慮使用它來實現,這也徹底符合它的名字中的靜態的特性。

那麼這種狀況動態代理就可以解決嗎?別急,可否解決接着往下看。

接着上面,咱們爲Bird新增chirp方法

interface Bird {
    fun fly()
    
    fun chirp()
}

而後再經過動態代理的方式來實現這個接口

class Main {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val proxy = (Proxy.newProxyInstance(this::class.java.classLoader, arrayOf(Bird::class.java), InvocationHandler { proxy, method, args ->
                if (method.name == "fly") {
                    println("calling fly.")
                } else if (method.name == "chirp") {
                    println("calling chirp.")
                }
            }) as Bird)
            
            proxy.fly()
            proxy.chirp()
        }
    }
}

輸出以下:

calling fly.
calling chirp.

方式很簡單,經過Proxy.newProxyInstance靜態方法來建立一個實現Bird接口的代理。該方法主要有三個參數分別爲:

  1. ClassLoader: 生成代理類的類類加載器。
  2. interface 接口Class數組: 對應的接口Class。
  3. InvocationHandler: InvocationHandler對象,全部代理方法的回調。

這裏關鍵點是第三個參數,全部經過調用代理類的代理方法都會在InvocationHandler對象中經過它的invoke方法進行回調

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

這就是上面將判斷調用具體接口方法的邏輯寫在InvocationHandler對象的invoke方法的緣由。

那它究竟是如何實現的呢?怎麼就成了一個代理類呢?我也沒看到代理類在哪啊?怎麼就全部調用都經過InvocationHandler的呢?

有這些疑問很正常,開始接觸動態代理時都會有這些疑問。致使這些疑問的直接緣由是咱們不能直接看到所謂的代理類。由於動態代理是在運行時生成代理類的,因此不像在編譯時期同樣可以直接看到源碼。

那麼下面目標就很明確了,解決看不到源碼的問題。

既然是運行時生成的,那麼在運行的時候將生成的代理類寫到本地目錄下不就能夠了嗎?至於如何寫Proxy已經提供了ProxyGenerator。它的generateProxyClass方法可以幫助咱們獲得生成的代理類。

class Main {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val byte = ProxyGenerator.generateProxyClass("\$Proxy0", arrayOf(Bird::class.java))
            FileOutputStream("/Users/{path}/Downloads/\$Proxy0.class").apply {
                write(byte)
                flush()
                close()
            }
        }
    }
}

運行上面的代碼就會在Downloads目錄下找到$Proxy0.class文件,將其直接拖到編譯器中,打開後的具體代碼以下:

public final class $Proxy0 extends Proxy implements Bird {
    private static Method m1;
    private static Method m4;
    private static Method m2;
    private static Method m3;
    private static Method m0;
 
    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }
 
    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }
 
    public final void fly() throws  {
        try {
            super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
 
    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
 
    public final void chirp() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
 
    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
 
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m4 = Class.forName("com.daily.algothrim.Bird").getMethod("fly");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("com.daily.algothrim.Bird").getMethod("chirp");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

首先$Proxy0繼承了Proxy同時實現了咱們熟悉的Bird接口;而後在它的構造方法中接受了一個var1參數,它的類型是InvocationHandler。繼續看方法,實現了類的默認三個方法equalstoStringhashCode,同時也找到了咱們須要的flychirp方法。

例如fly方法,調用了

super.h.invoke(this, m4, (Object[])null)

這裏的h就是以前的var1,即InvocationHandler對象。

到這裏迷霧已經揭曉了,調用invoke方法,同時將代理類的自身this、對應的method信息與方法參數傳遞過去。

因此咱們只須要在動態代理的最後一個參數InvocationHandlerinvoke方法中進行處理不一樣代理方法的相關邏輯。這樣作的好處是,無論你如何新增與刪除Bird中的接口方法,我都只要調整invoke的處理邏輯便可,將改動的範圍縮小到最小化。

這就是動態代理的好處之一(另外一個主要的好處天然是減小代理類的書寫)。

Android中運用動態代理的典型非Retrofit莫屬。因爲是一個網絡框架,一個App對於網絡請求來講接口天然是隨着App的迭代不斷增長的。對於這種變化頻繁的狀況,Retrofit使用動態代理爲入口,暴露出一個對應的Service接口,而相關的接口請求方法都在Service中進行定義。因此咱們每新增一個接口,都不須要作過多的別的修改,相關的網絡請求邏輯都封裝到動態代理的invoke方法中,固然Retrofit原理是藉助添加Annomation註解的方式來解析不一樣網絡請求的方式與相關的參數邏輯。最終再將解析的數據進行封裝傳遞給下層的OKHttp

因此Retrofit的核心就是動態代理與註解的解析。

這篇文章的原理解析部分就完成了,最後既然分析了動態代理與Retrofit的關係,我這裏提供了一個Demo來鞏固一下動態代理,同時借鑑Retroift的一些思想對一個簡易版的打點系統進行上層封裝。

Demo

Demo是一個簡單的模擬打點系統,經過定義Statistic類來建立動態代理,暴露Service接口,具體以下:

class Statistic private constructor() {
 
    companion object {
        @JvmStatic
        val instance by lazy { Statistic() }
    }
 
    @Suppress("UNCHECKED_CAST")
    fun <T> create(service: Class<T>): T {
        return Proxy.newProxyInstance(service.classLoader, arrayOf(service)) { proxy, method, args ->
            return@newProxyInstance LoadService(method).invoke(args)
        } as T
    }

}

經過入口傳進來的Service接口,從而建立對應的動態代理類,而後將對Service接口中的方法調用的邏輯處理都封裝到了LoadServiceinvoke方法中。固然Statistic也藉助了註解來解析不一樣的打點類型事件。

例如,咱們須要分別對ButtonText進行點擊與展現打點統計。

首先咱們能夠以下定義對應的Service接口,這裏命名爲StatisticService

interface StatisticService {
 
    @Scan(ProxyActivity.PAGE_NAME)
    fun buttonScan(@Content(StatisticTrack.Parameter.NAME) name: String)
 
    @Click(ProxyActivity.PAGE_NAME)
    fun buttonClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long)
 
    @Scan(ProxyActivity.PAGE_NAME)
    fun textScan(@Content(StatisticTrack.Parameter.NAME) name: String)
 
    @Click(ProxyActivity.PAGE_NAME)
    fun textClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long)
}

而後再經過Statistic來獲取動態代理的代理類對象

private val mStatisticService = Statistic.instance.create(StatisticService::class.java)

有了對應的代理類對象,剩下的就是在對應的位置直接調用。

class ProxyActivity : AppCompatActivity() {
 
    private val mStatisticService = Statistic.instance.create(StatisticService::class.java)
 
    companion object {
        private const val BUTTON = "statistic_button"
        private const val TEXT = "statistic_text"
        const val PAGE_NAME = "ProxyActivity"
    }
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val extraData = getExtraData()
        setContentView(extraData.layoutId)
        title = extraData.title

        // statistic scan
        mStatisticService.buttonScan(BUTTON)
        mStatisticService.textScan(TEXT)
    }

    private fun getExtraData(): MainModel =
            intent?.extras?.getParcelable(ActivityUtils.EXTRA_DATA)
                    ?: throw NullPointerException("intent or extras is null")

    fun onClick(view: View) {
        // statistic click
        if (view.id == R.id.button) {
            mStatisticService.buttonClick(BUTTON, System.currentTimeMillis() / 1000)
        } else if (view.id == R.id.text) {
            mStatisticService.textClick(TEXT, System.currentTimeMillis() / 1000)
        }
    }
}

這樣一個簡單的打點上層邏輯封裝就完成了。因爲篇幅有限(懶...)內部具體的實現邏輯就不展開了。

相關源碼都在android-api-analysis項目中,感興趣的能夠自行查看。

使用前請先把分支切換到 feat_proxy_dev

項目

android_startup: 提供一種在應用啓動時可以更加簡單、高效的方式來初始化組件,優化啓動速度。不只支持Jetpack App Startup的所有功能,還提供額外的同步與異步等待、線程控制與多進程支持等功能。

AwesomeGithub: 基於Github客戶端,純練習項目,支持組件化開發,支持帳戶密碼與認證登錄。使用Kotlin語言進行開發,項目架構是基於Jetpack&DataBindingMVVM;項目中使用了ArouterRetrofitCoroutineGlideDaggerHilt等流行開源技術。

flutter_github: 基於Flutter的跨平臺版本Github客戶端,與AwesomeGithub相對應。

android-api-analysis: 結合詳細的Demo來全面解析Android相關的知識點, 幫助讀者可以更快的掌握與理解所闡述的要點。

daily_algorithm: 每日一算法,由淺入深,歡迎加入一塊兒共勉。

推薦閱讀

重溫Retrofit源碼,笑看協程實現

我爲什麼棄用Jetpack的App Startup?

AwesomeGithub組件化探索之旅

相關文章
相關標籤/搜索