移动端SDK的优化之路2
极限优化
所谓极限优化,是指从多个角度、维度对sdk进行优化,重点是考虑网络优化以及电量消耗优化。能够做到代码精简,低网络流量,微能耗而不仅仅是低能耗。
香农定理是所有通信制式最基本的原理,我们知道C=B lb(1+S/N)
其中:C是信道支持的最大速度或者叫信道容量,B是信道的带宽,S是平均信号功率,N是平均噪声功率,S/N即信噪比。
从最初的1G网络到现在的4G网络,都是在利用这个公式提高速度。要么充分利用频道资源,要么提高整体带宽。但是频段资源都是有限的,所以不得不制定出更优秀的策略来提高资源的利用率。结合网络情况、手机电量等因素,我们采取以下几种方式进行优化:
1)合并网络请求,减少服务器压力和dns请求时间,减少手机的网络流量。
2)数据缓存到本地,最省电的方式就是不使用移动网络,数据缓存能大大减少网络请求的次数。
3)日志上报策略,批量非实时上报。日志生成后,首先存储在RAM中,基础策略是满30条发送,每隔一分钟轮询一次。为了满足客户定制需求,发送策略可通过后台配置。如果遇到异常情况,比如网络异常或者crash等,我们会将日志存储在本地sqlite中,在程序下次启动后,根据发送策略再次发送。
Image为了减少app的网络流量消耗,我们还将活动的图片新增了WebP的格式。
ImageWebP格式的图片好处是什么?举个例子,做一个简单的测试对比PNG 原图、PNG 无损压缩、PNG 转WebP(无损)、PNG 转WebP(有损)的压缩效果。
可以得出结论:PNG 转WebP 的压缩率要高于PNG 原图压缩率,同样支持有损与无损压缩。
转换后的WebP 体积大幅减少,图片质量也得到保障(同时肉眼几乎无法看出差异)。转换后的WebP 支持Alpha 透明和 24-bit 颜色数,不存在 PNG8 色彩不够丰富和在浏览器中可能会出现毛边的问题。
WebP 的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量。除此之外,国内外很多知名的应用已经使用了WebP格式,这也是我们使用它的原因之一。
在3.8版本的sdk中,用于活动的Marketing接口会返回PNG和WebP两种格式的图片。对于Android而言,如果操作系统版本在4.0以及4.0之后,它天生支持WebP格式,sdk会优先加载这种格式,加载不成功才会去加载PNG的图片。如果是Android 4.0以下,sdk只加载PNG图片。
对于iOS而言,目前iOS本身不支持WebP格式(但愿iOS10会支持它:(),要借助第三方库才能支持,比如SDWebImage。但是iOS sdk已经足够大了,不可能把SDWebImage集成到sdk。所以,目前iOSsdk不会像androidsdk一样存在imageloader,iOSsdk把图片加载的权利交给开发者。当然以后,我们肯定会给iOSsdk提供类似android的imageloader的功能。
借助Webp,我们替用户节省了流量,节省了手机内存和CPU资源。
未来,网络请求还会进一步优化。会考虑使用protobuf协议替换现在的返回json格式。protobuf返回的数据更小,而且是二进制的格式。从安全性的角度上说,在一定程度上能够防止被恶意抓取数据包进行分析。
第三方组件替换
对于移动端sdk的开发者来说,移动端其余的开发人员都是幸福的。他们可以尝试使用无数的第三方库,在github上每天都会诞生很多优秀的第三方库。sdk的开发者不得不自己去实现很多功能,因为考虑到sdk大小的问题。
对于sdk的开发者来说“这是一个最好的时代,也是一个最坏的时代”。他们必须自己去“造轮子”,但是会给他们带来更多收获,无论是接触到os的底层还是设计模式,都会比普通的开发者了解更多。
我们魔窗的sdk包括Androd、iOS版本在不断迭代的过程中,都经历过第三方组件的替换。以android为例,我们替换了json解析器和网络框架等等。
最初,我们使用fastjson,它是由阿里巴巴的工程师编写的,性能和稳定性都很好。我自己写app时,也会首选它作为json的解析器。但是它明显增大了sdk的体积,于是我们使用gson替换了fastjson。用了一段时间后,觉得gson还是很大。
最终,我们考虑重写jsonparser。重写的jsonparser,必须能兼容原先gson的一些api,避免sdk工程做太大的改动,这是我们重写的一个目标。
重写jsonparser之前,我们先对反射做了一次封装。传统的反射是这样写的:
Image封装之后的写法是这样的,基于流式API:
Image依托于简洁的反射,实现了自己的jsonparser。除此之外,还需要将http请求返回的结果借助自己的json工具类转换成对象、对象数组。类似于这样:
Image借助这个反射我们还获得的额外好处是,在android4.0以后的版本能够随时获取到App的ApplicationContext,以前还担心获取不到ApplicationContext,这样一来还能防止memory leak。因为,Activity的Context使用不当经常会引起内存泄露。
Image另一个被替换的第三方组件是volley。它是google开发的网络框架,便于android应用操作网络。替换volley的原因,是它功能太强大了,简直就是一个“全家桶”。我们用不到那么多功能,sdk需要的是一个符合自身业务需求的网络框架。同样,替换的准则是能够兼容原先volley的大部分api。于是我们做了一个简化版本的volley,它大致的流程如下图所示:
Image它最主要的四个部分是:Request、RequestQueue、NetworkExecutor和ResponseDelivery。
Request,即各种请求类型。包括StringRequest和ImageRequest,分别表示返回的数据是字符串和网络图片的请求。Request支持Get、Post请求,支持header、支持请求缓存、支持postbody、支持请求的重试机制。Request类还包含了一个回调处理的接口ResponseListener。
第二部分为消息队列RequestQueue,消息队列维护了提交给网络框架的请求列表,并且根据相应的规则进行排序。默认情况下更具优先级和进入队列的顺序来执行,该队列使用的是线程安全的PriorityBlockingQueue,因为我们的队列会被并发的访问,因此需要保证访问的原子性。
第三部分是NetworkExecutor,它是网络的执行者。该Executor继承自Thread,在run方法中循环访问第二部分的请求队列,请求完成之后将结果投递给UI线程。为了更好的控制请求队列,例如请求排序、取消等操作,这里我们并没有使用线程池来操作,而是自行管理队列和Thread的形式,这样整个结构也变得更为灵活。它的主要代码是这样的:
Image其中,doRequest()方法用于真正的网络请求和分发网络请求返回的Response。doRequest()支持重试机制,它的大致流程如下图所示:
第四部分是ResponseDelivery,在第三部分的Executor中执行网络请求,Executor是Thread,但是我们并不能在主线程中更新UI,因此我们使用ResponseDelivery来封装Response的投递,保证Response执行在UI线程。
总之,每个部分都符合单一职责的原则,便于日后的独立维护。
我们再看看怎么借助这个网络框架如何调用httppost请求。
Image一. NeteaseAPM是什么
对于普通的app开发来说,小版本快速迭代几乎是不可或缺的方法论。而对于sdk开发而言,“小步快跑,快速迭代”的策略不再适用。我们必须采取相对稳健的更新策略。
sdk是面向所有的开发者使用的,高版本必须向下兼容api。如果某个api确实需要过期的时候,至少保留几个版本后再删除过期的api,并附有详细的说明文档。
对于sdk而言,版本发布也不宜频繁,否则会让开发者会感觉自己是“小白鼠”。这样的体验,对于开发者是相当不友好的。
对于每一个小版本除了新增的功能之外,我们都会集中精力优化好某一块地方。每一个小版本都是“小步迭代”,但是经过几个版本的迭代之后,还是能够实现量变。下面的表格是我开始接手魔窗sdk之后,androidsdk体积的大小的变化。
image.png
从3.0到3.7版本,android sdk的大小,总体趋势是不断减少的。其实功能不断增加的,sdk的稳定性也得到提升,这就是我们采用小版本不断迭代带来的好处。
未来,sdk拆分
关于未来,我们追求的是在保证sdk稳定的前提下,继续努力减少sdk的大小。将我们的sdk拆分成多个组件,供用户挑选自己想要的各个组件。我们目前sdk的模块如下图所示。
Imagesdk最核心的部分是sdkcore,它是sdk必不可少的组成部分。它有以下几部分组成:
1)http组件,是我们自己开发的http模块,符合自己的业务需求。
2)imageloader组件,在sdk中显示活动图片的组件,是自己开发的模块。
3)domain,是sdk所需要的对象,包括http返回的对象以及业务模型。
4)config组件,是sdk必须的配置组件。
5)jsonparser组件,json解析器,是我们自己开发的模块。
6)utils,sdk中各种帮助工具类。
7)sqlite组件,操作数据库的相关类,把一些数据缓存到sqlite数据库。
其余的组件虽然没那么重要,但是可以通过自由组合的方式,组成开发者想要的功能。这是我们未来1-2月的努力方向——sdk的拆分。将sdk拆成更小更细粒度的模块,开发者也能更好地选择他们想要的模块。
比如一个开发者只想要tracking功能,那么他只需使用sdkcore包和tracking包。再比如一个开发者只想要mLink(基于deeplink深度改造)的功能,那么他会需要sdkcore包、tracking包、magicwindowview包和mLink包这几个包。
Ending
sdk无论怎么拆分,稳定性是最最重要的。它涉及到使用sdk的所有app,以及app背后的无数用户。作为sdk的开发者,必须对用户负责,要抱有一颗敬畏之心。
经历sdk的拆分之后,我们会逐步开源sdk的功能到github社区,接受所有开发者的监督。
QA环节
Q:sdk的耗电优化,请问你们在开发过程中有遇到哪些耗电问题没? A:一开始我们没有合并一些必要的网络请求,会导致耗电。后来我们做了优化,并对上报频率进行优化。
Q:网络通信中必然涉及到加密,对于sdk本地秘钥您是怎么确保其安全性的?
A:sdk首先是经过混淆的,所以可以保证密钥的安全性。
Q:通讯安全通过什么保障?
A:重要的接口用https,我们还有一个网关系统,在网关系统中有限流、黑名单机制、IP策略等等。
Q:webp图片压缩是调用第三分组件还是自己实现的,有没有推荐的
A:我们是自己写的,因为考虑的sdk的大小。推荐的话肯定是facebook的fresco这样的全家桶。
Q:你们的sdk日志是存sqlite吗?能不能直接存成文件形式在sd卡里面?是考虑到安全问题才存成数据库的吗?
A:一般放在RAM中批量上报日志,除非app crash了才会存sqlite中,等下次app启动把sqlite中的日志上传。目前是基于安全性考虑才存数据库。
Q: iOS SDK的体积本就大,支持bitcode 时更大。除了图片资源,组件选择之外,有更多iOS SDK的瘦身经验或提示或前沿新技术吗?
A:目前我们SDK去除了所有的第三方,全部调用原生的API,下一步考虑将SDK拆分。
Q:在产品格式的选择上,.a, framework, 静态库,动态库方面如何做选择?
A:考虑到App上架的问题,SDK肯定是优先选择静态库。
.a+.h+source=framwork,但是我们SDK中加了微信分享功能,所以优先选择了.a