愿历尽千帆 归来仍少年

Launcher应用的重构之路

字数统计: 5,548阅读时长: 19 min
2019/04/12

当前需求
新项目遵循竞品的Launcher样式设计
当前计划
重新编写Launcher,并尽可能的弥补老框架的不足

为什么需要重写Launcher?
从样式上来看
新版Launcher的UI显示上与老版本差异较大,左右滑动的菜单页变化最大
从代码结构上看
一. 不符合单一原则: 视图层承担过重的业务数据处理逻辑, 老版本的视图层代码量比较大,主Activity及LauncherService均近两千行,承担的任务过重。其中包含了View的显示逻辑,也有数据处理的逻辑,也有接收广播相关,也有与Fwk的网络接口交互处理的逻辑,还有一些其他应用需要在返回Launcher后对自身的一些判断逻辑等。
二. 从整个应用的角度来看,很多界面存在重复的冗余工作,比如判断接收login广播,判断电量,判断是否高温,json报文的解析等,需要抽取公共的行为封装并下沉,尽可能的使上层的处理优雅且简单
三. 页面间的耦合严重,彼此相互持有,难以剥离开来,且项目代码中大多缺少注释,理解上存在难度

综合考虑觉得采取重写或许会合适些

前期研究
当前市面上存在MVP,MVVM等等架构,先对这些架构做简单介绍


MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写
其中M层处理数据,业务逻辑等;V层处理界面的显示结果;C层起到桥梁的作用,来控制V层和M层通信以此来达到分离视图显示和业务逻辑层








视图层(View)
一般采用XML文件进行界面的描述
控制层(Controller)
Android的Controller层的重任通常落在了众多的Activity的肩上,这也是MVC架构的一大弊病
模型层(Model)
Model是与View无关,与业务相关的,对数据库的操作、对网络等的操作都应该在Model里面处理,对业务计算等操作也是必须放在的该层的。
1. View接受用户的交互请求;
2. View将请求转交给Controller;
3. Controller(用户做的动作比如:update数据,删除指定名字的学生等等)操作Model进行数据更新(根据用户指示,执行底层的数据动作等等);
4. 数据更新之后,Model通知View数据变化;
5. View显示更新之后的数据;

举个例子: Launcher中下拉状态栏显示天气的动作,如果是用MVC的架构,则大概是这样的一个过程:
View即状态栏接收到用户下拉的动作,此时C层即Activity向M层请求天气的数据,M层获取天气的方法被调用后,获取天气无论成功或失败,都会将状态通知给监听者这里即C层进行视图更新,这里的通知通过接口回调方式,即C层需要实现天气是否成功的接口比如onError, onSuccess

弊端:
第一,View层和Controller层没有分离,逻辑比较混乱;
第二,同样因为 View和 Controller层的耦合,导致Activity或者Fragment很臃肿,代码量很大。如果Activity 中的业务量很大,就像我们的老版Launcher,那么问题就会体现出来,一个Activity的代码行数高达近2000行


Presenter负责逻辑的处理,Model提供数据,View负责显示








MVP框架由3部分组成:View负责显示,Presenter负责逻辑处理,Model提供数据。在MVP模式里通常包含3个要素(加上View interface是4个):
View:负责绘制UI元素、与用户进行交互(在Android中体现为Activity)
Model:负责存储、检索、操纵数据(有时也实现一个Model interface用来降低耦合)
Presenter:作为View与Model交互的中间纽带,处理与用户交互的负责逻辑。
View interface:需要View实现的接口,View通过View interface与Presenter进行交互,降低耦合,方便进行单元测试

显而易见的变化时引入了P层,来承担之前C层的工作,而Activity的角色变成了仅仅显示的V层,但是考虑到V层需要和P层交互,所以这里增加了一层接口层View interface
概述下来就是:
当 View 需要更新数据时,首先去找 Presenter,然后 Presenter 去找 Model 请求数据,Model 获取到数据之后通知 Presenter,Presenter 再通知 View 更新数据,这样 Model 和 View 就不会直接交互了,所有的交互都由 Presenter 进行,Presenter 充当了桥梁的角色。很显然,Presenter 必须同时持有 View 和 Model 的对象的引用,才能在它们之间进行通信
好处很明显: view与model完全解耦,它们的通信都要经过presenter
弊端也显而易见: presenter会过于复杂庞大 , view与presenter交互频繁,耦合度高 , presenter持有activity引用,可能引起内存泄露,需要在Activity退出时做额外处理
还以我们的Launcher下拉状态栏为例,如果是用MVP的架构,则大概是这样的一个过程:
下拉状态栏动作触发后,调用Presenter中请求天气的接口,将自身传进去,Presenter的请求方法拿到Activity的引用,并在该请求方法中调用Model的请求数据方法,同样将自身传进去,Model层获取数据后无论失败与否都会回调Presenter的处理结果方法,Presenter的处理结果方法会回调View的处理结果方法,这样View层拿到数据并做视图更新
这里的实现常用做法一般会将Presenter和Model层的接口抽取出来,写对应的实现类,层次上看的更分明些


