魔窗沈哲:移动端 SDK 的优化之路

摘要

sdk 的优化, 最大的痛点是它的大小。每次对接客户, 他们都会问我们 sdk 的大小是多少? 每当提到 iOSsdk 时, 他们都会说还蛮大的, 他们自己家的 app 都已经几十 M 了, 接入我们的 sdk 会增加他们 app 的大小。所以, 不得不开始痛苦的 sdk 优化之路。

屏幕快照 2016-05-17 15.45.43.png

本文作者沈哲,擅长移动端、互联网后端技术, 曾经在安硕信息、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。

屏幕快照 2016-05-17 15.46.54.png

极限优化

所谓极限优化, 是指从多个角度、维度对 sdk 进行优化, 重点是考虑网络优化以及电量消耗优化。能够做到代码精简, 低网络流量, 微能耗而不仅仅是低能耗。香农定理是所有通信制式最基本的原理, 我们知道 C=B lb(1+S/N) 其中:C 是信道支持的最大速度或者叫信道容量,B 是信道的带宽,S 是平均信号功率,N 是平均噪声功率,S/N 即信噪比。从最初的 1G 网络到现在的 4G 网络, 都是在利用这个公式提高速度。要么充分利用频道资源, 要么提高整体带宽。但是频段资源都是有限的, 所以不得不制定出更优秀的策略来提高资源的利用率。结合网络情况、手机电量等因素, 我们采取以下几种方式进行优化:

1) 合并网络请求, 减少服务器压力和 dns 请求时间, 减少手机的网络流量。

2) 数据缓存到本地, 最省电的方式就是不使用移动网络, 数据缓存能大大减少网络请求的次数。

3) 日志上报策略, 批量非实时上报。日志生成后, 首先存储在 RAM 中, 基础策略是满 30 条发送, 每隔一分钟轮询一次。为了满足客户定制需求, 发送策略可通过后台配置。如果遇到异常情况, 比如网络异常或者 crash 等, 我们会将日志存储在本地 sqlite 中, 在程序下次启动后, 根据发送策略再次发送。

屏幕快照 2016-05-17 15.47.45.png

为了减少 app 的网络流量消耗, 我们还将活动的图片新增了 WebP 的格式。

屏幕快照 2016-05-17 15.47.51.png

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 之前, 我们先对反射做了一次封装。传统的反射是这样写的:

屏幕快照 2016-05-17 15.48.40.png

封装之后的写法是这样的, 基于流式 API:

屏幕快照 2016-05-17 15.48.48.png

依托于简洁的反射, 实现了自己的 jsonparser。除此之外, 还需要将 http 请求返回的结果借助自己的 json 工具类转换成对象、对象数组。类似于这样:

屏幕快照 2016-05-17 15.49.29.png

借助这个反射我们还获得的额外好处是, 在 android4.0 以后的版本能够随时获取到 App 的 ApplicationContext, 以前还担心获取不到 ApplicationContext, 这样一来还能防止 memoryleak。因为,Activity 的 Context 使用不当经常会引起内存泄露。

屏幕快照 2016-05-17 15.49.36.png

另一个被替换的第三方组件是 volley。它是 google 开发的网络框架, 便于 android 应用操作网络。替换 volley 的原因, 是它功能太强大了, 简直就是一个「全家桶」。我们用不到那么多功能,sdk 需要的是一个符合自身业务需求的网络框架。同样, 替换的准则是能够兼容原先 volley 的大部分 api。于是我们做了一个简化版本的 volley, 它大致的流程如下图所示:

屏幕快照 2016-05-17 15.50.15.png

它最主要的四个部分是:Request、RequestQueue、NetworkExecutor 和 ResponseDelivery。Request, 即各种请求类型。包括 StringRequest 和 ImageRequest, 分别表示返回的数据是字符串和网络图片的请求。Request 支持 Get、Post 请求, 支持 header、支持请求缓存、支持 postbody、支持请求的重试机制。Request 类还包含了一个回调处理的接口 ResponseListener。第二部分为消息队列 RequestQueue, 消息队列维护了提交给网络框架的请求列表, 并且根据相应的规则进行排序。默认情况下更具优先级和进入队列的顺序来执行, 该队列使用的是线程安全的 PriorityBlockingQueue, 因为我们的队列会被并发的访问, 因此需要保证访问的原子性。第三部分是 NetworkExecutor, 它是网络的执行者。该 Executor 继承自 Thread, 在 run 方法中循环访问第二部分的请求队列, 请求完成之后将结果投递给 UI 线程。为了更好的控制请求队列, 例如请求排序、取消等操作, 这里我们并没有使用线程池来操作, 而是自行管理队列和 Thread 的形式, 这样整个结构也变得更为灵活。它的主要代码是这样的:

屏幕快照 2016-05-17 15.50.23.png

其中,doRequest() 方法用于真正的网络请求和分发网络请求返回的 Response。doRequest() 支持重试机制, 它的大致流程如下图所示:

屏幕快照 2016-05-17 15.51.08.png

第四部分是 ResponseDelivery, 在第三部分的 Executor 中执行网络请求,Executor 是 Thread, 但是我们并不能在主线程中更新 UI, 因此我们使用 ResponseDelivery 来封装 Response 的投递, 保证 Response 执行在 UI 线程。总之, 每个部分都符合单一职责的原则, 便于日后的独立维护。我们再看看怎么借助这个网络框架如何调用 httppost 请求。

屏幕快照 2016-05-17 15.51.16.png

NeteaseAPM 是什么

对于普通的 app 开发来说, 小版本快速迭代几乎是不可或缺的方法论。而对于 sdk 开发而言,「小步快跑, 快速迭代」的策略不再适用。我们必须采取相对稳健的更新策略。sdk 是面向所有的开发者使用的, 高版本必须向下兼容 api。如果某个 api 确实需要过期的时候, 至少保留几个版本后再删除过期的 api, 并附有详细的说明文档。对于 sdk 而言, 版本发布也不宜频繁, 否则会让开发者会感觉自己是「小白鼠」。这样的体验, 对于开发者是相当不友好的。

屏幕快照 2016-05-17 15.52.27.png

对于每一个小版本除了新增的功能之外, 我们都会集中精力优化好某一块地方。每一个小版本都是「小步迭代」, 但是经过几个版本的迭代之后, 还是能够实现量变。下面的表格是我开始接手魔窗 sdk 之后,androidsdk 体积的大小的变化。

从 3.0 到 3.7 版本,android sdk 的大小, 总体趋势是不断减少的。其实功能不断增加的,sdk 的稳定性也得到提升, 这就是我们采用小版本不断迭代带来的好处。

未来,sdk 拆分关于未来, 我们追求的是在保证 sdk 稳定的前提下, 继续努力减少 sdk 的大小。将我们的 sdk 拆分成多个组件, 供用户挑选自己想要的各个组件。我们目前 sdk 的模块如下图所示。

屏幕快照 2016-05-17 15.53.20.png

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

头图来自华盖创意

最新文章

极客公园

用极客视角,追踪你不可错过的科技圈.

极客之选

新鲜、有趣的硬件产品,第一时间为你呈现。

张鹏科技商业观察

聊科技,谈商业。