Android 跨模块通信

随着项目规模的不断扩大,为了更好的进行协作开发,提高开发效率,必须对项目进行改造以支持模块化、插件化。在对项目进行模块化时遇到的第一个挑战就是模块之间的通信。这篇文章将探讨 Android 项目中的跨模块通信。
更多文章可查看我的独立博客

模块化

首先解释一下为什么需要跨模块通信,模块之间为什么不能直接通信。

模块化.png

上图是项目模块化之前和模块化之后的对比图。在模块化之前一般的 Android 项目都在同一个 module app 中,所有的功能模块都可以互相调用,不存在跨模块通信的问题。在模块化之后项目中的各模块有了层级关系,在最底层是一些与项目业务无关的 lib 库,上面一层是 base ,再上面一层是与业务紧密相关的各功能模块。在这种项目结构中,同一层级之间不直接依赖,因此同级模块之间不能直接跳转或通信,因此才有了各种跨模块通信机制。

跨模块通信机制

跨模块通信需要解决的两个问题
  1. 跨模块跳转
  2. 跨模块调用方法
跨模块通信核心原理

我们可以先考虑一下为什么模块之间不能直接通信?其实原因很简单,那就是模块之间禁止直接依赖,因为模块之间没有直接依赖,所以也就不能拿到对应的 class 对象。对应到 Android 就是在 startActivity 时拿不到要跳转的 Activityclass 对象。所以跨模块通信的核心原理非常简单:将字符串和 class 对象对应起来,然后通过字符串去进行通信。

跨模块通信实现

下面我们将一步一步实现一个简易版的跨模块通信框架。首先我们根据上图依赖关系新建一个模块化项目,依赖关系如下图:

模块化测试.png
跨模块通信框架简易版

可以看到项目依赖关系变简单了,但是这并不妨碍我们的模块间通信框架的设计。现在 main 中有 HomeActivity , mine 中有 MineActivity , account 中有 AccountActivity , 现在我们的需求是 HomeActivity 中点击按钮跳转到 MineActivity 。因为 main 没有直接依赖 mine ,所以我们不能简单的通过 AndroidstartActivity 的方式进行跳转,所以接下来我们就实现我们的第一版跨模块通信。
因为模块之间没有互相依赖,所以模块之间的通信只能通过他们共同依赖的 base 实现了。

1. 首先各模块将需要暴露的 Activity 注入到 base 中。
//位于 base 中,各模块通过调用 inject 方法将 class 对象保存到 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 方法中执行
MineArchmage.init();
MainArchmage.init();
AccountArchmage.init();

通过上面的代码已经将字符串和相应的 class 对象保存了起来。

2. 跳转
//此类位于 base 中,有了这个类之后,如果有跨模块跳转的需求可直接调用 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());
    }
}

通过以上很简单的方法我们已经实现了跨模块的跳转,虽然代码很粗糙,但是我们的需求确实实现了。

3.跨模块调用方法

现在 base 中有一个接口,account 中的 AccountUtilImpl 进行了实现:

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

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

我们的需求是在 MineActivity 中点击按钮调用 AccountUtilImpl 的 isLogin 方法,有了前面跨模块跳转的基础,这个需求就非常简单了。

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();

到此为止,我们已经实现了跨模块通信的两个基础需求,然而代码粗糙,功能过于简陋,还存在着很多问题,因此下面我们进一步完善。

跨模块通信改进版
跨模块通信第一版的不足
  1. 内存浪费;上面我们的代码是在 Application 初始化的时候就将所有的需要暴露的 Activity 和 接口的 class 都载入了内存,这是一种浪费,因为用户每次访问我们的应用的时候不是每一个页面都会访问到的,而我们却将所有的 Activity 的 class 都在应用初始化的时候就载入了内存,这确实是一种内存浪费,而且影响了应用的初始化速度。
  2. 跨模块调用方法需要强转(obtainService)和反射。如果每次调用方法都需要反射调用势必会影响应用的性能。
  3. 重复且毫无技术含量代码多(如各个 module 中的 Archmage)。

针对上面存在的问题,我们下面进一步改善。

我的思路是将所有需要暴露的 Activity 进行分组,在应用初始化的时候先将所有的组加载进内存,然后在调用到每个组的第一个 Activity 时将组内的所有 Activity 的 class 对象加载进内存,这样会有效的改善内存浪费的不足。其中组的划分以业务的关联程度为依据。

因此我们抽象出了 GroupLoader 接口, 所有的 GroupLoader 会在应用初始化的时候进行加载。在路由的时候如果调用到了当前 GroupLoader ,则 GroupLoader 会负责将组内所有的 Activity 的 class 对象加载进内存。 ActivityLoaderGroupLoader 调用加载组内所有的 Activityclass 对象。

// 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 中实现各个 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 则调用 AccountActivityLoader 进行加载 组内所有的 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;
    }
}

在进行了分组之后跳转时的 path 类似如:account/AccountActivity 。下面我们再看一下注入 GroupLoader 的代码:

// inject 将所有的 GroupLoader 和 service 进行注入。由于 inject() 位于 base 中, base 不能依赖到各 module 所以 GroupLoader 的注入采用了
//反射的方式,但是由于 GroupLoader 的数量不会太多,所以  GroupLoader 的注入对于性能的影响不会太大
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();
        }
    }

最后我们再看一下路由的代码

// 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 的实现

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 {
                // 对 service 进行了缓存,但是不是所有的 service 都能缓存,所以这一块需要进一步优化
                sServiceMap.put(serviceName, sServiceClassMap.get(serviceName).newInstance());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return sServiceMap.get(serviceName);
    }

经过上面的改进我们的跨模块通信框架已经完善了很多,但是还是有一个最大的问题没有解决。从上面的代码我们可以看到有很多无脑重复的代码,例如 各个 GroupLoaderActivityLoader ,如果要解决这个问题我们可以使用编译时注解,这就是另外一个话题了,在这里我只提供几个关键字 APTjavapoet

总结

本文通过一个简单的实例实现了一个简易版的跨模块通信机制,通过实现这个简易的跨模块通信框架应该对于跨模块通信有了一个基本的认识,再学习和使用一些完善的路由框架时也有了章法。
示例代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容