運(yùn)用動(dòng)態(tài)代理來提高代碼的靈活性

前言

本文會(huì)簡(jiǎn)單介紹下 Java 中動(dòng)態(tài)代理模式的使用跟衅,然后著重分享下動(dòng)態(tài)代理如何在提高代碼靈活性方面大展身手。文中會(huì)列舉兩個(gè)實(shí)例蹋绽,一個(gè)是在 MVP 中如何巧妙解決 Presenter 中頻繁使用 if (getView() != null) { } 這種重復(fù)代碼的問題;另一個(gè)是在項(xiàng)目中如何讓多個(gè) modules 間解耦更加靈活、更加純粹的問題郭厌。

動(dòng)態(tài)代理的基本使用

要使用動(dòng)態(tài)代理,主要涉及到兩個(gè)類雕蔽,一個(gè)是 Proxy 類折柠,一個(gè)是 InvocationHandler 類。在介紹如何使用之前批狐,需要明確的是:動(dòng)態(tài)代理的代理對(duì)象只能是 Interface扇售,不能是 Class ,也不能是 abstract class嚣艇。這是因?yàn)樗袆?dòng)態(tài)生成的代理類都繼承自 Proxy承冰。而 java 是單繼承的,所以只有接口對(duì)象能被動(dòng)態(tài)代理食零。

回到剛才介紹的兩個(gè)類困乒,Proxy 描述了一個(gè)代理對(duì)象,同時(shí)它提供了創(chuàng)建并實(shí)例化一個(gè)代理對(duì)象的靜態(tài)方法贰谣。InvocationHandler 是一個(gè)代理對(duì)象的調(diào)用處理器娜搂,它只有一個(gè) invoke 方法,所有被代理的對(duì)象的方法調(diào)用都會(huì)通過這個(gè)方法執(zhí)行吱抚,我們的代理行為也就是在這個(gè)方法里面實(shí)現(xiàn)的百宇。下面給出一個(gè)很簡(jiǎn)單的示例:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class DemonstrationProxy {

    interface A {
        void method();
    }


    static class AIpml implements A {

        @Override
        public void method() {
            System.out.println("method in AIpml");
        }
    }

    //動(dòng)態(tài)代理對(duì)象
    static class AProxy implements InvocationHandler {
        
        //被代理的對(duì)象實(shí)例
        final Object origin;

        AProxy(Object origin) {
            this.origin = origin;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("start to invoke method " + method.getName() + " proxy is " + proxy.getClass().getCanonicalName());
            //執(zhí)行實(shí)際對(duì)象的方法
            return method.invoke(origin, args);
        }
    }

    public static void main(String[] args) {
        final A a = new AIpml();
        final InvocationHandler handler = new AProxy(a);
        final A proxyA = (A) Proxy.newProxyInstance(a.getClass().getClassLoader(), a.getClass().getInterfaces(), handler);
        proxyA.method();
    }
}

編譯運(yùn)行后你將看到如下輸出:

start to invoke method proxy is $Proxy0
method in AIpml

MVP 中如何精簡(jiǎn)不必要的代碼

在 MVP 的開發(fā)模式中,Presenter 持有 View 的引用秘豹,當(dāng)我們需要與 View 進(jìn)行交互時(shí)恳谎,通過 getView() 方法獲得 View 對(duì)象。為了避免內(nèi)存泄漏憋肖,在不需要 Presenter 的時(shí)候(比如在 Activity 的 onDestroyed() 生命周期)將 View 對(duì)象置空因痛。然而此時(shí)可能一些異步任務(wù)沒有結(jié)束,當(dāng)它們結(jié)束后岸更,getView() 就會(huì)返回 null 鸵膏。為了避免 NPE,通常的做法是在所有異步任務(wù)里需要訪問 View 的地方怎炊,都要進(jìn)行 if(getView() != null) { } 這樣的檢查谭企。當(dāng)你寫了好幾個(gè) Presenter 之后廓译,便會(huì)發(fā)現(xiàn)這是一件很煩的事情,不僅僅是因?yàn)槊看我獙懲瑯拥臇|西债查,還有就是:

It's always a bad sign when the else branch is missing.

