关于近期对项目性能和稳定性优化的总结

重构原因

由于公司是购买的成品项目,由于开发人员的不断更换,且没有成熟的代码规范要求,也没有代码 review ,所以导致项目代码混乱,臃肿,直接导致线上 ANR、Crash率增高,稳定性堪忧,经常出现各种奇葩问题,好评率降低,所以开始了长期的优化工作。最近优化又到了一定的阶段,总结了这段时间优化的东西。以防以后再导致类似的问题发生。

重构步骤

代码结构

项目中使用的是市面上用的最多的 MVP 的结构,但是经过多年的努力和发展,V 层已经取缔了 M 层和 P层就百分之九十九的功能,实现了一个类将近万行的代码。所以拆分结构放在了第一步。充分利用 MVP 的特性,并且由于原本 MVP 架构的原因,原本架构中 P 层获取数据存储到 M 层,在转发到 V层,由于重点重构的模块是属于实时获取大量高频率一次用完就舍弃的特性,先存储再利用就不合适了,所以也需要对这个MVP的使用需要差别对待了。具体实施就不记录了。

卡顿优化

我是搞导航软件的,导航卡顿是很明显的问题,所以针对导航,做了很多性能方面的优化,优化后导航的卡顿改善了,经过产品部门统计用户评分和用户平均使用市场均以增加。下面说几个优化的点

CPU 线程相关

CPU 的合理利用是性能优化的很重要的因素,这也是为什么开启线程池线程的数量总要考虑到CPU的内核数量的原因。所以针对导航初始化慢,重新算路卡顿的原因,再经过对业务的梳理发现了以下关于CPU线程使用问题:

  1. 初始化导航除了地图以外,还有十几个额外增加导航功能的业务需要同时初始化。
  2. 每次重新计算路线都需要重新计算那十几个任务。
  3. 稳定导航的时候线程数不断的跳跃,且线程数偏高。
  4. 每隔几秒钟导航的图标会卡顿一次,频率比较稳定。
  • 针对上面的第一点和第二点做的优化是,将初始化任务分为两类,
    • 第一类为导航必须任务,这类任务执行快慢会影响导航的开始的速度。
    • 第二类是增强导航功能类别,这类是完善导航信息的,不执行也不影响开始导航。

针对上面的问题,设计思想如下图:
在这里插入图片描述
非必要任务以单线程根据任务优先级执行,第一可以降低CPU并发任务量,第二可以把CPU的任务主要给到必须任务,增加主要任务响应速度。第三发生重置的时候未进行的任务可以直接停止不再执行。当然需要注意的是其中某一个任务执行失败不能影响下一个任务的执行,并且增加重试功能。还可以通过线程优先级,把非必要任务降低优先级。

  • 线程数不断跳跃

线程数不断跳跃是因为线程不断的重建和回收,说明没有很好的利用线程池,我们项目中用了Rxjava 来处理线程调> > > 度,经过查看代码有很多代码使用的是 NewThread ,导航中会不断的调用该任务,所以需要更换其他线程池。

  • 页面 UI 有规律的卡顿

这一点,通过工具检测主线程的任务,发现了后台有定时任务,如根据位置变化不断的查询周边数据库,这个优化点有两个:

  1. 导航外部订阅的 Service 或者 Activity 中的订阅,需要在 Activity onPause() 以后或者导航开始以后停止任务,因为除了损耗性能以外,没什么太大的价值。
  2. Rxjava 用法错误,Observable.just(Param); just 操作符如果传入了一个函数,则该函数不会经过Rxjava的线程调度切换线程,只会在当前线程执行,经过检测有just 中查询数据库的,导致查询数据库时 UI 卡顿 (其实最好是在封装数据库查询时检验线程,如果线程不对抛出异常) ,just() 最好只传一个变量进来,其他操作符同理。
  3. 第三方SDK的问题,看似只是获取一个变量,但是实际上它内部不一定经过多少道工序,所以最好在子线程中处理。好多 ANR 也是在主线程中获取大量sdk内部数据导致的。
内存优化

