主持人:我们本期的分享嘉宾为极光高级Android工程师王可为,我们极光是做SDK的,SDK跟APP的性能应用这一块儿还是有很大的关联的,我也不是技术的,把时间留给我们的讲师。他演讲的主题是:移动端SDK优化的特点与经验分享

王可为:大家好!首先感谢网红徐老师的精彩分享,我代表极光感谢各位讲师各位到场的朋友到来,我做一个自我介绍。我是极光Android开发工程师,这次是从深圳飞过来跟大家做一次分享。我想分享的题目是移动端SDK优化的特点和个人的经验。

大概分了三部分,第一部分是SDK和APP的差别,我个人的理解是SDK是面向开发者的,而APP是面向用户的。这当中开发会存在一些差异化的东西,可以跟大家说一说。第二部分是极光在去年做了一次大的架构优化,这里有些经验也想拿出来讲一讲。第三部分是极光SDK的性能优化,举一些具体的应用,多进程、I/O、网络优化点。

我先讲一下为什么我想着重地提一下SDK跟APP的区别,这跟我个人的经历有关,之前我在做APP,开发了一款图片社区的APP,后面做面向企业的移动办公应用,到极光以后才开始做SDK,所以我个人在APP和SDK的区别上有比较深的体会。我想做一个小调查,在座的各位有做SDK开发的或者是曾经做过SDK开发的?在座就一位。

我们做程序开发的大家都有一据话叫做“不要重复做轮子”。什么意思呢?做APP开发希望快速地迭代,每个APP有自己独有的功能,业务代码只做独有的那一块儿,基础的业务直接用别人已经做好的文件框架或者是第三方导入进来。比如说想做网络的功能有Http和Volley的优秀稳定的框架。想做数据库这一块儿有Omlite Realm已经封装好的非常方便使用的数据库的包。插一个硬广,如果我们用到推送、体积可以用极光的SDK。

APP的核心代码只需要做上面一小块儿,但是这样做的结果是一个APP本身大小有几兆几十兆,甚至刚需的功能丰富强大的一百多兆的,这种用户还可以接受。但是做SDK不可能做这么大的,因为开发者不愿意用,本身SDK很大的话他会觉得臃肿或者有问题,所以SDK做的非常小,可能体积几百K,三百K以内,这是消费者能够接受的。

做基础业务像网络、数据库用安卓原生的EBL来做,首先能把体积控制在比较小的范围内,这样APP的开发者才可以用我们比较小的包来做他们自己的业务,这样的出发点是为了裁剪体积,同时也有一些优势、好处。保持代码的精简。因为这些代码是我们自己开发的内部可见,我们在做小的修改、特性的优化或者是BUG调试是可以办到,但如果你用的是第三方的库,可能本身就混淆不好改,调试也不方便。这是重复造轮子的意义,我们为了体积以及为了更好地优化架构、性能,所以我们要重复造轮子。

配置的区别,如果是APP开发者自己只需要写一份配置列表,应用的权限,用到哪些组件、边际成本是什么样的一张表就出来了。SDK的权限是交给开发者的,开发者具体买的什么权限或版本我们是控制不了的,只能通过指引说明这种形式,告诉他们怎么配置,所以这方面也会带来一些问题,或者我们要去处理的。我们可以考虑配置要做到尽量简便,这样开发者才好用,觉得你的学习成本低,可以直接拿过来用。

再就是要友好,如果他失误了,哪个地方没配好,我们要有好的反馈告诉他,你是漏了哪个权限或者是哪个版本配置有问题,这是要提示给他的。再有就是要灵活,每个开发者的需求不一样,可能用这个SDK只需要这一块儿的功能,可能他更需要拓展性的功能,所以要可选的配置,灵活可以变化。前面讲的要求也可以有一些更好的方式,这里也是一条硬广,我们极光SDK除了设动配置的方式也存在了Jcenter的配置方式,如果你是Android开发者,你在里面只需要配几条代码,其他的配权限都可以省过了,这是学习成本问题。

