BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

移动端SDK的优化之路

| 作者 沈哲 关注 0 他的粉丝 发布于 2016年5月25日. 估计阅读时间: 18 分钟 | Google、Facebook、Pinterest、阿里、腾讯 等顶尖技术团队的上百个可供参考的架构实例!

嘉宾介绍

沈哲,擅长移动端、互联网后端技术,曾经在安硕信息、decarta(已被uber收购)、京东商城等国内外知名软件公司、互联网公司工作。开发过decarta第一款地图导航app,今夜酒店特价app,负责过京东到家上海的移动端团队。现负责魔窗移动端团队,负责研发魔窗的sdk以及移动端相关产品。

本人自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:

  1. 脚本构建
  2. 极限优化(网络、日志上报、图片格式等方面优化)
  3. 第三方组件替换
  4. 小版本稳步迭代

脚本构建

我们从开始开发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中,在程序下次启动后,根据发送策略再次发送。

(点击放大图像)

为了减少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社区,接受所有开发者的监督。

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


感谢徐川对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT