浅谈Android IM型项目 本地消息架构

我没有为一个大型项目架构过,也没有做过很出色的IM项目,甚至我是一个菜鸡,所以本文仅作个人记录,勿喷勿扰。

此外希望可以写本文的时候有一些提升。

Android开发者独立开发一个项目,需要先考虑如下几点:

1.过一遍这个项目大大小小所有的细节,上到通信模块,下到一个点击交互,都要过一遍

2.拆分合理性。把这个项目拆分成若干模块,比如功能上,通信模块,业务上,本地聊天模块,好友模块,用户模块。。模块划分的合理是十分难而且重要的。划分了模块后模块间的依赖又是如何的?是否存在不合理或者可以优化的地方?

3.通信合理性。既然划分了模块,那么模块之间又应该如何通信?成本高吗?一个模块是应该暴露接口出来,还是通过隐式方式交互?是在gradle api引入远程地址进行直接的依赖,还是apk间打破壁垒以及再决定是显式依赖还是隐式依赖?(隐式依赖,可以用全局的ServiceManager去解决。此外四大组件间的通信要使用路由。有一个问题,路由和ServiceManager应该合二为一吗?)

4.是否需要接入层?通信模块和本地聊天模块之间要直接通信吗?如果通信模块前期通过集成sdk实现,后期打算换成自己写的推送后台,如何实现一个极低成本的替换呢?所以接入层(数据适配层)是否是需要的?

5.分模块后带来的问题。首先模块划分后,每个模块都是由一个团队独立开发的(自己开发app,那么自己就是多个团队了,哈哈)。大家开发完自己的模块后,最后整合,会暴露几个很常见的问题:一、资源名的重复,需要很好的gradle技术去协调二、依赖的重复引入,依赖是留在这个模块内部还是下沉到基类,这都需要仔细去斟酌。三、编译,如果是组件化,需要用gradle实现单模块编译技术;如果是apk,需要满足在独立编译的时候仍然能支撑运行的基础数据条件。

6.依赖。上面说了依赖所应该存在的位置,模块内部需要有一些依赖,所有模块都要统一依赖一个基层依赖库。此外,还要思考,我们的功能模块,比如上面说的通信模块,是作为一个模块存在,还是作为一个依赖而存在?功能模块是否有必要作为一个独立模块而存在?

7.模块整合之编译。上面说了可以通过组件化实现单模块编译,也可以使用单个apk独立编译。最后整合起来的时候,模块应该怎样的形式存在?apk?还是class文件的集合(资源包共享)?

8.模块和依赖的生命周期。模块,需要提供一个合理的生命周期,安装,更新,卸载。(学习自atlas)

9.模块间的代码边界。我接触过一个组件化的项目,模块之间通过路由和ServiceManager通信,但是其他模块的东西依然是可以直接调用的。所以如何屏蔽掉模块间的代码还是很重要的,避免出现了错误的依赖(别太相信自己,用工具严格约束代码边界是最好的)。

10.版本管理,每个bundle都有独立的版本管理,便于单个bundle版本更新、热修复的记录。模块管理者,需要很好地管理模块的按需加载,安装卸载都有模块管理者把控。文本,有许多东西都需要文本支撑,比如隐式依赖需要用文本记录调用方式;此外还需要记录一些本地协议,也是方便通过隐式的方式调用。

11.工具类之间的依赖。在开发新项目的时候,工具类肯定不会重新去写,所以需要工具类的方便迁移。而实现工具类之间的方便迁移,就需要工具类之间尽量地减少依赖。

12.分层。上面讲了这么多的元素,需要对这些元素进行一个最终的整理,划分清楚一个项目的层次。

13.局部架构。模块之间的架构是架构,UI、数据之间的解耦也叫架构。比如常用的MVP、MVVM,通常情况下是自己打造一套符合这个项目需求的解耦架构(不管啥架构,databinding都得用,这是神器)。

14.单一职责,屏蔽业务和框架。比如网络请求抽象化,sendRequest(String(约定后的本地协议))即可,不要耦合具体的框架

上述的点是必须要关注的。但是也不需要等所有的点都思考到位才开发,在你正确地划分出了一个模块之后,就可以进行单模块的开发了。(这也是分模块的好处!)

考虑IM本地消息系统如何架构之前,先弄清楚一些事情。

当app未启动的时候,推送服务可能是开启的,也可能是关闭的(甚至对于不同的人,还会有消息免打扰这种形式)。

如果推送服务是关闭的,那么消息只能等进入到app后再去获得了。

问题是:我们是去拉消息,还是和服务器connect后他主动推消息呢?

自然是connect后服务器主动推,我很少见到过在app启动的时候需要主动拉消息的推送服务。不过connect后服务器主动推,这一点和主动拉消息其实差不多。服务器应该是NIO的模式,在建立对应socket时,我们会传用户的唯一userID过去。然后后台就会根据userID到数据库去查找未推送的消息(可以从总表中找未推送的消息字段,也可以把未推送的消息弄到新的待发送消息的缓存表中),然后进行把未推送的消息推送到客户端。