我们每个版本迭代出来会把设立的代码自动地打包出来,开发者可以直接看到。直接拿来知道它是怎么正常工作的。一个APP如果要升级的话,开机新版本替换上架去应用商场把它换下来,旧版本一般情况下是看不到的,用户新拿到一个版本新安装的话是拿新版本,或者是旧版本更新也是可以更新到新版本。或者是你可以推送更新的通知给他,这样可以保证用户可以比较快地用到新版本。同时在线的版本范围比较少,最近几个版本同时在线,一些老的成本的占有率会慢慢地变的很低。

但是SDK的情况不一样,SDK不是上架市场,是开发者主动拿。所以会出现有些开发者几个月之后才来做更新,也就意味着几个月之后用户会用到新板的SDK,甚至有的从来不会更新,所以这种情况会导致同时在线有非常历史久远的以前老版本,这个问题会要求我们做很多兼容性的考虑,因为老版本同时也有大批量的用户存在。价格优化和性能优化上取三个因素来说,可靠性跟性能考虑的范围要比新功能重。

下面进入第二部分,我讲一下极光SDK在架构优化上的大的变动。2016年之前推的主要是两个SDK,一个是Jpush,一个是JMessage,在2016年推了多条的统计,比较专业的统计以及分享。

旧的架构推送跟IM是两个独立的SDK,存在很多种冗余代码。导致的毛病是占用空间大,因为同时装了两个一样的代码在里面,有重复的操作,两个SDK都需要做注册登陆,要做重复的任务,因为这些重复的占用的通道、资源都是重复的,要有格外的占用。再有就是在哪一块儿做了升级,统计你在隔壁的SDK也要做同样的升级,这是两个SDK的情况。

我们拓展了业务以后不光有这两个,还有统计、还有分享,未来可能有更多,这样的多条业务线如果还采用旧的架构的话会变的非常麻烦,所以针对多条业务线的考虑做了架构调整,我们把业务跟核心做了分成,核心的功能是各个组件通有的功能,把它集成在JCore里面做一个核心包。各个SDK做推送、统计都是运行在JCore里面的自有业务,好处是结构更加清晰利于拓展,资源式功能因为重复的动作在统一的包里面已经用完了。

实际实施会是这样的样子,这是一个极光的大组件,把核心的功能网络、线程等基础的业务在这里。基础的业务上报等也是在集成在JCore里。这是架构的演示。

实际实施会遇到各种经验上的事情,这是Jcore的核心组件,我们在服务器更新的时候,从协议的制定上要考虑兼容不同的组件以及前面讲的同时在线的不同版本都要兼容。在代码设计上涉及到工具类还好说,策略类的东西要用策略模式,方便替换更多的策略。

再就是核心组件跟上层的组件,分成两个组件,可能升级是不同步的,所以我们接口也要考虑的比较多些,要简洁要适应变化,不像以前的同一个包里面是开发者自己想改就改,但现在涉及两个组件。

再就是各个组件之间的通信,我们想通过命令模式,把动作抽象成一些可发送的对象,黄色的小旗是推送的,蓝色的小旗是统计的,它可以实现分发和缓冲。缓冲是用一些策略,比如说有些状态下把某些请求拿掉了,或者是设优先级,或者是定义重复的策略,首先代码设计上要做成命令模式。

安卓开发的工程设计是出台的包都要进行混淆设计,因为你拆分成两个包了,有些接口难免要暴露出来,因为你不暴露混淆以后的接口不一样,我们又不能完全暴露,还是要起到保护代码的作用,所以要平衡。因为引入了很多组件,所以在打包编译脚本的时候要用一些工具,现在也挺方便的,自己写脚本自己做集成,做了脚本各种各种编译的包都可以写出来。