java 的 GC 大家都知道,触发 GC 就代表整个世界都停止了。减少内存分配和回收确实可以增加项目的体验。

  • 图片
  • 我这个项目地图上需要画大量的图片,虽然是封装到了地图的view中,但是在创建的时候就考虑内存是否可以像 Bitmap 一样复用内存空间?经过调研文档和实验,这种方案确实可以减少很大的内存开辟和释放的损耗。
  • 根据 View 的大小压缩图片的大小,三方 SDK 一般都返回的图片比较高清,实际上展示的位置根本不需要这么高分辨率的图片,所以可以动态根据view的大小来进行压缩。
  • 对象池

导航中需要不断的更新各种数据,很明显大量不断实时更新并且不需要缓存数据的任务,对象池是跑不掉了,这也是减少内存 GC 次数不错的方案。

  • 内存泄漏问题

通过内训检测,发现了很多内存泄漏的问题,这种问题比较普遍了,监听未释放等导致的。我查到的举几个例子:Rxjava 订阅未取消,传入回调为释放等。内部类导致的内存泄漏(不过 Kotlin 传递lambda表达式这种方式不一样,也是回调但是是通过函数实现的不是内部类,所以没有对外部类的引用)

其他优化

序列化
  • 因为我们项目需要缓存路线数据,以方便下次打开 App 可以继续导航,所以缓存长路线时,使用 SharedPreferences 序列化时内存占用大,效率低。可以采用 MMKV 来替代,它对数据进行 protobuf 编码,并且通过 mmap 直接和内存交互,大大增加了保存效率。
取消单例模式
  • 原本单例模式是方便大家使用的,但是由于缺少获取数据的权限控制,导致全局数据各个组之间随意获取随意存储,不仅仅是重构的时候,随着业务扩展,单例模式就已经带来很大的麻烦,比如原本的需求是全局唯一的数据,现在改成可以动态传入多条数据了,那么再获取单例就不足够支持业务需求了。并且随着重构的进行,大量代码绑定了全局单例的数据等,用的时候真爽,改起来真麻烦。并且随意获取并且改动在项目数据稳定性上影响很大,可能会导致数据混乱,尤其是并发编程时的操作。
  • 重构时我们取消了大量全局直接引用的方式,需要数据的地方改为传入数据。并且需要用到全局唯一数据的时候增加权限校验,并且只许获取不许修改(获取对象的话可以采用深拷贝的方式,虽然会消耗一部分内存,但是由于没有专人限制代码审查的话,跟系统稳定性相比这样的处理在数据量不大的情况时非常合适的)。
接口隔离的好处

以前开发的时候总觉得,定义那么多接口,也没有那么多实现类有啥用,只有当做了大量的重构才知道,定义接口其一是为了限制外部调用(虽然将方法设置成 private 也是能达到同样效果),但是在重构的时候,只需要对外保留接口中开放的方法和返回值,其他的可以随意动,也就是说我只要不修改对外提供的门脸,其他内部随便搞,对重构是很有好处的。

代码注释和命名规范

缺少代码注释,并且也不能见名知意,是重构过程中很头疼的事情,并不是所有的模块所有的代码都是我一个人写的,经常要分析好久才知道这一段代码是什么功能,再考虑是否可以进行什么优化。所以说代码注释和代码结构一样重要,可以大大节约后期维护的成本。

并发引起的数据问题

由于经常有多线程去完善同一个数据源的问题,所以由于并发引起的数据错误,甚至是异常,导致项目使用出错。这对导航来说是致命的,会导致展示的数据和当前路线不匹配,会误导司机行驶。所以优化这块主要有两种方法,提供数据工厂,隔离数据源的并发操作,数据源合并数据加锁等操作。

谋定而后动

我经常跟同事开玩笑说这句话,也确实是,编码时间其实不需要太长,我记得有本书中写到编码时间应该只占需求整个流程的百分之十以下,编码时间长也不一定写出好代码,应该前期多规划,多思考,搭建架构,提前尽量预知问题,着急编码并不是一个好的选择,前期的节省后期需要投入多几倍的精力,得不偿失,我这个项目此次的重构也大概就是如此了。

猜你喜欢

转载自blog.csdn.net/ldxlz224/article/details/130179362