前言
互联网时代, App作为于用户交互的端, 可以说实际上是一个界面, 产品的业务, 服务都是由Server提供的. 而App与Server的交互依赖于网络, 故而网络优化, 也是我们的App优化中不可缺少的一个优化项.
典型的HTTP请求流程说明:
1、网络连接对用户的影响
App的网络连接对于用户来说, 影响很多, 且多数情况下都很直观, 直接影响用户对这个App的使用体验. 其中较为重要的几点:
流量App的流量消耗对用户来说是比较敏感的, 毕竟流量是花钱的嘛. 现在大部分人的手机上都有安装流量监控的工具App, 用来监控App的流量使用. 如果我们的App这方面没有控制好, 会给用户不好的使用体验.
电量电量相对于用户来说, 没有那么明显. 一般用户可能不会太注意. 但是如前文电量优化中说的那样, 网络连接(radio)是对电量影响很大的一个因素. 所以我们也要加以注意.
用户等待也就是用户体验, 良好的用户体验, 才是我们留住用户的第一步. 如果App请求等待时间长, 会给用户网络卡, 应用反应慢的感觉, 如果有对比, 有替代品, 我们的App很可能就会被用户无情抛弃.
2、分析网络连接的工具
2.1 Network Monitor
Android Studio内置的Monitor工具中就有一个Network Monitor:
其中:
Rx --- R(ecive) 表示下行流量, 即下载接收.
Tx --- T(ransmit) 表示上行流量, 即上传发送.
怎么使用Network Monitor?
Network monitor实时跟踪选定应用的数据请求情况. 我们可以连上手机, 选定调试应用进程, 然后在App上操作我们需要分析的页面请求.
例如, 上图就是以CoderPub为例, 针对从repo列表界面进入repo详情界面的监控数据.
可以看到从10s到30s之间, 20s时间内发生了多次数据请求, 且22s到27s之间的请求数据量还很大.
分析代码可以看到, 在请求repo详情的时候是打包了很多请求的:
@Override
public Observable<RepoDetail> getRepoDetail(String owner, String name) {
return Observable.zip(mRepoService.get(owner, name),
mRepoService.contributors(owner, name),
mRepoService.listForks(owner, name, "newest"),
mRepoService.readme(owner, name),
isStarred(owner, name),
new Func5<Repo, ArrayList<User>, ArrayList<Repo>, Content, Boolean, RepoDetail>() {
@Override
public RepoDetail call(Repo repo, ArrayList<User> users, ArrayList<Repo> forks, Content readme, Boolean isStarred) {
RepoDetail detail = new RepoDetail();
repo.setStarred(isStarred);
detail.setBaseRepo(repo);
detail.setForks(forks);
// because the readme content is encode with Base64 by github.
readme.content = StringUtil.base64Decode(readme.content);
detail.setReadme(readme);
detail.setContributors(users);
return detail;
}
});
}
这也验证了14s到20s间的四次数据请求, 另外由于repo详情界面会显示作者以及贡献者的图片, 而图片的数据量相对大, 故而23s到27s间有多次数据量很大的请求发生.
这个实际是有很多优化空间的, 我们稍后再说.
2.2 Wireshark, Fiddler, Charlesr等抓包工具
使用Charles、Fiddler等抓包工具同样可以实现Network Monitor的功能,而且更加强大。
2.3Stetho
Stetho是Facebook出品的一个Android应用的调试工具。无需Root即可通过Chrome,在Chrome Developer Tools中可视化查看应用布局,网络请求,sqlite,preference等。同样集成了Stetho之后也可以很方便的查看网络请求的各种情况。
3、网络优化
重点来了,网络优化主要从三个方面进行:1. 速度;2. 成功率;3. 流量。
3.1 接口设计
API设计
App与Server之间的API设计要考虑网络请求的频次, 资源的状态等. 以便App可以以较少的请求来完成业务需求和界面的展示.
例如, 注册登录. 正常会有两个API, 注册和登录, 但是设计API时我们应该给注册接口包含一个隐式的登录. 来避免App在注册后还得请求一次登录接口(有可能失败, 从而导致业务流程失败).
再例如, 上文提到的获取repo详情, 实际上请求了4个接口, 请求了repo的信息, forks列表, contributors列表, readme, 这是因为github提供的接口是尽量单一职责的. 然而在我们的实际开发中, 我们的Server除了提供这些单一职责的小接口外, 最好还能组合一个满足客户端业务需求的repo详情接口出来.
Gzip压缩
使用Gzip来压缩request和response, 减少传输数据量, 从而减少流量消耗.
考虑使用Protocol Buffer代替JSON
从前我们传输数据使用XML, 后来使用JSON代替了XML, 很大程度上也是为了可读性和减少数据量(当然还有映射成POJO的方便程度).
下图是对比Json数据使用 Gzip 压缩前后对比图
返回内容开启 Gzip压缩前
开启Gzip压缩后
对比发现,开启Gzip后可以减少57.3%的数据传输量
Protocol Buffer是Google推出的一种数据交换格式.
如果我们的接口每次传输的数据量很大的话, 可以考虑下protobuf, 会比JSON数据量小很多.
当然相比来说, JSON也有其优势, 可读性更高.
本文以网络流量优化的角度推荐protobuf作为一个选择, 具体还需更具实际情况考虑.
图片的Size
上面Network Monitor中看到的22s到27s之间的有多次请求, 且数据量还很大. 就是在获取图片资源.
图片相对于接口请求来说, 数据量要大得多. 故而也是我们需要优化的一个点.
我们可以在获取图片时告知服务器需要的图片的宽高, 以便服务器给出合适的图片, 避免浪费.
我们现在很多公司的图片资源都是使用第三方的云存储服务的(七牛, 阿里云存储之类的).
以七牛为例, 可以在请求图片的url中添加诸如质量, 格式, width, height等path来获取合适的图片资源:
imageView2/<mode>/w/<LongEdge>
/h/<ShortEdge>
/format/<Format>
/interlace/<Interlace>
/q/<Quality>
/ignore-error/<ignoreError>
参考七牛官方文档.
3.2 图片处理
3.2.1 图片下载
使用缩略图
App中需要加载的图片按需加载,列表中的图片根据需要的尺寸加载合适的缩略图即可,只有用户查看大图的时候才去加载原图。不仅节省流量,同时也能节省内存!之前使用某公司的图片存储服务在原图链接之后拼接宽高参数,根据参数的不同返回相应的图片。
有许多方式来使得图片更加容易下载,比如使用 WebP图片,动态地调整大小的图片,以及使用图片加载框架。
使用WebP图片
通过网络提供WebP文件来减少图片加载的时间和节省网络带宽,WebP文件通常会比它的PNG或者JPG文件小,但会拥有同样的图片质量。甚至是使用有损的设置,WebP也可以输出一个和原图几乎完全一样的图片。安卓系统从Android4.0(API 14)添加了有损耗的WebP support并且在Android4.2(API 17)对无损的,清晰的WebP提供了支持。
使用WebP格式;同样的照片,采用WebP格式可大幅节省流量,相对于JPG格式的图片,流量能节省将近 25% 到 35 %;相对于 PNG 格式的图片,流量可以节省将近80%。最重要的是使用WebP之后图片质量也没有改变。
动态修改图片
应用要求按指定的渲染大小来从网络上请求图片,这个渲染大小和设备规格有关,并且服务器提供的是合适大小的图片。这样能够最小化网络上的数据传输,减少持有图片对内存的大量损耗,直接影响到性能的提高和用户满意度。
当用户不得不等待图片下载的时候用户体验就会有所下降,使用合适的图片尺寸有助于解决这些问题,可以考虑让图片的请求都基于网络的类型或者网络连接的质量,这个尺寸可能会比目标值小。
使用图片加载框架
你的APP不应该重复获取图片,图片加载框架比如Glide或者Picasso获取图片,然后缓存,然后hook到你的View来显示占位符图片,直到真正的图片准备好了显示真正的图片。因为图片被缓存下来了,当图片下次被请求的时候,这些图片加载框架会直接从本地缓存中获取。
图片加载框架会管理他们的缓存大小,保留最近使用过的图片,这样你的APP的大小不会无限制地增长。
3.2.2 图片上传
图片(文件)的上传失败率比较高,不仅仅因为大文件,同时带宽、时延、稳定性等因素在此场景下的影响也更加明显;
避免整文件传输,采用分片传输;
根据网络类型以及传输过程中的变化动态的修改分片大小;
每个分片失败重传的机会。
备注:图片上传是一项看似简单、共性很多但实际上复杂、需要细分的工作。移动互联网的场景和有线的场景是有很多区别的,例如移动网络的质量/带宽经常会发生“跳变”,但有线网络却是“渐变”。
3.3 网络缓存
适当的缓存, 既可以让我们的应用看起来更快, 也能避免一些不必要的流量消耗.
关于Android App的网络缓存, 请参考MVP架构实现的Github客户端(4-加入网络缓存)一文.
3.4 打包网络请求
当接口设计不能满足我们的业务需求时. 例如可能一个界面需要请求多个接口, 或是网络良好, 处于Wifi状态下时我们想获取更多的数据等.
这时就可以打包一些网络请求, 例如请求列表的同时, 获取Header点击率较高的的item项的详情数据.
可以通过一些统计数据来帮助我们定位用户接下来的操作是高概率的, 提前获取这部分的数据.
3.5 监听相关状态
通过监听设备的状态:
休眠状态
充电状态
网络状态
在状态弱网下优化:
压缩/减少数据传输量
利用缓存减少网络传输
针对弱网(移动网络), 不自动加载图片
界面先反馈, 请求延迟提交例如, 用户点赞操作, 可以直接给出界面的点赞成功的反馈, 使用JobScheduler在网络情况较好的时候打包请求.
结合JobScheduler来根据实际情况做网络请求. 比方说Splash闪屏广告图片, 我们可以在连接到Wifi时下载缓存到本地; 新闻类的App可以在充电, Wifi状态下做离线缓存.
3.6 调整数据传输
有几种方式让你的APP适应网络条件,提供一个较好的用户体验的,比如,对请求划分优先等级来最小化用户等待信息的时间。也可以检测并适应较慢网络速度和发生网络连接的时候的发生的改变。
优先考虑带宽
不应该假设设备连接的网络是一个长时间持续并且稳定可靠的网络,app应该对网络请求划分优先级尽可能快地展示最有用的信息给用户。
立刻呈现给用户一些实质的信息是一个比较好的用户体验,相对于让用户等待那些不那么必要的信息来说。这可以减少用户不得不等待的时间,增加APP在慢速网络时的实用性。
为了达到这个目的,对网络请求进行排序比如文本的获取应该在富媒体之前,文本请求一般都比较小,压缩更好,并且传输速度快,这意味着你的APP可以快速地先显示内容。
在慢速网络的时候使用更少的带宽
你的APP传输数据的能力是否及时取决于网络连接,检测这些网络的质量并且调整你的APP使用网络的行为可以提供一个更好的用户体验。
使用下列的方法来检测外部不容易观察的网络质量,使用从这些方法返回的数据,你的APP应该调整对网络的使用来为用户的操作提供一个及时的响应
ConnectivityManager> isActiveNetworkMetered()
ConnectivityManager> getActiveNetworkInfo()
ConnectivityManager> getNetworkCapabilities(Network)
TelephonyManager> getNetworkType()
在一个慢速的网络连接中,考虑只下载低分辨率的媒体或者直接不下载。确保你的用户可以在慢速网络中继续使用你的APP,对于没有图片或者图片仍然在加载的情况,应该先显示一个占位符,使用 Palette library创建一个动态的占位符,生成一个符合目标图片颜色的占位符
在Android 7.0或更高版本的设备上,用户可以打开Data Saver设置,可以帮助最小化数据的使用,Android 7.0扩展ConnectivityManager来检测Data Saver设置。
检测网络改变,然后修改APP的行为
网络质量不是固定不变的,它会随着地理位置,网络流量和当地人口密度发生改变。APP 应该检测网络中的改变并且相应地调整带宽,让APP可以更好地适应网络质量,可以实现下面的这些方法检测网络状态:
ConnectivityManager> getActiveNetworkInfo()
ConnectivityManager> getNetworkCapabilities(Network)
TelephonyManager> getDataState()
随着网络质量的下降,减少请求的数量,随着网络质量的提升,你可以提高你的请求量到最优级别。
在更高的网络质量下,不计费使用流量的网络,可以考虑预取数据让数据提前可用。从用户体验的立场,这可能意味着一个新闻阅读应用在2G网络下一次只能获取3篇文章,而在WIFI状态下一次可以获取20篇文章。
当网络连接状态发生改变时会发出CONNECTIVITY_CHANGE广播,APP在前台运行的情况下,可以通过注册广播接收器来接受这个广播,在接收广播以后,你应该再评估当前的网络状态并且调整你的UI,处理网络请求,不能够在manifest文件中声明这个广播Action,因为在Android7.0版本之后这个Action就被删除了。
3.6 IP直连与HttpDns;
DNS解析的失败率占联网失败中很大一种,而且首次域名解析一般需要几百毫秒。针对此,我们可以不用域名,才用IP直连省去 DNS 解析过程,节省这部分时间。
另外熟悉阿里云的小伙伴肯定知道HttpDns:HttpDNS基于Http协议的域名解析,替代了基于DNS协议向运营商Local DNS发起解析请求的传统方式,可以避免Local DNS造成的域名劫持和跨网访问问题,解决域名解析异常带来的困扰。
3.7请求打包
合并网络请求,减少请求次数。对于一些接口类如统计,无需实时上报,将统计信息保存在本地,然后根据策略统一上传。这样头信息仅需上传一次,减少了流量也节省了资源。
3.8请求频率优化
可以通过提供一个最佳的网络体验来增强用户体验,比如,你可以让你的APP在离线状态下依然可以使用,使用GcmNetworkManager和ContentProvider,删除重复的网络请求。
让你的APP在离线状态下依然可用
在一些比较偏僻的地方,通常网络信号都不会很好,APP失去网络连接是很常见的事情。创建一个能够在离线状态依然正常使用的APP意味着用户可以在任何时候和你的APP进行互动。可以通过把网络数据保存在本地来实现这个需求,缓存数据,并且把发出的请求添加到队列中,当网络恢复的时候再及时发出。
在可能的情况下,app不应该通知用户网络已经失去连接了,只有当用户进行操作,并且这个操作一定需要网络连接的支持,才通知用户当前处于没有网络的状态。
当一个设备处于没有网络连接状态时,应用应该允许用户发出的网络请求,在网络连接恢复以后再执行这些网络请求。一个例子就是一个邮件客户端允许用户在设备处于离线的状态下依然能够创建,发送,查看,移动和删除已经存在的邮件,就是因为这些操作被保存下来并且在网络恢复以后再执行。这样的话,APP就能够在设备有网络和没有网络的时候都提供一个相似的用户体验。
使用GcmNetworkManager和contentProvider
确保你的APP使用数据库或者相似的结构来保存所有的数据到磁盘上,这样它就能够在不管网络条件如何的情况下表现出极佳的体验,比如使用(SQLite和ContentProvider)缓存数据, GCM Network Manager ( GcmNetworkManager)提供一个健全的机制和服务器同步数据当 content providers (ContentProvider)缓存这些数据,结合来提供一个允许在离线状态继续使用的结构。
App应该缓存从网络上获取的内容,在发起持续的请求之前,app应该先显示本地的缓存数据。这确保了app不管设备有没有网络连接或者是很慢或者是不可靠的网络,都能够为用户提供服务。
除去网络请求
一个离线优先的结构初始化时会尝试从本地获取数据,失败以后,从网络请求数据。从网络检索以后,数据会缓存到本地。这就确保对于同一块数据发起网络请求只会发生一次,对随后的请求都会使用本地数据来完成请求。为了达到这个,使用本地数据库来对数据持久化(通常使用android.database.sqlite 或者 SharedPreferences)
这个结构同样简化一个APP的流畅性,在离线和在线状态之间一方从网络获取数据保存到本地,另一方从缓存获取数据展示给用户。
对于短暂不持续的数据,也就是内容更新快的数据,使用一个有大小,或者时间限制的磁盘缓存,比如DiskLruCache,数据通常不会发生改变的就只从网络请求一次然后缓存下来以后使用,这样的数据一般是图片或者非临时的文档比如新闻文章或者推送消息。
4.结语
网络优化, 是App优化中相当重要的一项优化. 除了客户端, 接口的优化外, 很多一部分优化还依赖于服务器端, 包括服务器端的代码开发, 部署方式等. 跟你的服务器开发/运维工程师一起聊聊这个话题吧:)