所以对于客户端来说,如果app未启动的时候,有人发消息过来了,在app启动后,服务端应该是会主动推送消息到客户端来。

如果推送服务是开启的。消息是一直会发送到我们的另一个进程中去的,区别于推送服务是关闭的,推送服务开启的时候数据是存在的,只是app进程还未启动,所以我们需要缓存数据在另一个进程。在app启动的时候,再把另一个进程缓存的数据,发送到我们的主进程中来。所以他的判断大概是这样的:

消息到来,主进程是不存在的,那么我发送一个通知,同时缓存数据。在主进程启动的时候,我会把数据跨进程发送给主进程。

最后梳理一下:我们消息都是会从服务端发送到B进程的,如果B进程是死的,我们会在A进程启动的时候,激活B进程,同时B进程和服务端connect,会接收到用户离线的时候接收到的消息,然后B进程跨进程把消息发送给A进程。

如果B进程是活的,那么B进程会收到消息,会发送一个通知,同时缓存消息,等A进程启动的时候B进程再跨进程把消息发送给A线程。

非得跨进程吗?开销会不会很大?

其实把消息的接收机制写在本地也行,这样反而免去了跨进程的开销。但是IM项目的离线推送是不可缺少的功能,我们必须要有一个被保活的B进程去实时接收服务端的消息,再以通知的形式呈现。

所以你其实也可以在A、B进程各实现一套接收机制,B进程的作用仅仅是用来提醒有新消息到来,A进程启动的时候,还是通过自己本进程的消息接收机制去接收消息(但是问题在于需要关闭B进程和服务端的连接,同时还要把B进程在A进程不存在的时候接收到的消息跨进程地发给A进程,因为服务端不可能推两遍)

所以还是跨进程好,实现两套并且做额外的消息同步工作,其成本和维护难度也上升了。而且Binder跨进程的效率很高,实在不行用广播、Messenger也行。

A进程需要做的很简单

上面分析了A进程不存在的时候消息的相关问题,得出了一些结论。A进程启动后,我们会去开启B进程,激活推送模块。可能B进程本来就是活的。然后B如果是被开启的,那么服务器的缓存消息就会发到B,如果B本来就活着的,那么消息已经在B中缓存。如果缓存消息发到B,B就会同步地发给A;如果B中已有缓存,在A尝试开启B但是B已存活的时候,就会把B中的缓存消息发送给A。

所以A的一切逻辑就是,在A开启的时候,或来自网络缓存,或来自B缓存,都会有消息发送到A。不过这无所谓,这对于A是透明的。A只需要知道,在A启动后,A就能接收到A离线过程中接收到的离线消息。所以A只需要做两件事就可以了:

1.在A启动后,确保B连接是活的

2.写一个消息接口,如果A是在线的,那么A就会实时地接收到消息;如果A是离线的,那么A就会接收到离线过程中接收到的消息。

需要有一个消息处理中间人

A进程所希望接收的东西,就仅仅是

1.消息的来源,如果是一个用户就是userID,如果是一个群就是groupID

2.消息文本

而B进程中的sdk消息接收口中接收到的消息格式可能不是这样的,可能包含了其他的稀奇古怪的东西。为了适配两者的数据,同时不修改两者的逻辑,我们需要一个消息处理中间人,一个类似于Adapter的存在。有了这个中间人,我们A进程的消息格式永远是固定的,B进程假如更换了sdk,消息格式变了,我们只需要让消息处理中间人重新从B中取得对A有用的数据,再传给A就可以了。

这个中间人该放哪里?

B进程。迪米特原则,能把任务交给框架就交给框架。B进程中,有消息接口,是和服务端绑定的。接口中,会把获取的数据交给消息处理者处理,处理后的数据又会交给消息分发者去分发。

消息分发者如何分发最解耦且最具扩展性?

大概哪些地方需要消息呢?

1.微信icon边上的4

2.消息列表页面

3.具体的消息页面

4.还有可能需要直接和cache沟通(为了解耦)

此外,不仅仅是消息,还有

1.新的好友

2.发现的新文章或者其他的东西(也可能是本地拉的,而不是服务器推的)

都可能要用到推送功能。

所以本地也需要有一个消息分发者。B进程的消息分发者直接发送到A进程的消息分发者。

然后A进程的消息分发者进行一个本地的分发。建议采用EventBus并且配合tag(只是不知道索引优化在多apk下能否优化)。

大概就是这样了,想到了再补充。。。

还有希望过段时间自己搭一套推送后台和接受推送的客户端,想想有点小激动~~

猜你喜欢

转载自blog.csdn.net/qq_36523667/article/details/81383453