美团猫眼电影android模块化实战--可能是最详细的模块化实战

4.1 原项目耦合结构

开始模块化工作,我首先得给大家呈现下之前未模块时高度耦合的猫眼app。我们这里以电影详情页为例,看看他的耦合情况:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

电影详情页是建立在一层层的基类之上,这些基类耦合了具体的网络加载等各种服务。因为详情页有想看、评分、点赞等可编辑状态,所以还耦合了greendao数据库(以前网络加载也耦合了这个数据库,后来换成了retrofit+rxjava,所以替换到了这层耦合,谢天谢地)。该页面因为需要和其他页面互动(比如跳转、评分同步等),所以也同时耦合了其他页面的类。除此之外,还有utils,view,model等。如果想把电影详情页抽离出来,这些所有的耦合都要剥离。具体需要解决的问题,如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.2 准备工作

4.2.1 工作量评估

首先我们说下解耦时需要做的准备工作。因为这些工作是解耦拆分的基础。有两点需要做,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

首先说明一下,并不是我喜欢打五颗星。确实是这部分工作量比较大~~~

4.2.2 公共资源,model,utils等的拆分
4.2.2.1 耦合情况示例

第一点是公共资源,model,utils等的拆分。这些事情虽然不用考虑太多事情,但是很繁琐。在做模块化的时候,这个地方耗费了不少时间。很大一部分原因是,之前的猫眼历史版本代码不够规范,对代码耦合这些事情不够敏感。举几个例子吧:

  • 我们之前的utils基本都写在一个类MovieUtils里面了。这个类就像大染缸。什么都向里面放。在传入的参数方面也不够规范,甚至MaoyanBaseFragment这种业务代码都作为参数传入。导致这个东西及其难拆。
  • utils的方法不传context。前人写的时候图省事,在项目中统一加了一个静态的context,导致几乎所有的utils都没有传入context,这样的后果是这些工具方法直接以来宿主app。
  • 之前写的common view 不够独立。既然想写common view,那么就尽量让这个view能够独立,不要耦合其他第三方库,尽量使用android 官方库。
4.2.2.2 资源拆分经验

对于资源的拆分,其实是非常繁琐的。尤其是如果string, color,dimens这些资源分布在了代码的各个角落,一个个去拆,非常繁琐。其实大可不必这么做。因为android在build时,会进行资源的merge和shrink。res/values下的各个文件(styles.xml需注意)最后都只会把用到的放到intermediate/res/merged/…/valus.xml,无用的都会自动删除。并且最后我们可以使用lint来自动删除。所以这个地方不要耗费太多的时间。刚才说了,styles.xml需注意。那么需要注意什么呢?这个东西是这么写的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们在写属性名字的时候,一定要加上前缀限定词。如果不加的话,你这个lib打包成aar后给其他app使用的时候,会出现属性名名字重复的冲突,为什么呢?因为BezelImageView这个名字根本不会出现在intermediate/res/merged/…/valus.xml里, 所以不要以为这是属性的限定词!

4.2.3 集成式vs组合式(选做)

前面说了资源utils等的拆分,那么接下来说下第二点,基类的处理。我们看到电影详情页是建立在一堆的基类之上。每一层的基类都做了一些事情。(当时这么写是为了页面的快速开发)如果我们想将电影详情页独立出来,就需要把这些基类打包成一个aar,下沉到基础库,供所有页面使用。但是我们以前的这种基类耦合了很多猫眼的东西,像下拉刷新,页面状态什么的都是写死的,并且如果我需要写个页面,我就需要继承那么一大堆的fragment。当然这种改一改也可以移植。但对以后的代码迭代肯定是不好的(修改,添加业务)。因为它灵活性差。比如如果点评app上需要猫眼某个页面的一部分而不是整个页面,原来那种改起来就不是很方便。我希望的方式是这些页面都是view,而不是fragment。并且也不是这种继承方式,而是组合方式。即如果我想要一个带下拉刷新的列表view,那么我直接build出来这么一个view,需要什么配置就set进来,它就能够使用。这个view你可以放到任何一个view,fragment中和其他view进行组合。即:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个MovieRcPagePullToRefreshStatusBlock是一个view,可以用在任何页面进行view的组合。

