经过前一阶段的调查,大概对性能优化已经有了初步的解决方案:
先给大家介绍一下UC公司的性能优化指标以及部分方案:
一、性能优化六项指标:
性能、内存、稳定性、流量、电量、安装包大小;
二、背景 ---- Android程序卡顿产生原因:
1、Android系统低效
--渲染线程、同步接口、广播机制
:没有独立的渲染线程
:广播机制引入,可能同时又几百个广播机制在后台运行
2、运行环境恶劣
--后台进程、安全软件
3、低端机占比高
--低内存、弱cpu、IO瓶颈
:开源平台,导致高中低端的机型普遍存在;;
:低内存影响最大,一般可用内存在小于50M,意味着会由于小于50M就会杀死一些进程来维护内存的大小
: GPU是其次;
:读写速度比较慢,在有的手机上;
4、产品考虑不足
--功能定义简陋、功能堆积严重
: 一般的产品只会考虑需求,我要做什么,而并没有把整个闭环考虑清楚;
:在版本迭代的过程,在不注意间可能启动过程会越来越慢;
5、技术考虑不足
--很多
三、用户反馈应用卡顿怎么办?
困难:
1、复现性
-- 用户描述模糊、不稳定出现(复现率比较低);
2、定位难
-- 不同机型、固件、系统状态表现不一
--程序细节多、可疑面广
3、衡量难
-- 卡顿严重程度难以量化
-- 卡顿问题不便分类
: 是有一点卡、非常卡、还是什么
: 没有针对性的目标,提升百分之多少等等,不知道极限在哪里;
四、解决思路
卡 vs 顿,卡为主,顿为辅。卡和顿没有一个明显的界限,大部分顿的问题当环境足够恶劣时就会表现为卡。所以抓住卡,就能解决很多 问题。 2、打点统计 vs 全局监控:
短期目标:主路径性能保障,打点统计;
长期目标:整体的卡顿优化,全局监控; 3、 线下分析 vs 线上监控:
线下分析:实验室调试去复现一个问题,精确定位、粒度细;
线上监控:指标衡量、粒度粗。
4、打点统计分析:
(1)、启动速度
(2)、响应速度
(3)、版本比对 : app版本、Android版本
5、用户反馈分析:
将用户性能方面的反馈,测试人员进行分析,以邮件形式发送给技术负责人,进行分析;
反馈等级:
--预警机制
--用户分类
--功能分类
-- 纵向对比
6、anr日志分析:
--精确定位 : 堆栈信息比较清晰
--数据量化
主线程超时(5s ---> 1s)
-- 暴漏更多蕾体
-- 精确定位问题
-- 方便用户联调
-- 如果一个按钮响应时间超过800ms,用户感知起来就会很难受了。
7、全局监控 -- Looper Hook
-- 监测系统消息循环
--计算消息耗时
-- 定位耗时点
卡顿:无非就是主线程被卡住了,就是主线程的消息循环里面的某一帧执行时间非常长,导致后续的消息无法来得及执行,
数据指标卡顿率: 卡顿用户数/日活总数
8、 问题回顾:
-- 下载界面展开卡顿: 分段加载
-- 二维码界面展示慢: 延时加载、先出界面在初始化相机
-- 启动完成后操作卡: 线程枪战,低优先级后台进程+队列
-- 共享存储卡顿: sharedPerference( 主线程IO , commit(主) ---> apply(子))
-- 视频播放控制卡顿(API兼容性问题,异步化,视频播放停止暂停线程)
-- 获取网络代理卡顿(IPC异常【进程间通信】,异步DNS+缓存)
-- 第三方反馈卡死(固件问题,shield Activity,全部采用一个新的activity去做,这样不会对原来activity产生影响)
-- 网页滑屏操作卡顿(GPU加速 开启硬件加速)
-- So加载/jni注册卡(异步加载 + 时序控制)
-- 安全软件事件拦截(沟通反馈)
五、经验推广:
禁止:
-- 主线程文件IO(标记文件读写外)
-- 主线程耗CPU操作
-- 主线程同步IPC调用(时间不可预期)
推荐:
-- 异步化
【1】、 产品及程序设计 : 加载肯定是需要时间的,不可能实时展现;
【2】、预加载 (数据必备,功能执行之前将这些事先数据准备好)
【3】、闲时加载: 利用cpu的闲时做一些事情,主线程会设置一个ido handler,主线程所有消息操作完成之后会回调一个handler
【4】、按需加载
-- 线程管理
1、线程量限制 + 任务队列
2、非主线程优先级调低
--压力测试
-- 防御式编程
-- 全局性能检测
六、延伸
-- 精确化 & 自动化
用户反馈、卡顿日志
-- 新监控方案
Api Hook
-- 新优化方案
卡顿率 --> 帧率
低端机优化
-----------------------------------------------------UC浏览器方案结束-------------------------------------------------------
个人性能优化方案
Android性能优化代码规范
l 编码之初准备篇:
l 对于布局内容的数量要求:
单个Activity显示的视图一般情况少于20,层数少于4
对于Adapter控件,如ListView ,item的布局层数一般情况为2,不得超过3.
l 将Acitivity 中的Window 的背景图设置为空:
getWindow().setBackgroundDrawable(null);
android的默认背景不为空。
l 将Activity的背景放到Activity的Theme中设置。同时避免fragment和activity背景重复设置:
Theme设置属性
<item name="android:windowBackground">src_image</item>
l 采用硬件加速:
androidmanifest.xml中application添加
android:hardwareAccelerated="true"。
需要注意的是:android 3.0以上才可以使用。
l 使用ProGuard去除不必要的代码:
#删除无用的类
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
public static *** e(...);
public static *** i(...);
public static *** w(...);
}
l apk打包签名时,使用zipalign工具对齐:
zipAlignEnabled true
l 后台可以处理的逻辑不要放在前台,这样可能会有预料不到的问题
l 内存泄露引入三方框架LeakCanary :使用超级方便:
l Android程序冷启动优化(第一次启动应用):
1、在logoactivity设置一个theme,设置windowBackground属性,避免黑屏阶段。
2、对app进行延迟启动控制,采用延迟加载技术
private Handler handler = new Handler();
//延迟加载 runnable
private Runnable delayLoadRunnable = new Runnable() {
@Override
public void run() {
Logger.d("start delayLoadRunnable ");
init();
}
};
//优化的DelayLoad : 采用延迟加载策略
window.getDecorView().post(new Runnable() {
@Override
public void run() {
handler.post(delayLoadRunnable);
}
});
Activity 在启动时,会在第二次执行 performTraversals 才会去真正的绘制,原因在于第一次执行 performTraversals 的时候,会走到 Egl 初始化的逻辑,然后会重新执行一次 performTraversals 。 所以有人问为何在 run 方法里面还要 post 一次,如果在 run 方法里面直接执行 updateText 方法 ,那么 updateText 就会在第一个 performTraversals 之后就执行,而不是在第一帧绘制完成后才去执行,所以我们又 Post 了一次 。所以大概的处理步骤如下:
第一步:Activity.onCreate –> Activity.onStart –> Activity.onResume
第二步:ViewRootImpl.performTraversals –>Runnable
第三步:Runnable –> ViewRootImpl.performTraversals
第四步:ViewRootImpl.performTraversals –> init();
第五步:init();
l 禁止(避免)操作篇:
核心:少的对象创建,意味着少的GC操作。 杜绝引起内存溢出、内存抖动的操作行为;
l 禁止在单例模式中引用Activity的context:
l 禁止使用枚举:
使用枚举访问速度要比static变量慢4倍,枚举将造成大量的内存浪费;
l 禁止使用异步回调:
异步回调被执行的时间不确定,很有可能发生在activity已经被销毁之后,
这不仅仅很容易引起crash,还很容易发生内存泄露。
l 禁止static引用资源耗费过多的实例:
例如:context , Activity
对于某些不得不出现static引用context的情况,在onDestroy()方法中,解除Activity与static的绑定关系,
从而去除static对Activity的引用,使Context能够被回收;
l 避免在循环(for、while、listView - getView方法、onDraw)里创建对象:
对于onDraw中 Paint 我们可以这样优化
private Paint paint = new Paint();
public on Draw(){
paint.setColor(mBorderColor);
}
l 避免使用static成员对象:
static生命周期过长,对于需要传递的对象,使用(Intent)和(Handler)
l 避免使用浮点数:
浮点数会比整型慢两倍
l 避免Timer.schedule,对于延时操作,可用以下方式代替:
ScheduledExecutorService,
handler.postDelayed,
handler.postAtTime ,
handler.sendMessageDelayed ,
View.postDelayed,
AlarmManager
l 避免加载过大图片。压缩或者使用对象池后再使用
l 避免使用递归
l 避免使用轮询
l 避免长周期内部类、匿名内部类长时间持有外部类对象导致相关资源无法释放。如:Handler, Thread , AsyncTask
l 避免使用三方库,不需要的东西需要剔除
l 避免使用注解框架,毕竟是反射
l 非必要情况下,少用抽象
l 避免频繁网络请求
访问server端时,建立连接本身比传输需要跟多的时间,如非必要,不要将一交互可以做的事情分成多次交互(这需要与Server端协调好)。有效管理Service 后台服务就相当于一个持续运行的Acitivity,如果开发的程序后台都会一个service不停的去服务器上更新数据,在不更新数据的时候就让它sleep,这种方式是非常耗电的,通常情况下,可以使用AlarmManager来定时启动服务。如下所示,第30分钟执行一次。
1. AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALAR M_SERVICE);
2. Intent intent = new Intent(context, MyService.class);
3. PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
4. long interval = DateUtils.MINUTE_IN_MILLIS * 30;
5. long firstWake = System.currentTimeMillis() + interval;
6. am.setRepeating(AlarmManager.RTC,firstWake, interval, pendingIntent);
l 优化操作建议篇:
l 当数据量在100以内时,使用ArrayMap代替HashMap
l 为了避免自动装箱,当数量在1000以下时,使用如下容器
a)SparseBoolMap <bool , obj>
b)SparseIntMap <int , obj>
c)SparseLongMap <long , obj>
d)LongSparseMap <long ,obj>
l 字符串拼接用StringBuilder或StringBuffer
//这种string第一次初始化的情况下,下面得效率更高
String str1 = "abc"+“def”+"hij";
//非并发情况 , StringBuilder效率更优
StringBuilder str2 = str3 + str1 + "builder" ;
//并发情况使用 StringBuffer
StringBuffer str2 = str1 + "buffer" ;
l 文件、网络IO缓存,使用有缓存机制的输入流
BufferedInputStream替代InputStream
BufferedReader替代Reader
BufferedReader替代BufferedInputStream.
l 考虑使用Webp代替传统png图片。对于某些使用JPEG即可实现的效果,尽量采用JPEG
png虽能提供无损的图片,但相对于JPEG过大。Webp是既保持png优点,又能减少图片大小的新型格式.
l 尽量使用局部变量:
l 如果没有特殊需求,使用基本数据类型,而非对象类型
基本类似指:int , double , char等。
l 对于使用超过两次的对象成员, 将成员缓存到本地
反复使用的变量,保存到本地成为临时变量活成员变量后进行操作。尤其是在循环中
例:多次比较目标时间和当前时间差。
l 当new的对象并不是100%一定会被用到时,在使用时创建,有效减少不必要的对象生成
例如: Object ob = new Object();
int value;
if(i>0) value = ob.getVlaue();
改写为:int value;
if(i>0){
Object ob = new Object(); //用到时加载
value = ob.getVlaue();
}
l 及时释放不用的对象
a = new Object();
当a不为空时,应改写为:
a = null;
a = new Object();
l 不在使用的变量,手动置为null
通常对于对象成员如此使用,局部变量不需要
this.object = null;
l 常量用 static final修饰
l 对bitmap进行恰当的操作:
读取图片之前先查看其大小:
1. BitmapFactory.Options opts = new BitmapFactory.Options();
2. opts.inJustDecodeBounds = true;
3. Bitmap bitmap = BitmapFactory.decodeFile(imageFile, opts);
使用得到的图片原始宽高计算适合自己的smaplesize:
1. BitmapFactory.Options opts = new BitmapFactory.Options();
2. opts.inSampleSize = 4 ;// 4就代表容量变为以前容量的1/4
Bitmap bitmap = BitmapFactory.decodeFile(imageFile, opts);
对于过时的Bitmap对象一定要及时recycle,并且把此对象赋值为null:
1. bitmap.recycle();
2. bitmap = null;
l 布局用Java完成比XML快
l 默认不会显示的布局使用 viewstub 标签
<ViewStub
android:id="@+id/network_error_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/network_error" />
//非显示的转换ViewStub 获取
View viewStub = findViewById(R.id.network_error_layout);
viewStub.setVisibility(View.VISIBLE); // ViewStub被展开后的布局所替换
networkErrorView = findViewById(R.id.network_error_layout); // 获取 展开后的布局
l 对于两次以上相同的infalte操作,用成员变量代替局部变量,避免重复加载
l 正确使用fragment
界面绘制尽量使用fragment代替activity,fragment根据情况使用hide与add方式,还是replace
if (!showFragment.isAdded()) { // 先判断是否被add过
transaction.hide(currentFragment).add(R.id.fl_content, showFragment)
.commitAllowingStateLoss(); // 隐藏当前的fragment,add下一个到Activity中
} else {
// 隐藏 当前的fragment,显示下一个
transaction.hide(currentFragment).show(showFragment).commitAllowingStateLoss();
}
this.currentFragment = showFragment;
l 对于重复出现超过2-3次的子布局,用 include 实现复用
<include layout="@layout/foot.xml" />
l 当复用的布局中子View对所依赖的根节点要求不高时,使用 merge 作为根节点
要求不高标准:非复杂结构布局,无Background,padding等属性,且子View数量较少
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40"
android:layout_above="@+id/text"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40"
android:layout_alignParentBottom="true"
android:text="@string/app_name" />
</merge>
l 数据压缩:
传输数据经过压缩 目前大部门网站都支持GZIP压缩,所以在进行大数据量下载时,尽量使用GZIP方式下载,可以减少网络流量,一般是压缩前数据大小的30%左右。
1. HttpGet request = new HttpGet("http://example.com/gzipcontent");
2. HttpResponse resp = new DefaultHttpClient().execute(request);
3. HttpEntity entity = response.getEntity();
4. InputStream compressed = entity.getContent();
5. InputStream rawData = new GZIPInputStream(compressed);