第三部分讲性能优化。主要想讲多进程和多线程,多线程讲的不多。主要有一个主线程在移动端主线程要尽量克制的,也要考虑到用户的感受。所以在占用资源比较多耗时,像计算要另外开一个线程,I/O读取也是要另外开线程,这里面涉及语言的基本功跟关键字能不能用。
在安卓的应用设计理念就是内存是非常宝贵的资源,所以它从设计里面上就尽量不让开发者去管理这些内容,而是交给系统去处理。它自己运行一套回收的开发,各个APP占用多少资源运行算法会主动,如果你占用资源大系统就回收掉你了。我们开发者不能直接写代码控制内存的话,就要自己去想怎么去避免应用被回收。

如果我们是单进程的应用,可能你做的任务很多,内存占用数多,我们拍多个进程可以分担上面的一部分任务、资源,分担到另一个进程,这样避免占用资源太高了回收。再一个好处就是,我在后台跑了一些任务,因为在多进程里面,主进程因为一些无法预测的原因可能占用的资源太高被系统回收掉了,我在主进程的东西依然可以跑,依然可以继续执行下去。

这是好处,它是性能优化的好处。但问题也存在,它多进程以后内存空间是独立的,同一个代码看上去是一样的,但可能在主进程是初始化的,可能写代码的时候没有意识到。在运行的时候完全出乎你的意料了,所以这是多进程要考虑的点。你的数据不同步会有问题,你以为主进程已经做了这回事儿出了值,甚至你当时都不知道是在哪里运行的。

数据不同步的问题是不管主进程还是线进程都能遇到的问题。单粒模式大家都会用,但怎么把它的性能用的好又避免不出错,我个人经常用双重检查锁,可能复杂但有利于性能更好地搞起来,并且不容易出数据库的问题。

第二点是我前面讲的不同进程内存区不一样,值也会不一样。在进程间怎么保证性能有多进程数据又能同步呢?画了一个示意图,你在组进程做了数据一直在组进程。但你在主进程想取一个值,不想在中间拿,而是想跳过主进程,要考虑性能监控的问题。

性能优化的IO的储存方式有这么几种,座位开发者如果对性能敏感的话,内存级别是度曲非常大的,磁盘可能到了毫秒级。前面讲了多进程之间通信也是耗时的动作,也是毫秒级的。再到网络,因为我们知道移动端通过网络通信就很依赖当时的网络环境,可能是几秒,甚至有时候会失联。所以有个响应级别,尽量往上面的方式去用,能够达到性能优化的意图。

以安卓的SharedPreferences为例,我们通常的方式是封装成一个工具类,每一个字段对应一个值,都写GET方法和SET方法,但可能有不止一个,可以换一个字段,如果还是内存级别的那几万秒已经响应完了,这个是几乎感受不到差别的。假如说GET不是直接从内存拿的,而是要从文件里面拿,或者是本地的IO磁盘上存取的话会存在耗时前面讲的多进程通讯,如果去隔壁进程写值的话就是几毫秒的动作,这样的话排下来就比较耗时了。

如果是存在在循环里面是更耗时的,这里面找出了一些优化点,我们可以直接先把它保存在内部变量里面再来进行循环,在每次要用的时候就用内部变量来测,立马就变成内存级别的几纳秒的响应。数据库有批量的提交机制,可以把X、Y合并起来,先做编辑再一次性提交,这也是节省性能的方式。这是列了一些优化点。

同一个线程下可以做一次度曲多次使用,写操作可以批量提交。内存级别的响应更快,快进程也是一样,可以不要为了单个的字段去跨进程读取一次,可以把先要取的几类想好,把它组成数据包去跨进程调用,这也是比较快的。拆分存储区是什么意思呢?本身自己内部有枷锁的机制,同一个文件下去读取,只要是同一个文件下写会填枷锁,提出来了拆分的存储区的方式是因为有些字段分析它的使用场景,有些字段可能频繁地在读,只有很少的机会写,大部分的情况都是这样。但是有一些要拿ID或者是更新的时间,这并不是多次读而一次写,是拿一次写一次的。为了避免它跟其他的问题,可以把它变成独立的文件,这也是性能优化的方式。