4.2.3.1 组件的插拔式,组合式设计

其实我的做法更大胆,或者更“懒”一些。我希望我这个MovieRcPagePullToRefreshStatusBlock build成功以后,放到页面中就能显示运行,自动加载数据了。就像小时候玩积木那样,组件与组件都是插拔即用式的。至于这个block是怎么加载数据的,使用者无需关系。使用者只需要拿到这个block,然后build时set进去需要的东西。放到页面中就可以运行了。可以参考这个作为示例:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们可以看到这个页面,我只是build出来了两个view,然后放到这个page中,并没有关心数据加载什么的,数据加载是在这个block内部完成的。然后这个page就像前面说的那样,放到某个app的activty中就可以运行了。插拔式、傻瓜式的思想,可能我这个人比较“懒”~~

那这种架构怎么实现呢?,接下来粗略的看看这种框架大体的实现思路吧(具体的可以看下我写的这一篇android 官方mvp框架优化:lifecycle-mvp,像前端那样组合式写页面)。其实这个框架大体也是mvp框架的思想,不过同时解决了业务场景的一些问题,比如,生命周期,移植性,沟通成本,使用方便与否等。既然要说下实现思路,那么从开始说起,对自己是个总结,对读者们可能有有些许帮助。先说下mvp框架的含义:

4.2.3.2 mvp框架含义

mvp框架总体来说适用于android的场景需求。m代表model,提供数据;v即view,提供的是供presenter调用的view相关的方法;p 即presenter,提供的是页面里触发动作的逻辑方法。

4.2.3.3 官方mvp框架的缺点

mvp框架网上有很多,官方也推荐了mvp框架。和一般的区别是:用contract来承载view和presenter的接口定义。用fragment来实现view接口。不过官方使用fragment来实现view,也有它的无奈。为什么说它无奈呢?对于view层的接口,使用fragment来进行实现,主要是因为fragment有生命周期。但fragment太笨重了。试想一下,我有一个页面,里面有四五块内容。为了以后的各块内容的移动、去除、移植更方面,我希望每一块内容都做成mvp形式,块与块之间不耦合。那么官方的这个mvp框架就不适用了。因为你不可能在一个页面写5个fragment把。android的activity中不建议写那么多的fragment,fragment典型的使用场景是ViewPager。

4.2.3.4 常规变通

那么变通一下,5块内容的view层,不再用fragment实现,而只是一个个普通的view,每个view监听事件的响应还是在view中进行(调用各自的presenter方法)。而对于整个页面的初始化加载或者下拉刷新加载,这5块内容共用一个fragment,在这个fragment的onStart()和下拉刷新的监听回调中加载5块内容对应的presenter的方法。然后在fragment的onCreateView()中把5块内容的view填充进来。5块内容之间可能还需要通信,数据交流,这些借助presenter在fragment中进行。

4.2.3.5 带生命周期的mvp:lifecycle-mvp

上面那么做完全没有问题,并且上面那种做法也存在于我们的项目中。但通过几个版本的迭代,我发现了一些问题:presenter太乱,太散。fragment需要持有所有的presenter,在onStart()时load()数据。各自的view也需要持有各自的presenter。并且view和presenter之间需要互相set()。你还需要在activty或者fragment的onDetroy()方法中管理presenter。总体让人觉得很乱。尤其是如果你的组件需要被别人使用,或组件用需要用到其他app时,其他人拿到你的组件,你要关心两个东西view和presenter,他得知道这两个东西里面的方法,并且他需要在activty/fragment的生命周期中关联他们并调用一些方法。嗯。这个过程肯定存在的大量沟通成本~
所以才想到了前面讲的那种build方式来实例化组件,然后用pager组合组件。特点是(具体可以看下android 官方mvp框架优化:lifecycle-mvp,像前端那样组合式写页面):

  • 使用lifecycle-component这个组件提供生命周期。
  • presenter被view层内部持有,不向外暴露。
  • build创建view实例时,提供TypeFactory,用于业务的扩展。
  • 业务代码分层。
    用这种mvp的变种框架改写项目的原代/写新业务,就可以使页面更容易移植、拓展,页面内的模块也可以移动改变。当然,这种框架是建立在我们的业务基础之上,框架还是需要因项目而已,没有最好,只有更适合~

