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