目录
写在前面
一、网络优化维度
二、网络优化工具选择
2.1、Network Profiler
2.2、抓包工具
2.3、Stetho
三、精准获取流量消耗
3.1、如何判断APP流量消耗偏高
3.2、线上流量获取方案
3.3、前后台流量获取方案
四、网络请求流量优化
4.1、需要使用网络的场景
4.2、网络请求优化手段
五、网络请求质量优化
5.1、HTTPDNS
5.2、协议版本升级
5.3、网络请求质量监控
5.4、网络容灾机制
5.5、其它优化
六、网络体系化方案建设
独在异乡为异客,每逢佳节胖三斤!
不知不觉间端午小长假已经结束了,各位小伙伴们假期过的还好吗?又到了新的工作日了,都收收心吧,开始好好工作啦!努力工作,认真生活,不辜负你的每一天!上一篇中说到了Android的线程优化——《Android线程优化你了解多少》,今天继续Android性能优化系列专题的学习,来说说Android中的网络优化。
①、流量消耗
②、网络请求质量
③、其它维度
④、网络优化误区
它是Android Studio自带的工具:
首先我们打开Android Studio,然后在菜单栏选择Run--->Edit Configurations--->Android App--->app--->profiling,然后如下图勾选第一个Enable advanced profiling选项,点击APPLY--->OK即可:
然后进入Network Profiler的视图,此时我发送一条网络请求,然后时间线上就会显示出来这条请求,现在我选中这条请求的一个时间范围,然后下方就会显示出这条请求的名称、大小、类型、状态、时间等,右侧会有这条请求的详细信息,你可以预览数据,我这里的是一个接口请求,如果有图片请求右侧预览界面也可以预览图片:
下面是我使用Charles抓包工具抓包的一张截图,手机和电脑的网络保持一致,并且手动修改代理地址为电脑ip,然后就可以抓包了,具体的工具的使用方法就不过多介绍了,网上一搜一大把,主要功能大家可以去搜一点breakpoint断点功能,通过断点可以手动修改请求,map local可以本地模拟数据加快开发进度以及补充测试时模拟服务端各种脏数据情况,Throttle可以进行弱网环境下的场景模拟,大家可以具体的去了解一下,由于篇幅原因就不再多说了,毕竟工具的使用需要自己实际体验:
使用方式:
实际使用的话你会发现它和Network Profiler有点像,一般也很少会使用它去做抓包工具,所以关于它的实际用法只是简单的提一下,具体的有感兴趣的可以实际操作一下,
首先添加依赖并初始化,都是上面的步骤:
Stetho.initializeWithDefaults(this);
然后添加拦截器:
最后在Chrome浏览器(注意必须是Chrome浏览器)中输入chrome://inspect之后按照下图中框选出的位置点击即可开始抓包,这里可能会需要,因为我实际测试的时候发现不是报404的:
这个问题决定了我们什么时候开始对流量进行优化,这里就简单说一下答案:
流量测试方案:
当你的APP到了稳定期之后,日活可能会有成百上千万甚至更多,此时APP的功能应该是非常复杂的,并且还具备了其他的一些功能比如说监控,这些功能的网络请求并不是实时上报的,因此做流量消耗的周期应该会很长,不是简单的十几二十分钟就能搞定的。另外,我们还应该排除别的APP的干扰,比如你想要抓取你的APP的所有网络请求,此时应该打开设置界面进入流量管理,设置只允许你的这个APP可以联网,别的所有APP的联网功能全都关闭,这样使用抓包工具抓到的请求都是这个APP的请求了。这种抓包方案在线下测试一般都是没有问题的,但是某些场景可能会在线上出现,在线下就很难发现,所以这些场景就只能通过线上监控才能发现。
①、TrafficStats
这种方案其实不推荐使用,所以就不多说了,有兴趣的朋友可以自己尝试一下,直接调用它的静态方法就可以获取了:
//手机通过蜂窝流量接收到的流量信息
TrafficStats.getMobileRxBytes();
存在的问题:
②、NetworkStatsManager
接下来我们通过实战来统计这个Demo本月消耗的蜂窝流量情况:
public void getNetStats() {
//保护性操作
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
}
long netDataReceive = 0; //接收的流量
long netDataSend = 0; //发送的流量
TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
@SuppressLint("MissingPermission")
String subId = telephonyManager.getSubscriberId(); //此处需要权限,获取的是SIM卡唯一标识
//系统服务通过getSystemService方法拿到
NetworkStatsManager manager = (NetworkStatsManager) getSystemService(Context.NETWORK_STATS_SERVICE);
NetworkStats networkStats = null;
NetworkStats.Bucket bucket = new NetworkStats.Bucket(); //离散时间桶
try {
//参数分别是:网络类型、SIM卡唯一标识、开始时间、结束时间
networkStats = manager.querySummary(NetworkCapabilities.TRANSPORT_CELLULAR, subId, getStartTime(), System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
//遍历时间桶
while (networkStats != null && networkStats.hasNextBucket()) {
networkStats.getNextBucket(bucket);
int uid = bucket.getUid();
if (getAppUid() == uid) {
netDataReceive += bucket.getRxBytes();
netDataSend += bucket.getTxBytes();
}
}
LogUtils.i("anqinetuse------>" + (netDataSend + netDataReceive));
}
private static volatile int sUId = -1;
//通过包名获取uid
private int getAppUid() {
if (sUId == -1) {
PackageManager packageManager = getPackageManager();
try {
PackageInfo packageInfo = packageManager.getPackageInfo(getPackageName(), PackageManager.GET_META_DATA);
if (packageInfo != null) {
sUId = packageInfo.applicationInfo.uid;
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
return sUId;
}
后续可以将这个获取流量的方法进一步封装,比如将它封装到基础库中,然后通过入参网络类型、开始时间、结束时间等在应用层调用,更加方便灵活的获取你想要的数据。我们将获取到的结果进行打印输出:
可以看到这个值是292886,将这个除以1024得到的是286KB,然后到手机设置里面找到数据流量排行,选择本月,找到这个Demo看一下系统统计出来的流量消耗是多少:
经过对比发现两个值还是相当接近的,说明我们通过代码统计到的数据应该是很精确的一个值了,所以今后我们就可以统计出线上用户流量消耗的准确值了,如果今后有用户反馈说某一天流量消耗的比较多,我们就可以通过APM后台下发一条指令来回捞一下用户当天具体的流量消耗值,结合用户使用时长,就可以分析出用户的流量消耗是否存在异常。
场景:线上反馈App后台跑流量
很多用户对后台流量非常关心,大多数用户都是非常害怕你的应用在后台一直跑流量的,如果你只是使用上面讲到的这种方案,实际上我们并不知道消耗的这些流量前后台所占用的比例,所以只获取一个时间段的值不够全面,还需要知道这些流量到底是在前台消耗的还是在后台消耗的。
解决方案:
App启动时执行一个后台任务,这个后台任务每隔一段时间获取一下这段时间内的流量消耗,自己维护一份数据的统计,分别记录用户在前后台的流量消耗总量,结合别的监控在合适的时间上报到APM后台作为流量治理的依据,这样当用户反馈时,我们可以直接查看用户流量消耗统计,然后判断是否存在问题,这种方案可以结合上面的代码来实现,下面说一下实现思路:
首先将上面的代码封装一下,将开始时间和结束时间作为参数从调用方传递:
public long getNetStats(long startTime,long endTime) {
......//省略代码
return (netDataSend + netDataReceive);
}
其次可以监听app处于前台还是后台,可以使用标志位进行判定,具体的监听方法可以直接使用下面这种方式:
getApplication().registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
......//省略部分代码
@Override
public void onActivityResumed(@NonNull Activity activity) {
//处于前台
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
//进入后台
}
......//省略部分代码
});
然后再开启一个定时任务,每隔一段时间获取一次流量消耗情况:
Executors.newScheduledThreadPool(1).schedule(new Runnable() {
@Override
public void run() {
long useNum = getNetStats(System.currentTimeMillis()-30*1000,System.currentTimeMillis());
//前台还是后台
}
},30, TimeUnit.SECONDS);
总结:
①、数据缓存
首先来写一个针对无网络情况下的拦截器NoNetInterceptor,策略模式设置为没有网络时强制开启缓存:
public class NoNetInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder builder = request.newBuilder();
if(!Utils.isNetworkConnected(BaseApp.getApplication())){
//没有网络时强制开启缓存
builder.cacheControl(CacheControl.FORCE_CACHE);
}
return chain.proceed(builder.build());
}
}
然后在OkHttp中添加缓存:
就这两步就搞定了哦,然后来对比一下结果,没有缓存时关掉网络应用进去就是个空白页,有缓存的话即使关掉网络也是有数据能够展示的,给人一种好的用户体验:
②、增量数据更新
举个栗子:省市区等数据更新,这种类型的数据并不是经常变化的,如果每次都全量更新很明显造成了浪费,所以可以只更新有变化的那部分数据,这个场景需要和服务端实际配合,所以这里就不演示了,不过我相信大家也都明白是什么意思了。
③、数据压缩
下面来实际使用一下鲁班这个图片压缩库,看一下图片大小的前后对比情况,首先我在手机的外置存储卡上面准备了一张图片:/storage/emulated/0/Android/luban/yingbao.png,图片大小是2.55MB,然后在代码中进行压缩,压缩完了之后还存储在luban这个文件夹下:
Luban.with(MainActivity.this)
.load(Environment.getExternalStorageDirectory().getPath()+"/Android/luban/yingbao.png")
.setTargetDir(Environment.getExternalStorageDirectory().getPath()+"/Android/luban")
.launch();
OK,这样一行代码就压缩完了,我们来看看压缩之后的大小和压缩质量的对比,如下图所示:从2.55MB压缩到了40KB,极大的减小了图片的体积,并且通过右侧两张图片放大后的效果可以看到图片的质量几乎看不出来有什么差别:
所以图片压缩在使用过程中是相当重要的,一般情况下我们都要避免图片的原图上传。
④、优化发送频率和时机
⑤、图片相关
缩略图:原图是32KB,生成的缩略图是6KB,大小有效减小了将近5倍多,并且针对于手机列表仍然是可以进行展示的,所以使用缩略图可以有效降低流量的消耗
WebP:使用这种图片格式对于图片尺寸没有任何变化,但是图片大小也可以有效降低:
上一部分我们说了网络请求的流量优化,实际上对用户体验影响最大的是网络请求质量很差,这一点也是让人容易忽略的一个地方,我们开发测试阶段基本上都是在公司的wifi环境下进行的,网络环境一般都还是OK的。但是实际上线之后用户的网络环境我们是不可控的,假设有用户经常反馈界面打不开或者是打开较慢,图片加载不出来等情况,这些对用户的使用体验是有巨大影响的,很多情况下用户就会抛弃我们的APP,转而去寻求同类型下体验更好的APP,所以网络请求质量尤为关键。总的来说质量指标就是以下两点:
在介绍质量优化之前,先来说说Http请求的过程:
从上面介绍的请求的过程不难发现网络请求的成功率和速度一开始就受到DNS解析服务的影响,如果域名到IP地址这个过程被劫持、或者解析速度较慢都会严重影响用户体验,DNS被劫持就是用户得到的数据并不是真实想要提供给用户的数据,解析比较慢则会造成用户等待的时间比较长,所以DNS优化是网络请求质量优化的第一步。
实战OkHttp结合HTTPDNS的使用:
首先引入一个库:HTTPDNS是阿里云面向移动开发者提供的移动端DNS解析服务。通过该SDK,开发者可以在自己的Android APP中获得可靠、实时、精准的DNS解析服务,彻底解决传统DNS面临的域名劫持、解析时延长、调度不精准等问题,官方文档地址:https://help.aliyun.com/document_detail/150879.html
implementation ('com.aliyun.ams:alicloud-android-httpdns:1.1.7@aar') {
transitive true
}
接着来创建一个集合OkHttp使用的DNS解析服务的类,此类使用单例模式:
public class OkHttpDNS implements Dns {
//阿里云提供的HttpDns解析服务
private HttpDnsService dnsService;
//单例模式
private static OkHttpDNS instance = null;
private OkHttpDNS(Context context) {
dnsService = HttpDns.getService(context, ""); //用户id这里演示直接写的""
}
public static OkHttpDNS getIns(Context context) {
if (instance == null) {
synchronized (OkHttpDNS.class) {
if (instance == null) {
instance = new OkHttpDNS(context);
}
}
}
return instance;
}
@Override
public List lookup(String hostname) throws UnknownHostException {
//优先使用阿里云dns解析服务返回的ip地址,如果为空再走系统的DNS解析服务
String ip = dnsService.getIpByHostAsync(hostname);
if(ip != null){ //如果不为空直接使用这个ip进行网络请求
List inetAddresses = Arrays.asList(InetAddress.getAllByName(ip));
return inetAddresses;
}
return Dns.SYSTEM.lookup(hostname); //如果为空走系统的解析服务
}
}
最后在OkHttp中设置自己的DNS解析服务:
这样在网络请求的时候就可以绕过系统DNS解析这一步来提升网络请求的质量。
接着来说说HTTP协议版本的优化,上面在介绍HTTP请求的时候其中有一步是是创建连接,这里面会出现TCP的三次握手,这个时间是比较长的,如果每次网络请求都走三次握手,很明显效率是非常低的,所以HTTP的不同版本对这一点的优化也是非常多的,下面来看下HTTP协议不同版本之间的主要区别:
如果有条件的情况下,尽量选择高版本的HTTP协议。
①、OkHttp如何获取网络请求的质量数据
OkHttp给我们留了一个回调,叫做EventListener,我们可以自己实现一个EventListener,然后设置给每一次的网络请求:
首先创建一个model并且定义几个成员变量作为统计的对象:
public class OkHttpEvent {
public long dnsStartTime; //dns开始时间
public long dnsEndTime; //dns结束时间
public long responseBodySize; //网络请求返回值大小
public boolean apiSuccess; //网络请求是否成功
public String errorReason; //请求失败的具体原因
}
然后自定义一个OkHttpEventListener,并且实现它的回调方法,每次创建OkHttpEventListener的时候就创建出一个OkHttpEvent,在具体的回调方法中为其赋值:
public class OkHttpEventListener extends EventListener {
public static final Factory FACTORY = new Factory() {
@Override
public EventListener create(Call call) {
return new OkHttpEventListener();
}
};
OkHttpEvent okHttpEvent;
public OkHttpEventListener() {
super();
okHttpEvent = new OkHttpEvent();
}
@Override
public void callStart(Call call) {
super.callStart(call);
Log.i("Jarchie","callStart");
}
@Override
public void dnsStart(Call call, String domainName) {
super.dnsStart(call, domainName);
okHttpEvent.dnsStartTime = System.currentTimeMillis();
}
@Override
public void dnsEnd(Call call, String domainName, List inetAddressList) {
super.dnsEnd(call, domainName, inetAddressList);
okHttpEvent.dnsEndTime = System.currentTimeMillis();
}
@Override
public void responseBodyEnd(Call call, long byteCount) {
super.responseBodyEnd(call, byteCount);
//网络请求返回值的大小,此处统计网络请求的流量消耗,所以此处可做流量预警
okHttpEvent.responseBodySize = byteCount;
}
@Override
public void callEnd(Call call) {
super.callEnd(call);
okHttpEvent.apiSuccess = true;
}
@Override
public void callFailed(Call call, IOException ioe) {
Log.i("Jarchie","callFailed");
super.callFailed(call, ioe);
okHttpEvent.apiSuccess = false;
okHttpEvent.errorReason = Log.getStackTraceString(ioe);
Log.i("Jarchie","reason "+okHttpEvent.errorReason);
}
}
最后在OkHttp中进行设置:
通过上面的方法我们就可以拿到这次网络请求的每一步中包括dns解析的时间、request&response的时间、字节数等信息,这些数据都是做线上监控所必需的。
②、如何监听图片加载进度
关于图片加载现在大家也都是使用开源的解决方案,比较多的是Glide和Fresco了,我们需要做的是监听图片加载的整个过程,然后就可以计算出来每一步的耗时情况了,所以核心的是如何监听图片加载的全流程?这里具体的解决方案我就不再详细写了,下面把对应的链接提供给大家,直接点击查看详细的实现过程吧:
线下测试
线上监控分为两个部分:服务端监控和客户端监控
服务端监控
客户端监控
异常监控体系
OK,关于Android网络优化相关的介绍,就是上面这些了,那今天就先写到这里吧,各位小伙伴们,咱们下期再会!
祝:工作顺利!
Demo地址:https://github.com/JArchie/PerformanceOptimizeProject