Android 跨模塊通信

隨著項(xiàng)目規(guī)模的不斷擴(kuò)大癌蓖,為了更好的進(jìn)行協(xié)作開發(fā),提高開發(fā)效率份名,必須對(duì)項(xiàng)目進(jìn)行改造以支持模塊化碟联、插件化。在對(duì)項(xiàng)目進(jìn)行模塊化時(shí)遇到的第一個(gè)挑戰(zhàn)就是模塊之間的通信僵腺。這篇文章將探討 Android 項(xiàng)目中的跨模塊通信鲤孵。
更多文章可查看我的獨(dú)立博客

模塊化

首先解釋一下為什么需要跨模塊通信演怎,模塊之間為什么不能直接通信惜辑。

模塊化.png

上圖是項(xiàng)目模塊化之前和模塊化之后的對(duì)比圖译断。在模塊化之前一般的 Android 項(xiàng)目都在同一個(gè) module app 中碴开,所有的功能模塊都可以互相調(diào)用德谅,不存在跨模塊通信的問題熏瞄。在模塊化之后項(xiàng)目中的各模塊有了層級(jí)關(guān)系拍埠,在最底層是一些與項(xiàng)目業(yè)務(wù)無關(guān)的 lib 庫秸抚,上面一層是 base 豌蟋,再上面一層是與業(yè)務(wù)緊密相關(guān)的各功能模塊廊散。在這種項(xiàng)目結(jié)構(gòu)中,同一層級(jí)之間不直接依賴夺饲,因此同級(jí)模塊之間不能直接跳轉(zhuǎn)或通信奸汇,因此才有了各種跨模塊通信機(jī)制。

跨模塊通信機(jī)制

跨模塊通信需要解決的兩個(gè)問題
  1. 跨模塊跳轉(zhuǎn)
  2. 跨模塊調(diào)用方法
跨模塊通信核心原理

我們可以先考慮一下為什么模塊之間不能直接通信往声?其實(shí)原因很簡(jiǎn)單擂找,那就是模塊之間禁止直接依賴,因?yàn)槟K之間沒有直接依賴浩销,所以也就不能拿到對(duì)應(yīng)的 class 對(duì)象贯涎。對(duì)應(yīng)到 Android 就是在 startActivity 時(shí)拿不到要跳轉(zhuǎn)的 Activityclass 對(duì)象。所以跨模塊通信的核心原理非常簡(jiǎn)單:將字符串和 class 對(duì)象對(duì)應(yīng)起來慢洋,然后通過字符串去進(jìn)行通信塘雳。

跨模塊通信實(shí)現(xiàn)

下面我們將一步一步實(shí)現(xiàn)一個(gè)簡(jiǎn)易版的跨模塊通信框架。首先我們根據(jù)上圖依賴關(guān)系新建一個(gè)模塊化項(xiàng)目普筹,依賴關(guān)系如下圖:

模塊化測(cè)試.png
跨模塊通信框架簡(jiǎn)易版

可以看到項(xiàng)目依賴關(guān)系變簡(jiǎn)單了败明,但是這并不妨礙我們的模塊間通信框架的設(shè)計(jì)。現(xiàn)在 main 中有 HomeActivity , mine 中有 MineActivity , account 中有 AccountActivity , 現(xiàn)在我們的需求是 HomeActivity 中點(diǎn)擊按鈕跳轉(zhuǎn)到 MineActivity 太防。因?yàn)?main 沒有直接依賴 mine 妻顶,所以我們不能簡(jiǎn)單的通過 AndroidstartActivity 的方式進(jìn)行跳轉(zhuǎn),所以接下來我們就實(shí)現(xiàn)我們的第一版跨模塊通信。
因?yàn)槟K之間沒有互相依賴讳嘱,所以模塊之間的通信只能通過他們共同依賴的 base 實(shí)現(xiàn)了幔嗦。

1. 首先各模塊將需要暴露的 Activity 注入到 base 中。
//位于 base 中沥潭,各模塊通過調(diào)用 inject 方法將 class 對(duì)象保存到 sClassMap  中
public class Injector {
    private static Map<String, Class<?>> sClassMap = new HashMap<>();

    public static void inject(String name, Class<?> clazz) {
        sClassMap.put(name, clazz);
    }

    public static Class<?> getClass(String className) {
        return sClassMap.get(className);
    }
}

// 位于 mine 中
public class MineArchmage {
    public static void init() {
        Injector.inject("MineActivity", MineActivity.class);
    }
}

//下面的代碼可以在 Application 的 onCreate 方法中執(zhí)行
MineArchmage.init();
MainArchmage.init();
AccountArchmage.init();

通過上面的代碼已經(jīng)將字符串和相應(yīng)的 class 對(duì)象保存了起來邀泉。