關(guān)于這個(gè)問題非区,在 Medium 上也有討論,上面也列出了一些解決方案盹廷。比如用 ThirtyInch 這個(gè)第三方 MVP 框架征绸,它把所有對(duì) View 的操作封裝為一個(gè)個(gè)的 ViewActionTiPresenter 內(nèi)部會(huì)管理這些 ViewAction 的運(yùn)行俄占。只有在 View attach 到 Presenter 的時(shí)候管怠,才會(huì)執(zhí)行 ViewAction,否則會(huì)保留 ViewAction 直到 View 再次 attach 到 Presenter缸榄。還有就是用 WeakReference 或者 Optional 來管理 View 渤弛。對(duì)于第一個(gè),算是一個(gè)不錯(cuò)的解決方案甚带,但它作為一個(gè)框架她肯,使用它有一定的引入成本,還有另一個(gè)弊端就是 Presenter 和 View 的生命周期綁定得更加緊密鹰贵,增加了 ViewAction 的維護(hù)成本 晴氨。對(duì)于第二個(gè)方案,感覺像是轉(zhuǎn)移話題一樣砾莱,并沒有解決什么根本問題瑞筐。

其實(shí)這個(gè)問題的源頭在于 getView() 方法是 nullable 的凄鼻,如果該方法返回的 View 能確保非空腊瑟,而且不存在內(nèi)存泄漏問題,且無論是 View 處于哪種生命周期代碼都能得到正確的調(diào)用块蚌,那么問題就解決了闰非。

此時(shí)就輪到動(dòng)態(tài)代理出場(chǎng)了,當(dāng) View 還沒結(jié)束的時(shí)候峭范,getView() 對(duì)象返回的是真實(shí)的 View 對(duì)象财松,而當(dāng) View 的生命周期結(jié)束后,getView() 對(duì)象只需要返回一個(gè)代理 View 即可纱控,這樣就確保了 getView() 不會(huì)返回一個(gè)空的對(duì)象辆毡,自然就不需要反復(fù)檢查,而且代理對(duì)象并不會(huì)對(duì)真實(shí)的 View 有任何的影響甜害,所以代碼邏輯也不會(huì)有任何問題舶掖。

Talk is cheap. Show me the code.

public class AbsPresenter<View extends IView> implements IPresenter {

    private View mView;
    private Class<? extends IView> mViewClass;

    public AbsPresenter(@NonNull View iView) {
        this.mView = iView;
        this.mViewClass = iView.getClass();
        if (this.mViewClass.getInterfaces().length == 0) {
            throw new IllegalArgumentException("iView must implement IView interface");
        }
    }

    public void detach() {
        this.mView = null;
    }

    public @NonNull View getView() {
        if (mView == null) {
            return ViewProxy.newInstance(mViewClass);
        }
        return mView;
    }

    private static final class ViewProxy implements InvocationHandler {

        public static <View> View newInstance(Class<? extends IView> clazz) {
            return (View) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), new ViewProxy());
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Type type = method.getReturnType();
            if (type == boolean.class) {
                return false;
            } else if (type == int.class) {
                return 0;
            } else if (type == short.class) {
                return (short)0;
            } else if(type == char.class) {
                return (char)0;
            } else if (type == byte.class) {
                return (byte)0;
            } else if(type == long.class) {
                return 0L;
            } else if (type == float.class) {
                return 0f;
            } else if (type == double.class) {
                return 0D;
            } else {
                return null;
            }
        }
    }
}

這就是我在流利說項(xiàng)目里抽象出的 Presenter,主要展示了 getView() 方法是怎么利用動(dòng)態(tài)代理保證返回值為非空尔店。如此一來眨攘,就大可放心地在 Presenter 中使用 getView() 方法主慰,而不用擔(dān)心 NPE ,也不用擔(dān)心內(nèi)存泄漏鲫售,代碼還能更干凈共螺,一舉三得!

模塊間結(jié)構(gòu)如何更加靈活情竹,更加精簡(jiǎn)