最后想介绍网络,因为我们做极光推送,推送的东西主要的任务就是在长链接里,客户端跟服务器进行通信,先要进入接入,因为我们要找对应的客户端要做接入服务,怎么做优化呢?本身有一个SIS服务,另外开辟服务器去找你当时的设备,它介入哪一个IP,接除哪一个端口是更加合适,有更加快的响应,我们可以下发一个列表给客户端,让它们自己去选自己去尝试,找到哪个端口可以更快地响应。

首先是你做性能优化可以在本地做缓存,你的列表可以缓存到本地,先做尝试,就相当于跳过SIS这一块儿,先尝试本地的缓存,如果失败了再去拿SIS下发的地址。
可以写一些选择策略,策略的理念核心是优先选可能可用的,优先排除不可用的。比方说我记录了一下这个IP的端口在30秒之前刚刚成功过,我可能优先选择这个,因为它非常成功,这是优先选择可用的。优先排除不可用就是本地有一个列表,SIS有一个列表,或者是代码里面有可以写“死”的列表,这三个列表在不同情况下是有重复的。我在前面已经试过了它不可用,就要把它记录下来,等到下一个列表再遇到它就可以直接把它排除掉,因为几秒钟前刚把它排除掉,这就是排除策略。

再有就是我们把当时的状态,成功或者失败的原因反馈上报给服务器,这样服务器根据上报的数据做调整,服务端到底接入哪边是更合适的选择。
我分享的东西大概就这些。

主持人:有没有小伙伴有问题现在可以问一下?

提问:SIS到底是什么东西?
王可为:因为推送是客户端跟服务器要建立通信,客户端到服务器的请求大家都会,但如果服务器想下发东西给客户端的话就要依赖于SDK做推送业务的。因为我们建立的是长链接,它不光是设备,同一个设备可能装了十几台APP,都适用这个业务,这样算下来可能70亿台设备都用同一个IP,那服务器肯定是受不了。所以我们是用分布式,用不同的服务器和端口,在后台相当于分析哪一个台的压力大,即使地调整它的地址,告诉客户端你到底选哪一个端口更大。
你从服务端拿到SIS列表,可能你今天之内都可以优先用,本身SIS也是网络通信,也是耗程操作。

提问:我有一个问题,我们之前也是有用到长链接,提供推送最核心的地方是保额?
王可为:一个方案是做“心跳”,更优的方案是做“智能心跳”。尤其像安卓后面出的版本,6.0以后对后台的服务限制更加深了,所以我们要综合考虑平台的因素去做“智能心跳”。策略也跟性能优化的策略挺像的,近期刚好通过正常的通信,心跳延后一点,或者是刚刚失败过就频繁一点,刚刚成功过就避免频繁,因为很有可能下一次成功。失败的话就要更快一点去建立连接,可能你现在是离线状态,这是我自己关于智能心跳的看法。
提问:因为我们公司已经集成了极光推送,在海外延迟上会有比较大的问题。
王可为:海外推送我这里想的就是海外的环境跟国内环境不一样,一方面可以通过自己的服务器来解决,如果你是国外的服务器可以用460服务。再一个是推送通道可以去有多种选择。比如说国外方面选择谷歌的服务更稳定一些,可以集成JCM或者FCM这样的通道,在国外的条件是更稳定。
提问:谢谢!

提问:想请问一下百万用户到达率有多少?推送时长会有多少?
王可为:你有点问住我了,因为我是做SDK前端的,到达率的统计主要都是后台做统计,这个数据我是不太确定,尤其是你问到百万级的,我可能更加不太知道级别具体的数据。
提问:咱们怎么样做用户区分?
王可为:因为我是属于开发者服务这一块儿的,我们极光有大数据的业务组。他们组根据SDK上传的数据做用户画像,他们有更丰富的数据业务甚至广告的业务,他们是在做更精细化的用户画像,做数据分析,我们是有这一块儿业务,如果要问我细节的话我是不太知道怎么跟你说明。你可以跟我们数据业务的同事去了解业务。