×

Android使用Provider做业务数据交互

96
键盘男
2017.03.19 11:29* 字数 3131

前言

去年12月份写了一篇《App组件化与业务拆分那些事》大家应该瞅过一眼,总结了一些组件化、业务划分的经验,解释了他们的概念和优点,举了几个例子以说明。但文中仅仅是分析了原理,这次来点干货。Talk is cheap,show me the code!


概念

什么是业务拆分

根据产品或技术的业务规划,对工程进行模块划分,业务模块之间代码互不依赖。

为什么要划分业务

当工程存在以下问题:

代码量大,耦合严重
编译慢,效率低
业务开发分工不明确

这时就需要考虑对工程划分业务,它能解决或缓解以上问题。业务划分的优点:

架构更清晰,解耦
加快编译速度
业务代码分工明确,开发人员更专注业务
提高开发效率

(这些内容在《App组件化与业务拆分那些事》提到过,这里点到即止)

业务拆分需要解决的技术点

1.如何划分业务
2.业务间数据如何交互

做业务拆分,还应该考虑以下问题:

更改业务逻辑是否灵活?
开发文档或用例是否简单易懂?
接入业务是否容易?
开发效率是否提高?

一名好的架构师或者team leader,应该充分考虑这些问题,团队中的小伙伴能否适应划分业务开发。如果为了业务拆分而拆分,就本末倒置了。


业务数据交互方案

当然数据交互的方案很多,笔者给大家列出几种方案:

发布-订阅模式

Broadcast

Android四大组件之一,大家很熟悉了。如果运用合理,它就能业务间进行通讯。但仅限于有自身context的组件,例如activity、service。

但是比较蛋疼的,broadcast仅支持基本数组类型(int、string等),当然也支持Serializable对象,但要bean继承serializable不太优雅。

Eventbus

EventBus事件总线比broadcast灵活很多,可以传递任意对象,还能设置哪个线程接收数据。因为EventBus不依赖context,它仅仅以发布-订阅模式工作,所以除了activity、service,任意地方都可以用eventBus通讯。

然而,EventBus也有它的问题,正因为它是发布订阅模式,不能直接调用获取数据,也不能跨进程。因此,EventBus不太适合跨业务获取数据的需求,但很适合用于不同界面间、不同线程间互通发送事件。

直接调用

AIDL