流利說項(xiàng)目里有很多代表不同功能的模塊藐不,為了將模塊間解耦,我們?cè)谝粋€(gè)公共的模塊中定義各個(gè)功能模塊對(duì)外開放的接口鲤妥,并在各自模塊中實(shí)現(xiàn)佳吞。此外還需要一個(gè)類 (以下稱作 ModuleProvider) 管理這些模塊接口,它需要向外提供接口的 setget 方法棉安。在 Application 初始化的時(shí)候底扳,通過反射將這些接口實(shí)例化,然后各個(gè)模塊便可以通過 ModuleProvider 的 get 方法獲取其他模塊的接口贡耽。一切看起來既美好又和諧衷模,可是這里有個(gè)問題,如果我正在開發(fā) A 模塊蒲赂,為了更快的編譯速度阱冶,我在 build.gradle 中去掉了 B 模塊,ModuleProvider.getB() 將會(huì)返回為 null 滥嘴,那么 A 模塊中很多地方都會(huì)出現(xiàn) NPE 木蹬。當(dāng)然這可以通過判空解決,但顯然很蠢若皱,而且如此一來镊叁,代碼豈不是解耦地不徹底?

一開始的做法是對(duì)于所有的接口都有一個(gè)默認(rèn)的空實(shí)現(xiàn)走触,對(duì)應(yīng)到上面的例子就是 ModuleProvider.getB() 會(huì)有兩個(gè)不同的返回結(jié)果晦譬,一個(gè)是在 B 模塊內(nèi)對(duì)接口的實(shí)現(xiàn),另一個(gè)是在公共模塊的一個(gè)空實(shí)現(xiàn) 互广。如此一來敛腌,ModuleProvider.getB()方法就變成了這樣:

public static B getB() {
    if (b == null) {
      b = new EmptyB();
    }
    return b;
  }

現(xiàn)在隨意去掉不想要的模塊也能愉快地敲代碼了。

這樣的實(shí)現(xiàn)看起來已經(jīng)很不錯(cuò)了惫皱,可還是有優(yōu)化的空間像樊。問題在于每一個(gè)接口都有兩個(gè)實(shí)現(xiàn),每次要對(duì)接口作修改的時(shí)候旅敷,要同時(shí)維護(hù)兩個(gè)實(shí)現(xiàn)類生棍,而且其中一個(gè)實(shí)現(xiàn)并沒有實(shí)際的作用,更多地是只想把精力放在模塊中的實(shí)現(xiàn)上扫皱。這個(gè)時(shí)候動(dòng)態(tài)代理又可以大顯身手了足绅。既然只是一個(gè)空實(shí)現(xiàn)捷绑,那么當(dāng)模塊不存在時(shí)返回一個(gè)接口的動(dòng)態(tài)代理不就好了嗎?最重要的是氢妈,現(xiàn)在不需要同時(shí)維護(hù)兩個(gè)實(shí)現(xiàn)粹污,可以集中精力在有意義的改動(dòng)上。為此我在項(xiàng)目中提供了一個(gè)生成接口代理的工具類:

import com.xxx.xxx.annotations.SpecifyBooleanValue;
import com.xxx.xxx.annotations.SpecifyClassValue;
import com.xxx.xxx.annotations.SpecifyIntegerValue;
import com.xxx.xxx.annotations.SpecifyStringValue;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;


public class EmptyModuleProxy implements InvocationHandler {

    public static <T> T newInstance(Class<T> clazz) {
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new EmptyModuleProxy());
    }

   @Override
   public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
       final SpecifyClassValue specifyClassValue = method.getAnnotation(SpecifyClassValue.class);
       if (specifyClassValue != null) return specifyClassValue.returnValue();
       final SpecifyIntegerValue specifyIntegerValue = method.getAnnotation(SpecifyIntegerValue.class);
       if (specifyIntegerValue != null) return specifyIntegerValue.returnValue();
       final SpecifyStringValue specifyStringValue = method.getAnnotation(SpecifyStringValue.class);
       if(specifyStringValue != null) return specifyStringValue.returnValue();
       final SpecifyBooleanValue specifyBooleanValue = method.getAnnotation(SpecifyBooleanValue.class);
       if(specifyBooleanValue != null) return specifyBooleanValue.returnValue();
       return defaultValueByType(method.getReturnType());
   }

   private Object defaultValueByType(Class type) {
       if (type == boolean.class) {
           return false;
       } else if (type == int.class) {
           return 0;
       } else if (type == short.class) {
           return (short)0;
       } else if(type == char.class) {
           return (char)0;
       } else if (type == byte.class) {
           return (byte)0;
       } else if(type == long.class) {
           return 0L;
       } else if (type == float.class) {
           return 0f;
       } else if (type == double.class) {
           return 0D;
       } else {
           return null;
       }
   }
}

