Part 7 App网络优化
一 网络优化从哪些纬度开展
1、网络优化介绍
正确认识
1、网络优化需要从多个纬度展开
2、仅仅重视流量不够
3、网络流量消耗量:精准
4、整体均值掩盖单点问题(如:用户反馈app费流量,只统计流量消耗,不统计使用时长不好断定,还有前后台消耗流量的区分)
5、网络相关监控要全面
6、粗粒度监控不能帮助我们发现、解决深层次问题
2、网络优化纬度
流量消耗
一段时间流量消耗的精准度量,网络类型、前后台
监控相关:用户流量消耗均值、异常率(消耗多、次数多)
完整链路全部监控(Request、Response记录下来),主动上报(超过阈值)
网络请求质量
用户体验:请求速度、成功率
监控相关:请求时长、业务成功率、失败率、top失败接口
其他
公司成本:带宽、服务器数、CDN
耗电(网络请求密集)
3、网络优化误区
只关注流量消耗,忽视其他纬度
只关注平均值、整体,忽略个体
二 网络优化工具选择
1、NetWork Profiler
显示实时网络活动:发送、接收数据及连接数
需要启用高级分析
只支持HttpURLConnection和Okhttp网络库
启动高级分析功能
使用
2、抓包工具
工具的使用自己可以查看相关文档进行学习使用方法
Charles
Fiddler
wireshark
TcpDump
3、Stetho
应用调试桥,连接Android和Chrome
网络监控、视图查看、数据库查看、命令行扩展等
使用(相对与Charles来讲,功能不是很强大)
implementation 'com.facebook.stetho:stetho:1.5.0'
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.0'
//初始化
Stetho.initializeWithDefaults(mContext);
public class RetrofitNewsUtils {
private static final APIService API_SERVICE;
public static APIService getApiService() {
return API_SERVICE;
}
public static final String HTTP_SPORTSNBA_QQ_COM = "http://sportsnba.qq.com/";
static {
OkHttpClient.Builder client = new OkHttpClient.Builder();
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
Cache cache = new Cache(PerformanceApp.getApplication().getCacheDir(),10*1024*1024);
//添加拦截器
client.addNetworkInterceptor(new StethoInterceptor())
final Retrofit RETROFIT = new Retrofit.Builder()
.baseUrl(HTTP_SPORTSNBA_QQ_COM)
.addConverterFactory(FastJsonConverterFactory.create())
.client(client.build())
.build();
API_SERVICE = RETROFIT.create(APIService.class);
}
}
Chrome浏览器:chrome://inspect
三 精准获取流量消耗
问题思考:如何判断app流量消耗偏高
绝对值看不出高低
对比竞品,相同Case对比流量消耗
异常监控,超过正常指标
测试方案
设置——流量管理
抓包工具:只允许本App联网
可以解决大多数问题,但是线上场景线下可能遇不到
1、线上线下流量获取
线上流量获取方案
TrafficStats:API8以上重启以来的流量数据统计
getUidRxVytes(int uid)指定Uid的接收流量
getTotalTxBytes()总发送流量
总结:无法获取某个时间段内的流量消耗
NetworkStatsManager:API23之后流量统计
可获取指定时间间隔内的流量信息
可获取不同网络类型下的消耗
private void getNetStatus(long startTime, long endTime) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
}
long netDataRx = 0;//接收
long netDataTx = 0;//发送
TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
String subId = telephonyManager.getSubscriberId();
@SuppressLint("WrongConstant")
NetworkStatsManager manager = (NetworkStatsManager) getSystemService(Context.NETWORK_STATS_SERVICE);
NetworkStats networkStats = null;
NetworkStats.Bucket bucket = new NetworkStats.Bucket();
try {
networkStats = manager.querySummary(NetworkCapabilities.TRANSPORT_WIFI, subId, startTime, endTime);
} catch (RemoteException e) {
e.printStackTrace();
}
while (networkStats != null && networkStats.hasNextBucket()) {
networkStats.getNextBucket(bucket);
int uid = bucket.getUid();
if (getUidByPackageName() == uid) {
netDataRx += bucket.getRxBytes();
netDataTx += bucket.getTxBytes();
}
}
}
public int getUidByPackageName() {
int uid = -1;
PackageManager packageManager = MainActivity.this.getPackageManager();
try {
PackageInfo packageInfo = packageManager.getPackageInfo(MainActivity.this.getPackageName(), 0);
uid = packageInfo.applicationInfo.uid;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return uid;
}
2、前台后台流量获取
难题:线上反馈App后台跑流量,只获取一个时间段的值不够全面
方案:
getApplication().registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
//认为进入前台
appIsFront = true;
}
@Override
public void onActivityPaused(Activity activity) {
//认为进入后台
appIsFront = false;
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
});
Executors.newScheduledThreadPool(1).schedule(new Runnable() {
@Override
public void run() {
long netUse = getNetStatus(System.currentTimeMillis() - 30 * 1000, System.currentTimeMillis());//开始时间:当前时间-30秒,结束时间:就是当前时间
//前台还是后台
if (appIsFront) {
//前台
} else {
//后台
}
}
}, 30, TimeUnit.SECONDS);
private boolean appIsFront = false;
private long getNetStatus(long startTime, long endTime) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return 0;
}
long netDataRx = 0;//接收
long netDataTx = 0;//发送
TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
String subId = telephonyManager.getSubscriberId();
@SuppressLint("WrongConstant")
NetworkStatsManager manager = (NetworkStatsManager) getSystemService(Context.NETWORK_STATS_SERVICE);
NetworkStats networkStats = null;
NetworkStats.Bucket bucket = new NetworkStats.Bucket();
try {
networkStats = manager.querySummary(NetworkCapabilities.TRANSPORT_WIFI, subId, startTime, endTime);
} catch (RemoteException e) {
e.printStackTrace();
}
while (networkStats != null && networkStats.hasNextBucket()) {
networkStats.getNextBucket(bucket);
int uid = bucket.getUid();
if (getUidByPackageName() == uid) {
netDataRx += bucket.getRxBytes();
netDataTx += bucket.getTxBytes();
}
}
return netDataRx + netDataTx;
}
总结:
有一定的误差,在可接收范围内
结合精细化的流量异常报警针对性的解决后台跑流量
四 网络请求流量优化
1、使用网络的场景概述
数据:Api、资源包(升级包、H5、RN)、配置信息
图片:上传、下载
监控:APM相关、单点问题相关
2、数据缓存
服务端返回加上过期时间,避免每次重新获取
节约流量且大幅提高数据访问速度,更好的用户体验
Okhttp、Volley都有较好的实践
使用:
//创建拦截器
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(PerformanceApp.getApplication())){
builder.cacheControl(CacheControl.FORCE_CACHE);
}
return chain.proceed(builder.build());
}
}
public class RetrofitNewsUtils {
private static final APIService API_SERVICE;
public static APIService getApiService() {
return API_SERVICE;
}
public static final String HTTP_SPORTSNBA_QQ_COM = "http://sportsnba.qq.com/";
static {
OkHttpClient.Builder client = new OkHttpClient.Builder();
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
//设置缓存目录
Cache cache = new Cache(PerformanceApp.getApplication().getCacheDir(),10*1024*1024);
//设置拦截器
client.
eventListenerFactory(OkHttpEventListener.FACTORY).
addInterceptor(new NoNetInterceptor()).
addInterceptor(logging);
final Retrofit RETROFIT = new Retrofit.Builder()
.baseUrl(HTTP_SPORTSNBA_QQ_COM)
.addConverterFactory(FastJsonConverterFactory.create())
.client(client.build())
.build();
API_SERVICE = RETROFIT.create(APIService.class);
}
}
3、数据增量更新
加上版本的概念,只传输有变化的数据
配置信息、省市区等更新
4、数据压缩
Post请求Body使用GZip压缩
请求头压缩(只传递一次,以后只传递请求头的MD5值,服务端从之前的请求头中取)
图片上传之前必须压缩
//图片压缩库
implementation 'top.zibin:Luban:1.1.8'
// 以下代码是为了演示Luban这个库对图片压缩对流量方面的影响
Luban.with(holder.imageView.getContext())
.load(Environment.getExternalStorageDirectory()+"/Android/1.jpg")
.setTargetDir(Environment.getExternalStorageDirectory()+"/Android")
.launch();
//在demo中可以将1.94MB的图片压缩至446KB,且预览质量无差别
5、优化发送频率和时机
合并网络请求,减少请求次数(如埋点数据的统一上传)
性能日志上报:批量+特定场景上传(只在WiFi情况下上传)
6、图片优化
图片使用策略细化:优化缩略图
使用WebP格式图片(https://www.upyun.com/products/process#pic 里面有介绍)
五 网络请求质量优化
1、质量指标
网路请求成功率,网络请求速度
http请求过程
1、请求到达运营商的DNS服务器并解析成对应的IP地址
2、创建链接(TCP三次握手),根据IP地址找到相应的服务器,发送一个请求
3、服务器找到对应资源原路返回给访问的用户
DNS相关
问题:DNS被劫持,DNS解析慢
方案:使用HttpDNS,绕过运营商域名解析过程(传统DNS向DNS53端口发送,HttpDNS向80端口发送)
优势:降低平均访问时长、提高连接成功率
参考文档:
https://blog.csdn.net/z_xiaozhuT/article/details/80596469 (深入理解Http请求、DNS劫持与解析)
https://blog.csdn.net/yb223731/article/details/82858057 (HttpDNS 服务详解)
2、使用OKhttp和HttpDNS
//依赖
compile ('com.aliyun.ams:alicloud-android-httpdns:1.1.7@aar') {
transitive true
}
//添加OkHttpDNS解析类
public class OkHttpDNS implements Dns {
private HttpDnsService dnsService;
private static OkHttpDNS instance = null;
private OkHttpDNS(Context context) {
dnsService = HttpDns.getService(context, "");
}
public static OkHttpDNS getIns(Context context) {
if (instance == null) {
synchronized (OkHttpDNS.class) {
if (instance == null) {
instance = new OkHttpDNS(context);
}
}
}
return instance;
}
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
String ip = dnsService.getIpByHostAsync(hostname);
//如果不为空走OkhttpDNS解析
if(ip != null){
List<InetAddress> inetAddresses = Arrays.asList(InetAddress.getAllByName(ip));
return inetAddresses;
}
//如果为空走系统的DNS解析
return Dns.SYSTEM.lookup(hostname);
}
}
public class RetrofitNewsUtils {
private static final APIService API_SERVICE;
public static APIService getApiService() {
return API_SERVICE;
}
public static final String HTTP_SPORTSNBA_QQ_COM = "http://sportsnba.qq.com/";
static {
OkHttpClient.Builder client = new OkHttpClient.Builder();
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
Cache cache = new Cache(PerformanceApp.getApplication().getCacheDir(),10*1024*1024);
//在OkHttpClient中添加dns
client.
eventListenerFactory(OkHttpEventListener.FACTORY).
dns(OkHttpDNS.getIns(PerformanceApp.getApplication())).
addInterceptor(new NoNetInterceptor()).
addInterceptor(logging);
final Retrofit RETROFIT = new Retrofit.Builder()
.baseUrl(HTTP_SPORTSNBA_QQ_COM)
.addConverterFactory(FastJsonConverterFactory.create())
.client(client.build())
.build();
API_SERVICE = RETROFIT.create(APIService.class);
}
}
3、协议版本升级
1.0:版本的TCP连接不复用
1.1:引入持久连接,但数据通讯按次序进行
2:多工,客户端、服务器双向实时通信
4、网络请求质量监控
接口请求耗时、成功率、错误码等
图片加载的每一步耗时
请求的监控
实现okhttp的EventListener
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("lz","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<InetAddress> inetAddressList) {
super.dnsEnd(call, domainName, inetAddressList);
okHttpEvent.dnsEndTime = System.currentTimeMillis();
}
@Override
public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) {
super.connectStart(call, inetSocketAddress, proxy);
}
@Override
public void secureConnectStart(Call call) {
super.secureConnectStart(call);
}
@Override
public void secureConnectEnd(Call call, @Nullable Handshake handshake) {
super.secureConnectEnd(call, handshake);
}
@Override
public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol) {
super.connectEnd(call, inetSocketAddress, proxy, protocol);
}
@Override
public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol, IOException ioe) {
super.connectFailed(call, inetSocketAddress, proxy, protocol, ioe);
}
@Override
public void connectionAcquired(Call call, Connection connection) {
super.connectionAcquired(call, connection);
}
@Override
public void connectionReleased(Call call, Connection connection) {
super.connectionReleased(call, connection);
}
@Override
public void requestHeadersStart(Call call) {
super.requestHeadersStart(call);
}
@Override
public void requestHeadersEnd(Call call, Request request) {
super.requestHeadersEnd(call, request);
}
@Override
public void requestBodyStart(Call call) {
super.requestBodyStart(call);
}
@Override
public void requestBodyEnd(Call call, long byteCount) {
super.requestBodyEnd(call, byteCount);
}
@Override
public void responseHeadersStart(Call call) {
super.responseHeadersStart(call);
}
@Override
public void responseHeadersEnd(Call call, Response response) {
super.responseHeadersEnd(call, response);
}
@Override
public void responseBodyStart(Call call) {
super.responseBodyStart(call);
}
@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("lz","callFailed ");
super.callFailed(call, ioe);
okHttpEvent.apiSuccess = false;
okHttpEvent.errorReason = Log.getStackTraceString(ioe);
Log.i("lz","reason "+okHttpEvent.errorReason);
}
}
//实体信息类
public class OkHttpEvent {
public long dnsStartTime;
public long dnsEndTime;
public long responseBodySize;
public boolean apiSuccess;
public String errorReason;
}
//添加到OkHttpClient中
client.
eventListenerFactory(OkHttpEventListener.FACTORY).
dns(OkHttpDNS.getIns(PerformanceApp.getApplication())).
addInterceptor(new NoNetInterceptor()).
addInterceptor(logging);
5、网络容灾机制
备用服务器分流
多次失败后一定时间内不进行请求,避免雪崩效应
6、其他
CDN加速、提高带宽、动静资源分离(更新后清理缓存)
减少传输量,注意请求时机及频率
OKhttp的请求池
六 网络体系化的方案建设
1、线下测试相关
方案:至抓单独App
侧重点:请求有误、多余、网络切换、弱网、无网测试
2、线上监控相关
服务端:
请求耗时(区分地域、时间段、版本、机型)
失败率(业务失败和请求失败)
Top失败接口、异常接口
客户端:
接口每一步详细信息(DNS、连接、请求等)
请求次数、网络包大小、失败原因
图片监控
异常监控体系
服务器防刷:超限拒绝访问
客户端大文件预警、异常兜底策略(如重连5次都失败了,延长重连时间,或者取消)
单点问题追查