2. 跳轉(zhuǎn)
//此類位于 base 中,有了這個(gè)類之后钝鸽,如果有跨模塊跳轉(zhuǎn)的需求可直接調(diào)用 startActivity 方法即可
public class Transfer {

    public static void startActivity(Activity activity, String path, Intent intent) {
        Class<?> clazz = parsePath(path);
        if (clazz == null || !Activity.class.isAssignableFrom(clazz)) {
            throw new IllegalStateException("con't find the class!");
        }
        intent.setClass(activity, clazz);
        activity.startActivity(intent);
    }

    private static Class<?> parsePath(String path) {
        if (TextUtils.isEmpty(path)) {
            throw new IllegalArgumentException("path must not null!");
        }

        return Injector.getClass(path);
    }
}

//例如
public class HomeActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
    }

    public void toMine(View view) {
        Transfer.startActivity(this, "MineActivity", new Intent());
    }
}

通過以上很簡(jiǎn)單的方法我們已經(jīng)實(shí)現(xiàn)了跨模塊的跳轉(zhuǎn)汇恤,雖然代碼很粗糙,但是我們的需求確實(shí)實(shí)現(xiàn)了寞埠。

3.跨模塊調(diào)用方法

現(xiàn)在 base 中有一個(gè)接口屁置,account 中的 AccountUtilImpl 進(jìn)行了實(shí)現(xiàn):

//位于 base 中
public interface AccountUtil {
    boolean isLogin();
}

//位于 base 中
public class AccountUtilImpl implements AccountUtil {
    @Override
    public boolean isLogin() {
        return false;
    }
}

我們的需求是在 MineActivity 中點(diǎn)擊按鈕調(diào)用 AccountUtilImpl 的 isLogin 方法焊夸,有了前面跨模塊跳轉(zhuǎn)的基礎(chǔ)仁连,這個(gè)需求就非常簡(jiǎn)單了。

public class AccountArchmage {
    public static void init() {
        Injector.inject("AccountActivity", AccountActivity.class);
        Injector.inject(AccountUtil.class.toString(), AccountUtilImpl.class);//新增
    }
}

//新增方法
 public static Class<?> obtainService(Class<?> service) {
        return parsePath(service.toString());
    }

AccountUtil accountUtil = (AccountUtil) Transfer.obtainService(AccountUtil.class).newInstance();
Toast.makeText(this, String.valueOf(accountUtil.isLogin()), Toast.LENGTH_SHORT).show();

到此為止阱穗,我們已經(jīng)實(shí)現(xiàn)了跨模塊通信的兩個(gè)基礎(chǔ)需求饭冬,然而代碼粗糙,功能過于簡(jiǎn)陋揪阶,還存在著很多問題昌抠,因此下面我們進(jìn)一步完善。

跨模塊通信改進(jìn)版
跨模塊通信第一版的不足
  1. 內(nèi)存浪費(fèi)鲁僚;上面我們的代碼是在 Application 初始化的時(shí)候就將所有的需要暴露的 Activity 和 接口的 class 都載入了內(nèi)存炊苫,這是一種浪費(fèi),因?yàn)橛脩裘看卧L問我們的應(yīng)用的時(shí)候不是每一個(gè)頁面都會(huì)訪問到的冰沙,而我們卻將所有的 Activity 的 class 都在應(yīng)用初始化的時(shí)候就載入了內(nèi)存侨艾,這確實(shí)是一種內(nèi)存浪費(fèi),而且影響了應(yīng)用的初始化速度拓挥。
  2. 跨模塊調(diào)用方法需要強(qiáng)轉(zhuǎn)(obtainService)和反射唠梨。如果每次調(diào)用方法都需要反射調(diào)用勢(shì)必會(huì)影響應(yīng)用的性能。
  3. 重復(fù)且毫無技術(shù)含量代碼多(如各個(gè) module 中的 Archmage)侥啤。

針對(duì)上面存在的問題当叭,我們下面進(jìn)一步改善。

我的思路是將所有需要暴露的 Activity 進(jìn)行分組盖灸,在應(yīng)用初始化的時(shí)候先將所有的組加載進(jìn)內(nèi)存蚁鳖,然后在調(diào)用到每個(gè)組的第一個(gè) Activity 時(shí)將組內(nèi)的所有 Activity 的 class 對(duì)象加載進(jìn)內(nèi)存,這樣會(huì)有效的改善內(nèi)存浪費(fèi)的不足赁炎。其中組的劃分以業(yè)務(wù)的關(guān)聯(lián)程度為依據(jù)醉箕。