這樣一來ModuleProvider.getB() 方法就變成這樣:

public static B getB() {
    if (b == null) {
      b = EmptyModuleProxy.newInstance(B.class);
    }
    return b;
  }

至此首量,通過動(dòng)態(tài)代理完美地解決了問題壮吩。注意到 EmptyModuleProxy 中還有很多注解,這是因?yàn)楫?dāng)一些模塊沒有引入的時(shí)候加缘,希望它的某些接口能返回一些指定的值以方便測(cè)試鸭叙,所以額外定義了一些注解來解決這個(gè)問題。比如 B 接口里面定義了一個(gè) getBoolean() 方法拣宏,默認(rèn)返回的是 false 沈贝,但實(shí)際上我希望在沒有引入 B 模塊的時(shí)候返回 true。那么 B 接口就可以做如下聲明:

public interface B {

    @SpecifyBooleanValue(returnValue = true)
    Class<?> getBoolean();
}

總結(jié)

通過 MVP 和模塊間解耦這兩個(gè)實(shí)際項(xiàng)目中的例子勋乾,能夠充分地說明動(dòng)態(tài)代理技術(shù)的運(yùn)用宋下,能夠給我們的代碼帶來很多靈活性,讓一些實(shí)現(xiàn)變得更加簡(jiǎn)潔辑莫、也更加優(yōu)雅学歧。當(dāng)然動(dòng)態(tài)代理能帶給我們的不僅僅是靈活性。比如 Retrofit 就通過動(dòng)態(tài)代理將我們聲明的各種 Service 接口轉(zhuǎn)換為一個(gè)個(gè)的 ServiceMethod 各吨,然后交給 OkHttp 執(zhí)行具體的網(wǎng)絡(luò)操作枝笨,從而讓網(wǎng)絡(luò)請(qǐng)求變得如此優(yōu)雅自然。所以說合理地運(yùn)用這項(xiàng)技術(shù)揭蜒,能讓你把代碼敲地更嗨横浑!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市忌锯,隨后出現(xiàn)的幾起案子伪嫁,更是在濱河造成了極大的恐慌领炫,老刑警劉巖偶垮,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異帝洪,居然都是意外死亡似舵,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門葱峡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砚哗,“玉大人,你說我怎么就攤上這事砰奕≈虢妫” “怎么了提鸟?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)仅淑。 經(jīng)常有香客問我称勋,道長(zhǎng),這世上最難降的妖魔是什么涯竟? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任赡鲜,我火速辦了婚禮,結(jié)果婚禮上庐船,老公的妹妹穿的比我還像新娘银酬。我一直安慰自己,他們只是感情好筐钟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布揩瞪。 她就那樣靜靜地躺著,像睡著了一般篓冲。 火紅的嫁衣襯著肌膚如雪壮韭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天纹因,我揣著相機(jī)與錄音喷屋,去河邊找鬼。 笑死瞭恰,一個(gè)胖子當(dāng)著我的面吹牛屯曹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播惊畏,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼恶耽,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了颜启?” 一聲冷哼從身側(cè)響起偷俭,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缰盏,沒想到半個(gè)月后涌萤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡口猜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年负溪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片济炎。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡川抡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出须尚,到底是詐尸還是另有隱情崖堤,我是刑警寧澤侍咱,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站密幔,受9級(jí)特大地震影響放坏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜老玛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一淤年、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蜡豹,春花似錦麸粮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至娇唯,卻和暖如春齐遵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背塔插。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工梗摇, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人想许。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓伶授,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親流纹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子糜烹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,127評(píng)論 25 707
  • 國(guó)家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說閱讀 10,969評(píng)論 6 13
  • 簡(jiǎn)書連載風(fēng)云錄薔薇小說目錄擇一世長(zhǎng)安專題擇一世長(zhǎng)安【目錄】 文丨薔薇下的陽光 上一章丨《第八十章:小搗蛋》 前情提...
    薔薇下的陽光閱讀 792評(píng)論 6 15