MVVM可以算是MVP的升级版,其中的VM是ViewModel的缩写,ViewModel可以理解成是View的数据模型和Presenter的合体,ViewModel和View之间的交互通过Data Binding完成,而Data Binding可以实现双向的交互,这就使得视图和控制层之间的耦合程度进一步降低,关注点分离更为彻底,同时减轻了Activity的压力。







MVVM架构通过ViewModel隔离了UI层和业务逻辑层,降低程序的耦合度。通过DataBinding实现ViewViewModel之间的绑定。
好处:
在MVVM中,这些都是通过数据驱动来自动完成的,数据变化后会自动更新UI,UI的改变也能自动反馈到数据层,数据成为主导因素。这样MVVM层在业务逻辑处理中只要关心数据,不需要直接和UI打交道,在业务处理过程中简单方便很多
MVVM模式中,数据是独立于UI的
在MVVM中,数据发生变化后,我们在工作线程直接修改(在数据是线程安全的情况下)ViewModel的数据即可,不用再考虑要切到主线程更新UI了,这些事情相关框架都帮我们做了
关于MVVM不做多介绍,最大的变化无非就是引入了Data Binding,使得数据成为核心驱动

MVC -> MVP -> MVVM 这几个软件设计模式是一步步演化发展的,MVVM是从 MVP的进一步发展与规范,MVP 隔离了MVC中的 M 与 V 的直接联系后,靠 Presenter 来中转,所以使用MVP 时 P 是直接调用 View 的接口来实现对视图的操作的,这个 View 接口的东西一般来说是showDatashowprogress等。M 与 V已经隔离了,方便测试了,但代码还不够优雅简洁,所以 MVVM就弥补了这些缺陷。在 MVVM 中就出现的 Data Binding 这个概念,意思就是 View 接口的 showData 这些实现方法可以不写了,通过Data Binding 来自动实现

老版launcher的可以认为是VC结构,M的数据处理内容多放在C层或者util包中,utils包下还是直接持有引用调用,所以只有两层V和C

研究下来,觉得如上的三种模式都不太适合我们的项目,理由如下
1. MVC到改进版MVP再到后期的MVVM架构,MVC本质上是MC架构,Model与Control层耦合严重,MVP在MVC的基础上增加一层接口解耦,MVVM在MVP的基础上又增加了一层接口,虽耦合性进一步降低,但是存在很多的接口定义导致代码的可读性降低
2. 可切入性不强: 不利于团队同事快速加入适应,存在学习成本
3. 如上的三种模式本质其实是思想的演变,边界并不是很明显,只要能够很好地解耦就是最好的解决方法

Step1: 分层结构










这是我们Launcher的分层结构
业务逻辑层:
由业务需求来决定,如二维码展示,通知,SOS,绑定解绑等模块。通用功能抽取成独立于具体业务需求的模块,在模块内部实现通用的业务逻辑,同时对外暴露调用接口,不同的业务只需调用通用模块即可
基础框架层:
往往是根据功能来划分,可细分为网络支持功能、图片库、日志系统、数据库支持等模块
这一层目标是与具体业务解耦并对外提供良好的交互接口,后续的修改尽可能不在原来基础上修改而是采用扩展的方式,遵循开闭原则
lib库层:主要是三方的lib库,这些库为上层功能支持,如我们项目当前使用到的Glide,二维码等

