
我們一直都在使用Retroift,都知道它的核心是動態(tài)代理。例如在之前的文章重溫Retrofit源碼,笑看協(xié)程實(shí)現(xiàn)中也簡單提及到動態(tài)代理(來填之前挖的坑...)。
咳咳,大家不要關(guān)注起因,還是要回歸當(dāng)前的內(nèi)容。
這次主要是來分析一下動態(tài)代理的作用與實(shí)現(xiàn)原理。既然都已經(jīng)分析了原理,最后自然也要動手仿照Retrofit來簡單實(shí)現(xiàn)一個Demo。
通過最后的Demo實(shí)現(xiàn),相信動態(tài)代理你也基本沒什么問題了。
靜態(tài)代理
既然說到動態(tài)代理,自然少不了靜態(tài)代理。那么靜態(tài)代理到底是什么呢?我們還是通過一個簡單的場景來了解。
假設(shè)有一個Bird接口來代表鳥的一些特性,例如fly飛行特性
interface Bird {
fun fly()
}
現(xiàn)在分別有麻雀、老鷹等動物,因?yàn)樗鼈兌际区B類,所以都會實(shí)現(xiàn)Bird接口,內(nèi)部實(shí)現(xiàn)自己的fly邏輯。
// 麻雀
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)
}
}
麻雀與老鷹的飛行能力都實(shí)現(xiàn)了,現(xiàn)在有個需求:需要分別統(tǒng)計麻雀與老鷹飛行的時長。
你會怎么做呢?相信在我們剛學(xué)習(xí)編程的時候都會想到的是:這還不簡單直接在麻雀與老鷹的fly方法中分別統(tǒng)計就可以了。
如果實(shí)現(xiàn)的鳥類種類不多的話,這種實(shí)現(xiàn)不會有太大的問題,但是一旦實(shí)現(xiàn)的鳥類種類很多,那么這種方法重復(fù)做的邏輯將會很多,因?yàn)槲覀円矫恳环N鳥類的fly方法中都去添加統(tǒng)計時長的邏輯。
所以為了解決這種無意義的重復(fù)邏輯,我們可以通過一個ProxyBird來代理實(shí)現(xiàn)時長的統(tǒng)計。
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實(shí)現(xiàn)了Bird接口,同時接受了外部傳進(jìn)來的實(shí)現(xiàn)Bird接口的對象。當(dāng)調(diào)用ProxyBird的fly方法時,間接調(diào)用了傳進(jìn)來的對象的fly方法,同時還進(jìn)行來時長的統(tǒng)計。
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
上面這種模式就是靜態(tài)代理,可能有許多讀者都已經(jīng)不知覺的使用到了這種方法,只是自己沒有意識到這是靜態(tài)代理。
那它的好處是什么呢?
通過上面的例子,很自然的能夠體會到靜態(tài)代理主要幫我們解決的問題是:
- 減少重復(fù)邏輯的編寫,提供統(tǒng)一的便捷處理入口。
- 封裝實(shí)現(xiàn)細(xì)節(jié)。
動態(tài)代理
既然已經(jīng)有了靜態(tài)代理,為什么又要來一個動態(tài)代理呢?
任何東西的產(chǎn)生都是有它的必要性的,都是為了解決前者不能解決的問題。
所以動態(tài)代理就是來解決靜態(tài)代理所不能解決的問題,亦或者是它的缺點(diǎn)。
假設(shè)我們現(xiàn)在要為Bird新增一種特性:chirp鳥叫。
那么基于前面的靜態(tài)代理,需要做些什么改變呢?
- 修改
Bird接口,新增chirp方法。 - 分別修改
Sparrow與Eagle,為它們新增chirp的具體實(shí)現(xiàn)。 - 修改
ProxyBird,實(shí)現(xiàn)chirp代理方法。
1、3還好,尤其是2,一旦實(shí)現(xiàn)Bird接口的鳥類種類很多的話,將會非常繁瑣,這時就真的是牽一發(fā)動全身了。
這還是改動現(xiàn)有的Bird接口,可能你還需要新增另外一種接口,例如Fish魚,實(shí)現(xiàn)有關(guān)魚的特性。
這時又要重新生成一個新的代理ProxyFish來管理有關(guān)魚的代理。
所以從這一點(diǎn),我們可以發(fā)現(xiàn)靜態(tài)代理的機(jī)動性很差,對于那些實(shí)現(xiàn)了之后不怎么改變的功能,可以考慮使用它來實(shí)現(xiàn),這也完全符合它的名字中的靜態(tài)的特性。
那么這種情況動態(tài)代理就能夠解決嗎?別急,能否解決接著往下看。
接著上面,我們?yōu)?code>Bird新增chirp方法
interface Bird {
fun fly()
fun chirp()
}
然后再通過動態(tài)代理的方式來實(shí)現(xiàn)這個接口
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靜態(tài)方法來創(chuàng)建一個實(shí)現(xiàn)Bird接口的代理。該方法主要有三個參數(shù)分別為:
- ClassLoader: 生成代理類的類類加載器。
- interface 接口Class數(shù)組: 對應(yīng)的接口Class。
- InvocationHandler: InvocationHandler對象,所有代理方法的回調(diào)。
這里關(guān)鍵點(diǎn)是第三個參數(shù),所有通過調(diào)用代理類的代理方法都會在InvocationHandler對象中通過它的invoke方法進(jìn)行回調(diào)
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
這就是上面將判斷調(diào)用具體接口方法的邏輯寫在InvocationHandler對象的invoke方法的原因。
那它到底是如何實(shí)現(xiàn)的呢?怎么就成了一個代理類呢?我也沒看到代理類在哪???怎么就所有調(diào)用都通過InvocationHandler的呢?
有這些疑問很正常,開始接觸動態(tài)代理時都會有這些疑問。導(dǎo)致這些疑問的直接原因是我們不能直接看到所謂的代理類。因?yàn)閯討B(tài)代理是在運(yùn)行時生成代理類的,所以不像在編譯時期一樣能夠直接看到源碼。
那么下面目標(biāo)就很明確了,解決看不到源碼的問題。
既然是運(yùn)行時生成的,那么在運(yùn)行的時候?qū)⑸傻拇眍悓懙奖镜啬夸浵虏痪涂梢粤藛??至于如何?code>Proxy已經(jīng)提供了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()
}
}
}
}
運(yùn)行上面的代碼就會在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同時實(shí)現(xiàn)了我們熟悉的Bird接口;然后在它的構(gòu)造方法中接受了一個var1參數(shù),它的類型是InvocationHandler。繼續(xù)看方法,實(shí)現(xiàn)了類的默認(rèn)三個方法equals、toString與hashCode,同時也找到了我們需要的fly與chirp方法。
例如fly方法,調(diào)用了
super.h.invoke(this, m4, (Object[])null)
這里的h就是之前的var1,即InvocationHandler對象。
到這里迷霧已經(jīng)揭曉了,調(diào)用invoke方法,同時將代理類的自身this、對應(yīng)的method信息與方法參數(shù)傳遞過去。
所以我們只需要在動態(tài)代理的最后一個參數(shù)InvocationHandler的invoke方法中進(jìn)行處理不同代理方法的相關(guān)邏輯。這樣做的好處是,不管你如何新增與刪除Bird中的接口方法,我都只要調(diào)整invoke的處理邏輯即可,將改動的范圍縮小到最小化。
這就是動態(tài)代理的好處之一(另一個主要的好處自然是減少代理類的書寫)。
在Android中運(yùn)用動態(tài)代理的典型非Retrofit莫屬。由于是一個網(wǎng)絡(luò)框架,一個App對于網(wǎng)絡(luò)請求來說接口自然是隨著App的迭代不斷增加的。對于這種變化頻繁的情況,Retrofit使用動態(tài)代理為入口,暴露出一個對應(yīng)的Service接口,而相關(guān)的接口請求方法都在Service中進(jìn)行定義。所以我們每新增一個接口,都不需要做過多的別的修改,相關(guān)的網(wǎng)絡(luò)請求邏輯都封裝到動態(tài)代理的invoke方法中,當(dāng)然Retrofit原理是借助添加Annomation注解的方式來解析不同網(wǎng)絡(luò)請求的方式與相關(guān)的參數(shù)邏輯。最終再將解析的數(shù)據(jù)進(jìn)行封裝傳遞給下層的OKHttp。
所以Retrofit的核心就是動態(tài)代理與注解的解析。
這篇文章的原理解析部分就完成了,最后既然分析了動態(tài)代理與Retrofit的關(guān)系,我這里提供了一個Demo來鞏固一下動態(tài)代理,同時借鑒Retroift的一些思想對一個簡易版的打點(diǎn)系統(tǒng)進(jìn)行上層封裝。
Demo
Demo是一個簡單的模擬打點(diǎn)系統(tǒng),通過定義Statistic類來創(chuàng)建動態(tài)代理,暴露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
}
}
通過入口傳進(jìn)來的Service接口,從而創(chuàng)建對應(yīng)的動態(tài)代理類,然后將對Service接口中的方法調(diào)用的邏輯處理都封裝到了LoadService的invoke方法中。當(dāng)然Statistic也借助了注解來解析不同的打點(diǎn)類型事件。
例如,我們需要分別對Button與Text進(jìn)行點(diǎn)擊與展示打點(diǎn)統(tǒng)計。
首先我們可以如下定義對應(yīng)的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來獲取動態(tài)代理的代理類對象
private val mStatisticService = Statistic.instance.create(StatisticService::class.java)
有了對應(yīng)的代理類對象,剩下的就是在對應(yīng)的位置直接調(diào)用。
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)
}
}
}
這樣一個簡單的打點(diǎn)上層邏輯封裝就完成了。由于篇幅有限(懶...)內(nèi)部具體的實(shí)現(xiàn)邏輯就不展開了。
相關(guān)源碼都在android-api-analysis項目中,感興趣的可以自行查看。
使用前請先把分支切換到
feat_proxy_dev
項目
android_startup: 提供一種在應(yīng)用啟動時能夠更加簡單、高效的方式來初始化組件,優(yōu)化啟動速度。不僅支持Jetpack App Startup的全部功能,還提供額外的同步與異步等待、線程控制與多進(jìn)程支持等功能。
AwesomeGithub: 基于Github客戶端,純練習(xí)項目,支持組件化開發(fā),支持賬戶密碼與認(rèn)證登陸。使用Kotlin語言進(jìn)行開發(fā),項目架構(gòu)是基于Jetpack&DataBinding的MVVM;項目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger與Hilt等流行開源技術(shù)。
flutter_github: 基于Flutter的跨平臺版本Github客戶端,與AwesomeGithub相對應(yīng)。
android-api-analysis: 結(jié)合詳細(xì)的Demo來全面解析Android相關(guān)的知識點(diǎn), 幫助讀者能夠更快的掌握與理解所闡述的要點(diǎn)。
daily_algorithm: 每日一算法,由淺入深,歡迎加入一起共勉。