官网上对AIDL描述(https://developer.android.com/guide/components/aidl.html):

AIDL (Android Interface Definition Language) is similar to other IDLs you might have worked with. It allows you to define the programming interface that both the client and service agree upon in order to communicate with each other using interprocess communication (IPC). On Android, one process cannot normally access the memory of another process. So to talk, they need to decompose their objects into primitives that the operating system can understand, and marshall the objects across that boundary for you. The code to do that marshalling is tedious to write, so Android handles it for you with AIDL.

通常Android进程间内存相互不能访问,但可以通过把对象转换成操作系统能识别的原语,进程间通讯就成为可能。因此,Android使用AIDL来完成这一任务。

用过AIDL的同学,都懂的.....太麻烦了。如果不是万不得已,不建议使用AIDL。

私有协议

原理上有点像RPC(远程过程调用)。如图:

假设,定义获取用户数据的协议"data://user/{uid}",因此"data://user/1"是获取uid=1的用户数据:

int      uid      = 1;
Protocol protocol = ...;
String   userJson = protocol.get("data://user/" + uid);
// json : {"uid":1,"name":"键盘男"}

Protocol:放在基础业务,所有业务都依赖它。它只是提供调用的接口;
Router:根据发送过来的协议,发现对应的服务,并调用具体服务的接口,获取数据,返回protocol.get()调用者;
服务B:在APP启动时,注册到服务中心,并告诉Router "data://user/"与此服务相关;
服务中心:一个概念,管理注册的服务,有很多实现方式,类似AndroidManifest的用途。

私有协议,通过服务中心、路由到相关服务获取数据,常用于跨业务。其实,这种方式我们经常用,例如,在浏览器输入http://www.baidu.com,就能浏览百度网页,我们通过http协议来获取某网站的数据,你把http协议理解为“公开的私有协议”即可。

私有协议很多优点:

高度解耦
非常灵活

因为通过协议获取的数据是基本类型或json,因此不依赖任何java bean,业务与业务充分解耦。至于灵活性,例如开发业务A,要获取业务B的数据,但开发中并不希望业务A依赖业务B,这时可以自己接入一个服务B2,模拟服务B返回数据;当集成到宿主工程时,就接入服务B。这样,尽管业务A依赖业务B的数据,但开发中业务A完全不依赖业务B

由于以上优点,很多大厂就用这种方式(至少笔者在XX大会听大厂的讲师介绍)。

但是,私有协议也有不足的地方。例如:

高度依赖开发文档
容易写错协议

在开发时,业务A要获取业务B什么数据,协议是怎样,这些都必须用开发文档规范。还有,由于协议是纯字符串,编译器并不能检测协议对错,写错了协议要运行的时候才发现bug。但可以通过单元测试等手段,去确保协议书写正确。

(由于本文不具体讨论这种方式,暂且介绍这么多,读者有不解之处请见谅。)

Provider

原理上跟私有协议有几分相像,也是通过注册一个服务,通过协议从某个服务获取数据。如图:

调用方式:

ProtocolB protocolB = Protocols.getProtocolB();
String    json      = protocolB.getUser(1);
流程

1.ProviderB继承ProtocolB接口
2.ProviderB注册到服务中心;
3.业务A通过Protocols.getProtocolB()获取ProtocolB接口;
4.protocolB.getUser()获得user json

原理

先上ProtocolB(代码放在公共library,业务A、B都依赖):

public interface ProtocolB {
    String getUser(int uid);
}

ProviderB(代码在业务B的library中,业务A不依赖ProviderB):

public class ProviderB extends implements ProtocolB {

    @Override
    public String getUser(int uid) {
        // todo 从数据库获取user对象,并转化成json
        ...
        String json = ...
        return json;
    }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="xx.xxx.user">

        <provider
            android:name=".provider.ProviderB"
            android:authorities="provider b"/>
</manifest>

Protocols

public class Protocols {

    public static ProtocolB getProtocolB() {
        return (ProtocolB) get("provider b");
    }
    
    /**
     * 获取ContentProvider
     *
     * @param authorities AndroidManifest.xml配置provider的authority
     *
     * @return
     */
    public static ContentProvider get(String authority) {
        try {
            Context               context = AppContextProvider.getContext();
            ContentProviderClient client  = context.getContentResolver().acquireContentProviderClient(authority);
                
            if (client != null) {
                ContentProvider provider = client.getLocalContentProvider();
                    
                return provider;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

依赖关系:ProtocolB、Protocols放在公共library,所有业务都能依赖;Provider B在业务B,其他业务不需要依赖。

其实原理就炒鸡简单,通过"provider b"Context获取ProviderB这个ContentProvider由于ProviderB继承ProtocolB,因此调用ProtocolB.getUser()接口实际上就是调用ProviderB.getUser()方法,从而获取到user json

优缺点分析

优点:

调用方式直观
对文档依赖程度低

缺点:

由于protocol放在公共library,需求变更时,需要频繁更改protocol,合并及集成代码优点麻烦


Provider实践经验

为什么选provider方案

归纳一下,有以下原因:

1.开发人员不多
2.一个主工程开发
3.业务改动不频繁
4.接口直观,调用方式与常规开发近似

之前公司最多有7个安卓开发(目前减少了),后入职的同事做新业务,往往依赖其他基础业务,例如用户信息。那么User业务提供一个UserProvider,其他业务获得UserProtocol,就知道有什么方法可用,注释写得好的话,沟通也很顺畅。

工程数量,视乎开发人员数量、技术程度和业务复杂度。目前来说,一些组件会放在一个library工程,发布到maven让主工程依赖,主工程下是各个业务library,业务数量也有限。所以,一个主工程够玩了。

笔者公司的APP业务划分比较清晰,业务之间不会有太复杂的交互,功能改动大多数仅改该业务界面的逻辑,所以第一次把protocol写好,以后修改比较少。

开发中遇到困难

小伙伴未习惯分业务开发

这个是最最主要的问题了,有的同事刚毕业,有的仅一两年开发经验,并不清楚为什么要分业务开发,也不知道怎么做,会遇到什么情况,所以分业务开发意愿不强。当然,这里也有老同事(包括笔者)在架构、文档上做得不够完善的责任。

单独业务编译,安装时与主APP provider冲突

笔者的工程,是可以让一个业务单独编译、打包、运行到Android手机上。前文提到,provider需要在AndroidManifest.xml声明android:authorities,而一个authority在Android系统上只能存在一个,因此当宿主工程和业务B工程都声明了android:authorities:"provider b",那手机只能装其中一个。

解决方法还是有的,而且不少。例如,声明provider时这么写:

<provider
    android:name=".provider.ProviderB"
    android:authorities="${PROVIDER_B}"/>

(宿主工程为app,业务A library为lib.b,业务A application为app.b)

业务B的build.gradle:

android {
    publishNonDefault true
    defaultPublishConfig 'release'
    
    buildTypes {
        release {
            ...
            // 替换Manifest里${PROVIDER_B}占位符
            manifestPlaceholders = [PROVIDER_B: 'provider b']
        }
        debug {
            ...
            manifestPlaceholders = [PROVIDER_B: 'debug_provider_b']
        }
    }
}

宿主工程build.gradle:

dependencies {
    compile project(':lib.b')
}

业务A application:

dependencies {
    compile project(path: ':lib.b' , configuration: 'debug')
}

这样配置后,运行app application会用android:authorities="provider b",而业务A application则android:authorities="debug_provider_b",这样provider就不冲突了。

同时,Protocols也有做相应修改:

public class Protocols {

    public static ProtocolB getProtocolB() {
        return (ProtocolB) get(BuildConfig.DEBUG ? "debug_provider_b" : "provider b");
    }
}

解决了以往哪些问题

1.业务代码相互耦合
2.一个业务改动,引发其他业务问题

耦合问题,老生常谈。第二个问题其实是第一个问题引发的。业务A与业务B耦合,修改业务A代码,很大可能影响业务B。然后就出现:

“这个版本我明明没改,为什么出现这个bug?” (X一样的队友所为,哈哈哈哈)

把业务独立出来,业务之间用provider进行交互,业务之间代码不依赖、不耦合。

不仅仅是受害者获益,往往你并不知道哪个同事做哪个业务时,调用了你的代码(Android Studio用Find Usage还是可以查看的,不过有时忘了),或者不正确的调用,也会出问题,这时责任就互相推卸了。

使用Protocol-Provider后,其他业务只能调用protocol提供的接口,别的代码动不了,大家都安心了。

关于文档

笔者知道很多移动开发者都不写文档,原因主要就是没时间写,业务经常改动,文档也要改动,麻烦.....笔者对文档持保留态度,毕竟每个团队情况不一样。

但还是建议试用provider时写写最基本的文档,例如:ProtocolB调用方式,AndroidManifest如何声明,gradle如何配置等。

未来面临的问题

1.多个工程、多个团队开发,protocol-provider是否好用?
2.如果由于业务改动导致protocol频繁修改,考虑使用私有协议

小结

业务数据交互是业务拆分很重要的一环,正如普通的架构师关注业务怎么分层、分业务,牛逼的架构师首先关注每个业务、服务用哪些中间件调用。

如果你在旧工程业务拆分遇到耦合解不开,不妨试试provider这种方式,使用恰当的话,会有意想不到的效果。

本文的示意图均笔者绘制,如有疑惑或说得不对的地方,敬请指出!

实战经验:《悦跑圈Android单业务开发,提高编译效率15倍》


关于作者

我是键盘男。
在广州生活,在互联网公司上班,猥琐文艺码农。喜欢科学、历史,玩玩投资,偶尔旅行。

Android
Web note ad 1