Step2: 分层结构基础上进一步细化
为解决层与层以及同层之间的页面通信问题,引入数据处理框架
数据处理框架示意图:



















  1. 我们当前的项目中请求服务端数据比较直接,直接调用Fwk的接口,虽然该接口是我们自己实现做在Fwk中并已经做了封装,不过因接收到返回数据的处理逻辑很多都是做在视图层, 且页面间的数据交互是通过相互持有实例调用,页面间耦合严重
  2. 视图层收到报文后直接显示,缺少校验的过程,这部分逻辑若做在视图层,会增加代码量且不符合单一职责
  3. 因联网后有多处需要调用Fwk接口获取网络数据,比较分散,且开多个线程占用内存较多且和UI线程抢占CPU资源,有概率影响UI线程视图的刷新速度
  4. 将处理数据均放置在数据处理模块,便于后续问题定位,数据定位模块细分为网络数据上传,网络数据拉取,持久化数据存储,报文的解析校验等

数据处理模块在这里起到了一个加工数据并中转的角色,这样视图层和网络层就解耦了,避免了视图层直接和网络层交互,后续所有视图层需要和网络数据交互,视图界面之间的数据交互均通过数据处理模块,这样一来还有个好处就是视图层的代码量将大幅减少,视图页面间的耦合性消除,代码的可读性增


将所有需要和网络交互的数据获取和上传,以及数据库读写,File文件的读写,网络JSON数据的解析提取关键信息等数据处理相关的代码均放置在数据处理模块中


考虑到我们的Launcher需要在联网后做很多同步服务端的操作,比如同步时间,同步功能控制,同步天气等,这些操作实时性要求较高,需要当请求到达时,工作线程已经存在。故采取开启线程池以支持这些同步操作能够并发进行,节省了创建线程的过程,并能保证任务超过核心线程数时能得到复用,一定程度上节省了内存开销,另外一个好处是最大程度上加快联网数据获取,同时线程池默认background优先级,尽可能的保证UI线程的视图绘制优先进行,这样开机后Launcher加载时就不可能出现视图卡顿的现象。
线程池的理想大小取决于被提交任务的类型以及机器的处理器数量,线程池的大小需要避免“过大”和“过小”这两种极端情况

  1. 如果线程池过大,那么大量的线程将在相对很少的CPU和内存资源上发生竞争,这不仅会导致更高的内存使用量,而且还可能耗尽资源。
  2. 如果线程池过小,那么将导致许多空闲的处理器无法执行工作,从而降低吞吐率
    一般来说,核心线程数设置为2N+1(N为核数)


阿里java开发手册中有如下解释:
【强制】}新建线程时,必须通过线程池提供(AsyncTask 或者 ThreadPoolExecutor 或者其他形式自定义的线程池),不允许在应用中自行显式创建线程。 说明: 使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解 决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致 消耗完内存或者“过度切换”的问题。另外创建匿名线程不便于后续的资源使用分析, 对性能分析等会造成困扰。
【强制】}线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明: Executors 返回的线程池对象的弊端如下:
FixedThreadPoolSingleThreadPool: 允 许 的 请 求 队 列 长 度 为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM;
CachedThreadPoolScheduledThreadPool: 允 许 的 创 建 线 程 数 量 为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
当前的自定义线程池设置了核心线程数为5,当请求任务到达时,若池中暂无可用线程,则会走到拒绝策略中,将请求存在BlockingQueue中,待有闲置线程后,则继续执行
当前封装的线程池暴露出一个接口供Launcher中所有需要异步的地方使用,减少了内存的开销并且统一管理所有异步操作


考虑到任何数据源都无法保证能够返回合法的数据,如果不对数据错误进行容错处理,直接返回给视图层的话,会导致视图层无数据甚至异常。之前存在功能控制收到的报文中存在key对应的value是空的情况,Launcher直接Crash,这样很不友好,所以容错处理还是需要的,在数据处理模块中做数据的校验处理,确保返回给视图层的数据是否有效不知道,起码是合法的,不会出现空数据的情况,以保证视图层不会崩溃或显示异常。

Step2.3 如何和视图层通信?
前期研究
当前市面比较流行的是LiveData以及较早的EventBus,Otto等,其中基于LiveData的事件总线最为轻量且是谷歌官方支持,支持感知组件生命周期,且不需要解注册,对内存泄露都做了防控
为何不采用市面成熟的事件总线?

  1. 我们的工程是源码工程不是gradle工程,无法通过依赖添加,只有通过jar包引入,弊端是不方便升级
  2. 项目需要向下兼容,低版本上如4.4存在注解不能使用的情况
  3. LiveData需要依赖Android官方Android Architecture Components组件的LiveData,在gradle工程下比较方便,对于我们的源码工程且需要支持多平台,需要引入较多的依赖文件,且存在一定的学习成本
  4. 基于LiveData的事件总线框架成熟且功能强大,功能较多,引入需投入时间成本进行研究,不然后续若出问题排查会无从下手
    自行设计事件总线的原理
    对市面主流的eventbus做了研究,考虑到eventbus文件数多达近二十个,考虑后续方便移植且方便定位问题,考虑设计一个满足我们业务需求的简化版事件总线
    当前设计的事件总线采用类似订阅发布机制,支持一对一以及一对多通信,传递的消息实体支持泛型,核心当前只有两支文件,当前测试下来基本满足需求为方便应用使用,暴露的接口很简单,只有一个发送以及订阅接口,后续根据业务需要再看是否需要扩展

