本人自2015年9月底加入魔窗,开始着手优化魔窗移动端sdk的工作。
魔窗是基于Deep Link技术的开放平台,通过提供生态落地最后一公里的deep link、跨App store渠道的归因分析以及场景还原(deferred deep link)等解决方案为App开发者构建一个去中心化的高效连接时代。最重要的产品就是iOS和Android端的SDK。
sdk优化过程,是一段血泪史,可以吐槽的地方无数。移动端sdk不像app一样方便,sdk发布后出现任何问题,都会影响到很多家的app。不能像一家app一样,可以及时发布一个hotfix,或者强制升级app,又或者热更新app。所以sdk发版之前,必须经过严格的测试,每一次sdkhotfix的发布都会对我们的用户造成严重的影响。
sdk的优化,最大的痛点是它的大小。每次对接客户,他们都会问我们sdk的大小是多少?每当提到iOSsdk时,他们都会说还蛮大的,他们自己家的app都已经几十M了,接入我们的sdk会增加他们app的大小。所以,不得不开始痛苦的sdk优化之路。
我们主要从以下几个方面进行优化sdk:
脚本构建
极限优化(网络、日志上报、图片格式等方面优化) 第三方组件替换 小版本稳步迭代 脚本构建
我们从开始开发sdk到目前正在开发中的3.8版本,一直推崇借助脚本进行自动化打包,例如android使用gradle。借助脚本的好处在于:
1)androidsdk混淆
2)自动生成文档,便于开发者查阅,例如android可以很方便的生成javadoc文档
3)androidsdk上传aar包,iOSsdk发布到cocoa-pods,便于开发者集成
4)节省人工时间,减少出错
脚本通常能帮助我们实现很多自动化的事情,能提高工作效率的方法是一定会被采纳的。
接下来我们来看看借助gradle如何实现sdk混淆,核心的task是proguardJar这个task。
(点击放大图像)
极限优化
所谓极限优化,是指从多个角度、维度对sdk进行优化,重点是考虑网络优化以及电量消耗优化。能够做到代码精简,低网络流量,微能耗而不仅仅是低能耗。
香农定理是所有通信制式最基本的原理,我们知道C=B lb(1+S/N)
其中:C是信道支持的最大速度或者叫信道容量,B是信道的带宽,S是平均信号功率,N是平均噪声功率,S/N即信噪比。
从最初的1G网络到现在的4G网络,都是在利用这个公式提高速度。要么充分利用频道资源,要么提高整体带宽。但是频段资源都是有限的,所以不得不制定出更优秀的策略来提高资源的利用率。结合网络情况、手机电量等因素,我们采取以下几种方式进行优化:
1)合并网络请求,减少服务器压力和dns请求时间,减少手机的网络流量。
2)数据缓存到本地,最省电的方式就是不使用移动网络,数据缓存能大大减少网络请求的次数。
3)日志上报策略,批量非实时上报。日志生成后,首先存储在RAM中,基础策略是满30条发送,每隔一分钟轮询一次。为了满足客户定制需求,发送策略可通过后台配置。如果遇到异常情况,比如网络异常或者crash等,我们会将日志存储在本地sqlite中,在程序下次启动后,根据发送策略再次发送。
(点击放大图像)
相关厂商内容
想要快速学习Amazon EMR最佳实践,赶快报名InfoQ在线课堂 ArchSummit 晚场活动对外招募,邀您和架构师畅聊技术 如何快速搭建一个完整的移动端直播系统 你离成为一位合格的技术领导者还有多远? 证券行业的Docker应用实践相关赞助商
GMTC全球移动技术大会2016年6月24日-25日,北京,点击了解详情!
为了减少app的网络流量消耗,我们还将活动的图片新增了WebP的格式。
(点击放大图像)
WebP格式的图片好处是什么?举个例子,做一个简单的测试对比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之前,我们先对反射做了一次封装。传统的反射是这样写的:
(点击放大图像)
封装之后的写法是这样的,基于流式API:
依托于简洁的反射,实现了自己的jsonparser。除此之外,还需要将http请求返回的结果借助自己的json工具类转换成对象、对象数组。类似于这样:
(点击放大图像)
借助这个反射我们还获得的额外好处是,在android4.0以后的版本能够随时获取到App的ApplicationContext,以前还担心获取不到ApplicationContext,这样一来还能防止memory leak。因为,Activity的Context使用不当经常会引起内存泄露。
(点击放大图像)
另一个被替换的第三方组件是volley。它是google开发的网络框架,便于android应用操作网络。替换volley的原因,是它功能太强大了,简直就是一个“全家桶”。我们用不到那么多功能,sdk需要的是一个符合自身业务需求的网络框架。同样,替换的准则是能够兼容原先volley的大部分api。于是我们做了一个简化版本的volley,它大致的流程如下图所示:
它最主要的四个部分是:Request、RequestQueue、NetworkExecutor和ResponseDelivery。
Request,即各种请求类型。包括StringRequest和ImageRequest,分别表示返回的数据是字符串和网络图片的请求。Request支持Get、Post请求,支持header、支持请求缓存、支持postbody、支持请求的重试机制。Request类还包含了一个回调处理的接口ResponseListener。
第二部分为消息队列RequestQueue,消息队列维护了提交给网络框架的请求列表,并且根据相应的规则进行排序。默认情况下更具优先级和进入队列的顺序来执行,该队列使用的是线程安全的PriorityBlockingQueue,因为我们的队列会被并发的访问,因此需要保证访问的原子性。
第三部分是NetworkExecutor,它是网络的执行者。该Executor继承自Thread,在run方法中循环访问第二部分的请求队列,请求完成之后将结果投递给UI线程。为了更好的控制请求队列,例如请求排序、取消等操作,这里我们并没有使用线程池来操作,而是自行管理队列和Thread的形式,这样整个结构也变得更为灵活。它的主要代码是这样的:
其中,doRequest()方法用于真正的网络请求和分发网络请求返回的Response。doRequest()支持重试机制,它的大致流程如下图所示:
第四部分是ResponseDelivery,在第三部分的Executor中执行网络请求,Executor是Thread,但是我们并不能在主线程中更新UI,因此我们使用ResponseDelivery来封装Response的投递,保证Response执行在UI线程。
总之,每个部分都符合单一职责的原则,便于日后的独立维护。
我们再看看怎么借助这个网络框架如何调用httppost请求。
一. NeteaseAPM是什么
对于普通的app开发来说,小版本快速迭代几乎是不可或缺的方法论。而对于sdk开发而言,“小步快跑,快速迭代”的策略不再适用。我们必须采取相对稳健的更新策略。
sdk是面向所有的开发者使用的,高版本必须向下兼容api。如果某个api确实需要过期的时候,至少保留几个版本后再删除过期的api,并附有详细的说明文档。
对于sdk而言,版本发布也不宜频繁,否则会让开发者会感觉自己是“小白鼠”。这样的体验,对于开发者是相当不友好的。
对于每一个小版本除了新增的功能之外,我们都会集中精力优化好某一块地方。每一个小版本都是“小步迭代”,但是经过几个版本的迭代之后,还是能够实现量变。下面的表格是我开始接手魔窗sdk之后,androidsdk体积的大小的变化。
从3.0到3.7版本,android sdk的大小,总体趋势是不断减少的。其实功能不断增加的,sdk的稳定性也得到提升,这就是我们采用小版本不断迭代带来的好处。
未来,sdk拆分
关于未来,我们追求的是在保证sdk稳定的前提下,继续努力减少sdk的大小。将我们的sdk拆分成多个组件,供用户挑选自己想要的各个组件。我们目前sdk的模块如下图所示。
sdk最核心的部分是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社区,接受所有开发者的监督。