因此我們抽象出了 GroupLoader 接口, 所有的 GroupLoader 會(huì)在應(yīng)用初始化的時(shí)候進(jìn)行加載。在路由的時(shí)候如果調(diào)用到了當(dāng)前 GroupLoader 琅攘,則 GroupLoader 會(huì)負(fù)責(zé)將組內(nèi)所有的 Activity 的 class 對(duì)象加載進(jìn)內(nèi)存垮庐。 ActivityLoaderGroupLoader 調(diào)用加載組內(nèi)所有的 Activityclass 對(duì)象。

// GroupLoader 接口坞琴,位于 base 中
public interface GroupLoader {
    Map<String, GroupLoader> injectModule();

    Map<String, Class<? extends IService>> injectService();

    Class<? extends Activity> getActivity(String activityName);
}

// ActivityLoader 接口哨查,位于 base 中
public interface ActivityLoader {
    Map<String, Class<? extends Activity>> injectActivity();
}

然后在各 module 中實(shí)現(xiàn)各個(gè) GroupLoaderActivityLoader

// 位于 account 中
public class AccountGroupLoader implements GroupLoader {
    private Map<String, Class<? extends Activity>> sActivityMap;

    @Override
    public Map<String, GroupLoader> injectModule() {
        Map<String, GroupLoader> result = new HashMap<>();

        result.put("account", new AccountGroupLoader());
        return result;
    }

    public Map<String, Class<? extends IService>> injectService() {
        Map<String, Class<? extends IService>> serviceMap = new HashMap<>();
        serviceMap.put(AccountUtil.class.getSimpleName(), AccountUtilImpl.class);
        return serviceMap;
    }

    @Override
    public Class<? extends Activity> getActivity(String activityName) {
       // 若 sActivityMap 為 null 則調(diào)用 AccountActivityLoader 進(jìn)行加載 組內(nèi)所有的 Activity 的 class
        if (sActivityMap == null) {
            sActivityMap = new AccountActivityLoader().injectActivity();
        }
        if (sActivityMap == null) {
            throw new IllegalStateException(activityName + "not found!");
        }

        return sActivityMap.get(activityName);
    }
}

// 位于 account 中
public class AccountActivityLoader implements ActivityLoader {
    @Override
    public Map<String, Class<? extends Activity>> injectActivity() {
        Map<String, Class<? extends Activity>> result = new HashMap<>();

        result.put("AccountActivity", AccountActivity.class);

        return result;
    }
}

在進(jìn)行了分組之后跳轉(zhuǎn)時(shí)的 path 類似如:account/AccountActivity 剧辐。下面我們?cè)倏匆幌伦⑷?GroupLoader 的代碼:

// inject 將所有的 GroupLoader 和 service 進(jìn)行注入寒亥。由于 inject() 位于 base 中, base 不能依賴到各 module 所以 GroupLoader 的注入采用了
//反射的方式荧关,但是由于 GroupLoader 的數(shù)量不會(huì)太多溉奕,所以  GroupLoader 的注入對(duì)于性能的影響不會(huì)太大
static void inject() {
        try {
            GroupLoader mainGroupLoader = (GroupLoader) Class.forName("com.huweiqiang.main.MainGroupLoader").newInstance();
            Map<String, GroupLoader> mainModuleLoaderMap = mainGroupLoader.injectModule();
            if (mainModuleLoaderMap != null) {
                sModuleLoaderMap.putAll(mainModuleLoaderMap);
            }
            Map<String, Class<? extends IService>> mainServiceMap = mainGroupLoader.injectService();
            if (mainServiceMap != null) {
                sServiceClassMap.putAll(mainServiceMap);
            }

            GroupLoader mineGroupLoader = (GroupLoader) Class.forName("com.huweiqiang.mine.MineGroupLoader").newInstance();
            Map<String, GroupLoader> mineModuleLoaderMap = mineGroupLoader.injectModule();
            if (mineModuleLoaderMap != null) {
                sModuleLoaderMap.putAll(mineModuleLoaderMap);
            }
            Map<String, Class<? extends IService>> mineServiceMap = mineGroupLoader.injectService();
            if (mineServiceMap != null) {
                sServiceClassMap.putAll(mineServiceMap);
            }

            GroupLoader accountGroupLoader = (GroupLoader) Class.forName("com.huweiqiang.account.AccountGroupLoader").newInstance();
            Map<String, GroupLoader> accountModuleMap = accountGroupLoader.injectModule();
            if (accountModuleMap != null) {
                sModuleLoaderMap.putAll(accountModuleMap);
            }
            Map<String, Class<? extends IService>> accountServiceMap = accountGroupLoader.injectService();
            if (accountServiceMap != null) {
                sServiceClassMap.putAll(accountServiceMap);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

最后我們?cè)倏匆幌侣酚傻拇a

// Transfer 位于 base 中
public class Transfer {

    public static void startActivity(Activity activity, String path, Intent intent) {
        Class<?> clazz = parseActivityPath(path);
        if (clazz == null || !Activity.class.isAssignableFrom(clazz)) {
            throw new IllegalStateException("con't find the class!");
        }
        intent.setClass(activity, clazz);
        activity.startActivity(intent);
    }

    public static IService obtainService(Class<? extends IService> service) {
        return Injector.getService(service.getSimpleName());
    }

    private static Class<?> parseActivityPath(String path) {
        String module = parseModule(path);

        GroupLoader groupLoader = Injector.getModuleLoader(module);

        String activityName = parseClass(path);

        return groupLoader.getActivity(activityName);
    }

    private static String parseModule(String path) {
        if (TextUtils.isEmpty(path)) {
            throw new IllegalArgumentException("path must not null!");
        }

        int separatorIndex = path.indexOf("/");
        if (separatorIndex == -1) {
            throw new IllegalStateException("path must has / ");
        }

        return path.substring(0, separatorIndex);
    }

    private static String parseClass(String path) {
        if (TextUtils.isEmpty(path)) {
            throw new IllegalArgumentException("path must not null!");
        }

        int separatorIndex = path.indexOf("/");
        if (separatorIndex == -1) {
            throw new IllegalStateException("path must has / ");
        }
        return path.substring(separatorIndex + 1);
    }
}

下面再看看 Injector.getServiceInjector.getModuleLoader 的實(shí)現(xiàn)

static GroupLoader getModuleLoader(String moduleName) {
        return sModuleLoaderMap.get(moduleName);
    }

    static IService getService(String serviceName) {
        if (sServiceMap.get(serviceName) != null) {
            return sServiceMap.get(serviceName);
        }

        if (sServiceClassMap.get(serviceName) != null) {
            try {
                // 對(duì) service 進(jìn)行了緩存,但是不是所有的 service 都能緩存忍啤,所以這一塊需要進(jìn)一步優(yōu)化
                sServiceMap.put(serviceName, sServiceClassMap.get(serviceName).newInstance());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return sServiceMap.get(serviceName);
    }

經(jīng)過上面的改進(jìn)我們的跨模塊通信框架已經(jīng)完善了很多加勤,但是還是有一個(gè)最大的問題沒有解決。從上面的代碼我們可以看到有很多無腦重復(fù)的代碼同波,例如 各個(gè) GroupLoaderActivityLoader 鳄梅,如果要解決這個(gè)問題我們可以使用編譯時(shí)注解,這就是另外一個(gè)話題了未檩,在這里我只提供幾個(gè)關(guān)鍵字 APT 戴尸、 javapoet

總結(jié)

本文通過一個(gè)簡(jiǎn)單的實(shí)例實(shí)現(xiàn)了一個(gè)簡(jiǎn)易版的跨模塊通信機(jī)制,通過實(shí)現(xiàn)這個(gè)簡(jiǎn)易的跨模塊通信框架應(yīng)該對(duì)于跨模塊通信有了一個(gè)基本的認(rèn)識(shí)冤狡,再學(xué)習(xí)和使用一些完善的路由框架時(shí)也有了章法孙蒙。
示例代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市悲雳,隨后出現(xiàn)的幾起案子挎峦,更是在濱河造成了極大的恐慌,老刑警劉巖怜奖,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浑测,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡歪玲,警方通過查閱死者的電腦和手機(jī)迁央,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來滥崩,“玉大人岖圈,你說我怎么就攤上這事「破ぃ” “怎么了蜂科?”我有些...
    開封第一講書人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵顽决,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我导匣,道長(zhǎng)才菠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任贡定,我火速辦了婚禮赋访,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘缓待。我一直安慰自己蚓耽,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開白布旋炒。 她就那樣靜靜地躺著步悠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瘫镇。 梳的紋絲不亂的頭發(fā)上鼎兽,一...
    開封第一講書人閱讀 49,806評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音汇四,去河邊找鬼接奈。 笑死,一個(gè)胖子當(dāng)著我的面吹牛通孽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播睁壁,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼背苦,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了潘明?” 一聲冷哼從身側(cè)響起行剂,我...
    開封第一講書人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎钳降,沒想到半個(gè)月后厚宰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡遂填,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年铲觉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吓坚。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡撵幽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出礁击,到底是詐尸還是另有隱情盐杂,我是刑警寧澤逗载,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站链烈,受9級(jí)特大地震影響厉斟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜强衡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一捏膨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧食侮,春花似錦号涯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至眉尸,卻和暖如春域蜗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背噪猾。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工霉祸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人袱蜡。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓丝蹭,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親坪蚁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子奔穿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348

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