Step2.4 视图框架设计

  1. 主界面:
    考虑使用Fragment+ViewPagerFragment用来承载视图,优势在于轻量,加载速度快
    菜单页均是RecyclerViewListView展示数据,考虑到这些ListView均需要ViewHolder来填充视图,需要Adapter来填充数据,如果每个需要ListView的界面都维护各自的一套ViewHolder及Adapter,那么页面逻辑变得臃肿,整个Launcher的代码量也会比较大,所以这里考虑采用如下方式:
    封装一个Adapter公共处理类,提供多种构造函数,其中有一个type参数,用来标明使用哪个ViewHolder,在Adapter的getView方法中,根据type参数,获取具体的ViewHolder实现
    经过封装之后,视图层菜单页只需要向Adapter公共处理类传入一个type参数即可得到对应的Adapter,等数据返回到视图层后,再将数据传给Adapter公共处理类,其他什么都不用管,就可以展示列表数据了。这样设计的目的是将公共的行为抽取出来,大幅度减少菜单页的代码量,后续增加菜单页也会变得很简单。
  2. 下拉栏界面
    分为statusbar以及通知栏,这里的诸多数据均需要调用系统的接口,比如电量,信号格,数据类型,将这些接口单独放在公共模块中,便于其他页面也能调用到
  3. 表盘界面
    即待机界面,包括长按后进入表盘样式选择界面,这里已经做好了基类封装公共行为的操作,使得表盘样式的扩展变得很容易,只需要继承基类,改下布局文件即可

Step2.5 代码结构划分
当前代码结构划分如下:




































老项目上上存在的代码问题

  1. 存在不再使用的库文件但是并未移除,对库文件拿来直接用,对于所需功能过大过全,缺少裁剪过程,使用直接调用,与本身应用代码耦合度高,建议二次封装;
  2. 重复性代码较多,如接收心跳,监测电量,熄灭屏等
  3. 变量名和方法名很随意,建议驼峰命名,缺少注释,可读性差,添加类文件和文件夹,后面因为种种原因废弃,由于缺少注释且代码可读性差,导致成为僵尸代码,不利于后续接手人员维护。
  4. 逻辑过于冗长的方法,一大堆的if else,建议拆分优化;
  5. 没有考虑一些边界条件,比如请求失败,没有数据的情况,缺少容错处理;
  6. 代码存在很多容易造成空指针代码,最常见调用equals方法时,未遵循 “常量”.equals(变量)
  7. static滥用,为内存泄露埋下伏笔
  8. 异步线程的滥用,使用new Thread方式过于简单粗暴,一个线程占用约1M内存,过多的开启会耗用不少内存
  9. 调用大多通过直接new对象方式持有引用,占用内存且强耦合,针对不同场景考虑单例或消息总线通信
  10. 代码美观方面,缺少缩进对齐随意空行,代码不规整
  11. 新加图片未进行压缩,建议png 图片使用 tinypng 或者类似工具压缩处理,减少包体积
  12. 对主线程子线程运行环境存在使用不当,Activity的生命周期方法中,广播的onreceive方法以及普通service中,存在耗时操作
  13. 修改已有稳定方法比较随意,往往塞进一大堆代码,带来隐患,建议进行扩展而不是修改原有设计,或者新增单独接口
  14. 对异步任务被中断情况,缺少资源清理,如AsyncTask或Handler异步更新UI,对任务未完成界面被退出情况,需要在ondestory或onPause中进行任务清理
  15. 过度的try catch,特别是catch了Exception这种基类,导致存在异常都被catch掉,将bug隐蔽起来,导致后期排查比较困难
  16. 界面间随意传递上下文context,为内存泄漏埋下伏笔

good night!
CATALOG