4.3 接口的抽离

前面已经阐述了模块化的准备工作,接下来我们需要做什么呢?根据前面介绍的原项目耦合结构,我们知道我们以前的项目直接依赖了各种service的具体实现。我们接下来要做的是把这些具体service实现用接口来剥离:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.3.1 使用servieloader进行解耦—非显式的调用服务实现类
4.3.1.1 官方serviceloader

从图上可以看到,我们的实现类都被对应的接口所代替。但就这一步本身来说,并没有太大的难度:找到以前服务调用的地方,然后换成接口调用。无非就是有些服务用的比较多,换起来繁琐一些。但我们现在需要考虑一个问题:服务的实现,我们怎么给?首先想到的是,我们留一个参数来传入。但这种方式会导致将来使用lib的时候,沟通成本太大:你需要告诉别人哪里哪里我需要传入什么类型的参数。不然你这个lib就没法运行。我不希望别人在使用你lib的时候,还需要去内部查下你的代码是什么,应该怎么传参数。我希望 别人在使用的时候,对他们来说,lib是尽量透明的。不需要知道lib内部写的是什么,只需要在外部配置一个txt的文本就可以运行lib!那应该怎么做呢?
其实java很早就提供了这种类似的功能:在资源目录META-INF/services中放置提供者配置文件,然后在app运行时,遇到Serviceloader.load(XxxInterface.class)时,会到META-INF/services的配置文件中寻找这个接口对应的实现类全路径名,然后使用反射去生成一个无参的实例。
我们大体的使用方式也是基于java提供的这种功能:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.3.1.2 对官方serviceloader改造
4.3.1.2.1 官方serviceloader缺陷

从前面的阐述来看,java官方提供的serviceloader至少有三个地方需要改进。

  • serviceloader没有缓存功能。因为对于服务来说,大部分我们都需要使用单例模式,而不会频繁的生成新的实例。
  • serviceloader使用无参的构造方法进行构建实例。这点不用多说,肯定需要改进。谁的服务构建的时候不需要传入参数呢?
  • serviceloader没有预检查等问题。因为在运行时,需要在配置文件中去寻找接口对应的实现类名。那么肯定会遇到接口名写错了,类名写错了,配置方式写错了,找不到接口实现类等,这些错误在编译器是发现不了的。同时,使用serviceloader是一种非显式的调用服务实现类方式,如果不在proguard中保护这些实现类,那么肯定会被shrink掉。除了proguard问题外,配置文件写在资源目录META-INF/services下对于一些手机(三星)也有兼容问题。最后,考虑servic配置文件手动注册的缺点,serviceloader需要提供自动注册功能。

对于上面三种情况的处理,第一点很容易解决。提供一个缓存就可以了,不多说。

4.3.1.2.2 serviceloader构造实例

第二点我们是这么解决的:我们让所有使用serviceloader加载服务的接口都实现Iprovider接口。Iprovider接口提供了一个init(Context context)方法。这样所有的服务实现类都需要实现init(Context)方法,在里面做原构造方法里需要做的初始化逻辑。因此,我们在调用serviceloader加载服务的时候就类似这样:

ImageLoader imageLoader = MovieServiceLoader.getService(context, ImageLoader.class);

在MovieServiceLoader内部,生成的实例会调用一下init(Context)方法。这样我们就解决了第二个问题。这里可能也会有一些朋友有些疑问(比如和美团平台的童鞋就此事讨论过):为什么只传入context参数。如果一个服务实现类还需要其他参数怎么办?就我们的服务和而言,我认为只需要传入context,基本上通过context能够获得android绝大部分的参数。并且对于服务来说,既然它是一种服务,按理说不会依赖你项目一个具体的一个组件。所以我认为传入context就够了,而不是传入不定格式的object参数:

MovieServiceLoader.getService(Object… params, ImageLoader.class);

这种方式固然能够解决所有问题。但是这种设计的思想已经违背了接口和实现的隔离概念。比如说,我想使用图片加载服务,按理说我只需要调用一下

imageLoader = MovieServiceLoader.getService(context, ImageLoader.class);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

文末

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

d移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值