qmsggg_BlogCollect
qmsggg_BlogCollect copied to clipboard
Android应用性能优化最佳实践
FPS
FPS(Frames Per Second):表示每秒传递的帧数。在理想情况下,60FPS就感觉到不卡,这也就是说每个绘制时长应该是在16ms以内,即1000ms/60; 但是Android系统很有可能无法及时完成那些复杂的界面渲染操作。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需的60FPS,即为了实现60FPS,就意味着程序的大多数绘制操作都必须在16ms内完成。
http://androidperformance.com/ 领导推荐的一个网址,里面有性能优化相关的内容
Android主线程的主要职责:
- UI生命周期控制
- 系统事件处理
- 消息处理
- 界面布局
- 界面绘制
- 界面刷新
双缓冲
双缓冲:显示内容的数据内存,为什么要用双缓冲,我们知道在LInux上通常使用FrameBuffer来做显示输出,当用户进程跟新FrameBuffer中的数据后,显示驱动会把FrameBuffer中每个像素点的值更新到屏幕,但是这样会带来一个问题,如果上一帧的数据还没显示完,FrameBuffer中的数据又更新来,就会带来残影的问题,给用户直观的感觉就会有闪烁,所以普遍采用来双缓冲技术。双缓冲意味着要使用两个缓冲区(在SharedBUfferStack中),其中一个称为Front Buffer,另外一个称为BackBuffer。UI总是先在BackBUffer中绘制,然后再和Front Buffer交换,渲染到显示设备中。即只有当另一个buffer的数据准备好后,通过io_ctrl来通知显示设备切换Buffer。
GPU
应用层绘制一个页面(View),主要有三个过程:CPU准备数据->GPU从数据缓存列表获取数据->Display设备绘制。这个三个过程的耗时可以通过一个手机开发辅助工具查看:Profile GPU Rendering。PGR是从Android4.1系统开始提供的。 PGR功能特点如下:
- 它是一个图形监测工具,能实时反应当前绘制的耗时。
- 横轴表示时间,纵轴表示每一帧的耗时(单位为ms)。
- 随着时间的推移,从左到右的刷新呈现。
- 提供来一个标准的耗时,如果高于标准耗时,表示当前这一帧丢失。
不同颜色解释:
- 每一条柱状图都由4种颜色组成:红,黄,蓝,紫,这些对线对应每一帧在不同阶段的实际耗时。
- 蓝色代表测量绘制的时间,它代表需要多长时间区创建和更新DisplayList。在Android中,一个视图在进行渲染之前,它必须被转换称GPU熟悉的格式,简单来说就是几条绘图命令,蓝色就是记录了在屏幕上更新视图需要花费的时间,也可以理解为执行每一个View的onDraw方法,创建或者更新每一个View的Display List对象。在蓝色的线很高时,有可能因为需要重新绘制,或者自定义视图的onDraw函数处理事情太多。
- 红色代表执行的时间,这部分是Android进行2D渲染Display List的时间,为了绘制到屏幕上,Android需要使用OpenGl ES的API接口来绘制Display List,这些API有效的将数据发送到GPU,最终在屏幕上显示出来。当红色的线非常高时,可能是由重新提交来视图而导致的。
- 橙色部分表示处理时间,或者是CPU告诉GPU渲染一帧的地方,这是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复,如果柱状图很高,就意味着GPU太繁忙了。
- 紫色段表示将资源转移到渲染线程的时间,只有在Android4.0及以上的版本才会提供。
在实际的开发周宏,从图形上虽然可以看到绘制的时间,但对于不便进行数据分析,比如进入某一个页面,柱状图虽然实时绘制出来,但不能封号地分析,这里可以通过:adb shell dumpsys gfxinfo com..(包名)把具体的耗时输出到日志中来分析。 任何时候超过绿线(警戒线,对应时长16ms),就由可能丢失一帧的内容,虽然对于大部分应用来说,丢失几帧确实感觉不出来卡顿。在用GPR查看了有问题的页面后就可以用Hierarchy Viewer来优化布局。
TraceView:
TraceView是AndroidSDK自带的工具,用来分析函数调用过程,可以对Android的应用程序以及FrameWork层的代码进行性能分析。它是一个图形化的工具,最终会产生一个图表,用于对性能分析进行说明,可以分析到应用具体每一个方法的执行时间,使用可以非常直观简单,分析性能问题很方便。
- 使用方法
在使用TraceView分析之前需要得到一个*.trace的文件,然后通过TraceView来分析trace文件的信息,trace文件的获取有两种方式:
(1)在DDMS中使用
(2)在代码中使用
- 在需要开始监控的地方调用android.os.Debug.startMethodTracing().
- 在需要结束监控的地方调用android.os.Debug.stopMethodTracing().
- 系统会在SDK卡中创建(trace-name).trace文件
- 使用traceview打开该文件进行分析. 注:需要使用WRITE_EXTERNAL_STORAGE权限。
2.TraceView视图说明
- Name:所有的调用项,展开可以看到所有的Parent和Children子项,指调用和被调用
- Inclusive:统计函数本身运行的时间 + 调用子函数运行的时间
- Incl: inclusive时间占总时间的百分比
- Exclusive: 同级函数本身运行的时间
- Excl: 执行占总时间的百分比
- Calls+Recur Calls /Total 该方法调用次数 + 递归次数
- Cpu Time / Call 该方法耗时
- Real Time / Call 实际时长
使用TraceView查看耗时时,主要关注Calls + Treur Galls / Total 和Cpu Time / Call这两个值,也就是关注调用次数多和耗时久的方法,然后优化这些方法的逻辑和调用次数,减少耗时。
RealTime与CpuTime区别为:因为RealTime包括来CPU的上下文切换,阻塞,GC等,所以RealTime方法的实际执行时间要比CPU Time稍微长一点。
SysTrace使用方法
布局优化工具Hierarchy Viewer
布局层级检测 Android Lint
布局优化方法
-
减少层级
- 合理使用RelativeLayout和LinearLayout
- 合理使用Merge ReativeLayout也存在性能低的问题,原因是RelativeLayout会对子View做两次测量,在RelativeLayout中View的排列方式是基于彼此的依赖关系,因为这个依赖关系可能和布局中VIew的顺序并不同,在确定每个子VIew的位置时,需要先给所有子View做一次排序。如果在RelativeLayout中允许子View横向和纵向相互依赖,就需要横向,纵向分别进行一次排序测量。但如果在LinearLayout中有weight属性,也需要进行两次测量,因为没有更多的依赖关系,所以仍然会比RelativeLayout效率高,在布局上RelativeLayout不如LinearLayout快。 但是如果布局本身层次太深,还是推荐使用RelativeLayout减少布局本身层次,相较于测量两次,虽然会增加一些计算时间,但是在体验上影响不会太大,如果优化掉两层布局仅仅是增加一次测量,还是非常值得的,布局层次深会增加内存消耗,甚至引起栈溢出等问题,即使耗点时间,也不能让应用不可用。 根据以上分析,可以总结出一下几点布局原则:
- 尽量使用RelativeLayout和LinearLayout。
- 在布局层级相同的情况下,使用LinearLayout。
- 用LinearLayout有时会使嵌套层级变多,应该使用RelativeLayout,使用面尽量扁平化。
-
Merge的使用 使用Merge的场合主要有以下两种:
- 在自定义View的使用中,父元素尽量是FrameLayout和LinearLayout。
- 在Activity中整体的布局,根元素需要时FrameLayout。 原理就是Android布局的源码中,如果是Merge标签,那么直接将其中的子元素添加到Merage标签Parent中。 注:如果Merge 代替的布局元素为LinearLayout,在定义布局代码中将LinearLayout的属性添加到引用上,如垂直布局,背景色等。
使用Merge需要注意:
- Merge只能用在布局XML文件的根元素
- 使用merge来加载一个布局时,必须指定一个ViewGroup作为其父元素,并且要设置加载的attachToRoot参数为true(参照inflate(int, ViewGroup, boolean))。
- 不能在ViewStub中使用Merge标签。原因就是ViewStub的inflate方法中没有attachToRoot的设置。
布局优化方法之提高显示速度
背景:
使用VIEW.GONE等来隐藏View效率非常低,因为它还在布局当中。仍然会测试和解析这些布局。
ViewStub使用
ViewStub是一个轻量级的View,它是一个看不见的,并且不占用布局位置,占用资源非常消的视图对象,可以为ViewStub指定一个布局,加载布局是,只有ViewStub会被初始化,然后当ViewStub被设置为可见时,或者调用了ViewStub.inflate()时,Viewstub所指向的布局会被加载和实例化,然后ViewStub的布局属性都会传给它指向的布局
ViewStub使用注意
- ViewStub只能加载一次,之后ViewStub对象会被设置为空,换句话说,某个被ViewStub指定的布局被加载后,就不能在通过ViewStub来控制它来,所以它不适用于需要按需来显示隐藏的情况。
- ViewStub只能用来加载一个布局文件,而不是某个具体的View,当然也可以把View写在某个布局文件中,如果想操作一个具体的View,还是使用visibility属性。
- ViewStub不能使用嵌套的Merge标签。
ViewStubshying场景
- 在程序运行期间,某个布局在加载后,就不会有变化,除非销毁该页面再重新绘制加载。
- 想要控制显示于隐藏一个布局文件,而非某个VIew
布局优化方法之布局复用
include标签
Android的布局复用通过
优化自我总结:
1.尽量使用RelativeLayout和LinearLayout,不要使用绝对布局AbsoluteLayout
2.将可复用的组件抽取出来并通过 标签使用
3.使用<ViewStub/>标签加载一些不常用的布局
4.使用<merge/>标签减少布局的嵌套层次
5.尽可能少用wrap_content,wrap_content会增加布局measure时的计算成本,已知宽高为固定值时,不用wrap_content
6.删除控件中的无用属性
避免过度绘制
过度绘制(Overdraw)是在在屏幕上的某个像素在同一帧的时间内被绘制来多次。在多层次重叠的UI结构(如带背景的TextView)中,如果不可见的UI也在做绘制的操作,就会导致某些像素区被绘制来多次。
导致过度绘制的原因
- XML布局 --- 控件有重叠且都有设置背景
- View自绘 --- View.onDraw里面同一个区域被绘制多次
过度绘制检测工具Show GPU OverDraw
- 无色: 没有过度绘制,每个像素绘制来1次
- 蓝色:每个像素多绘制来1次,
- 绿色:每个像素多绘制来2次
- 淡红:3次,这个区域不超过屏幕的1/4是可以接受的
- 深红:4次及以上,需要优化
如何避免过度绘制之布局上的优化
- 移除XML中非必需的背景,或根据条件设置
- 移除Window默认的背景
- 按需显示占位背景图片 使用Android自带的一些主题时,activity往往会被设置一个默认的背景,这个背景由DecorView持有,当自定义布局有一个全屏的背景时,比如设置来这个界面的全屏黑色背景,DecorView的背景此时对我们来说是无用的,但是它会产生一次OverDraw,因此没有必要的话,也可以移除。
protected void onCreate(Bundle savedInstancestate) {
super.onCreate(savedInstanceState)
this.getWindow().setBackgroundDrawable(null);
}
针对ListView中的Avatar ImageView的设置,在getView的代码中,判断是否获取对应的Bitmap,获取Avatar的图像之后,把ImageView的Background设置为Transparent,只有当图像没有获取到时,才设置对应的Background占位图片。
自定义View过度绘制优化
Canvas.ClipRect
启动优化之应用启动流程
Android 应用程序的载体是APK文件,其中包括来组件和资源,APK文件可能运行在一个独立的进程中,也有可能产生多个进程,还可以多个APK运行在同一个进程中,可以通过不同的方式来实现。 但需要注意两点:
- 第一:每个应用只对应一个Application对象,并且启动应用一定会产生一个Application对象;
- 第二:应用程序可视化组件Activity是应用的基本组成之一;
Application
Application 是Android系统框架中的一个系统组件,Android程序启动时,系统会创建一个Application对象,用来存储系统的一些信息。Android系统会自动在每个程序运行时创建一个Application类的对象,并且只创建一个,可以理解为Application是一个单例类。 应用可以不指定一个具体的Application,系统会自动创建,但一般在开发中都会创建一个继承于系统Applicaton的类实现一些功能,比如一些数据库的创建,模块的初始化等。但这个派生类必须在AndroidManifest.xml中定义好,在application标签增加name属性,并添加自己的Application的类名。 启动Application时,系统会创建一个PID,即进程ID,所有的Activity都会在此进程上运行。在Application创建初始化全局变量,同一个应用的所有Activity都可以读取到这些全局变量的值,Application的生命周期是整个应用程序中最长的,它的生命周期等于这个应用程序的生命周期,因为它是全局的单例的,所以在不同的Activity或者Service中获得的对象都是同一个对象。因此在安卓中要避免使用静态变量来存储长久保存的值,可以用Application,但并不建议使用太多的全局变量。
public class LibApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public void onTerminate() {
super.onTerminate();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
@Override
public void onLowMemory() {
super.onLowMemory();
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
}
}
- attachBaseContext(Context base):得到应用上下文的Context,在应用创建时首先调用。
- onCreate():应用创建是调用,晚于attachBaseContext(Context base)方法。
- onTerminate():应用结束时调用。
- onConfigurationChanged(Configuration newConfig):系统配置发生变化时调用。
- onLowMemory(): 系统低内存时调用。
- onTrimMemory(int level): 系统要求应用释放内存时调用,level为级别。
注意:
在开发过程中,尽量使用Application中的Context实例,因为使用Activity中的Context可能会导致内存泄漏。也可以使用Activity的getApplicationContext方法。
Activity
Activity生命周期金字塔模型
onCreate方法包括一个savedInstanceState参数,在有关重新创建Activity中非常有用。
应用启动的流程
- 冷启动: 因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,在创建和初始化MainActivity类,最后显示界面
- 热启动: 因为会从已有的进程中启动,所以热启动不会在创建和初始化Application,而是直接创建和初始化MainActivity,即Application只会初始化一次,只包含Activity中的生命周期。
启动->Applicaiton->attachBaseContext()->onCreate()->Activity生命周期。
启动优化之启动耗时检测
1.adb shell am
- ThsTime:一般和TotalTime时间一样,如果在应用启动时开了一个过度的全通明的页面(Activity)预先处理一些事,再显示出主页面(Activity),这样将比TotalTime小。
- TotalTime:应用的启动时间,包括创建进程+Applicaton初始化+Activity初始化到界面显示。
- WaitTime:一般比TotalTime大些,包括系统影响的耗时。
2.代码打点
package com.errarehest.android.androidlib.utils.timemonitor;
import android.util.Log;
import java.util.HashMap;
import java.util.Iterator;
/**
* Created by luohao on 2017/11/7.
* Created date 2017/11/7
* 统计耗时的数据结构
*/
public class TimeMonitor {
private final String TAG = TimeMonitor.class.getSimpleName();
private int mMonitorId = -1;
// 保存一个耗时统计模块的各种耗时,tag对应某一个阶段的时间
private HashMap<String, Long> mTimeTag = new HashMap<>();
private long mStartTime = 0;
public TimeMonitor(int id) {
mMonitorId = id;
}
public int getMonitorId() {
return mMonitorId;
}
public void startMoniter() {
// 每次重新启动,都需要把前面的数据结构清除,避免统计到错误数据
if (mTimeTag.size() > 0) {
mTimeTag.clear();
}
mStartTime = System.currentTimeMillis();
}
// 打一次点,tag交线需要统计的上层自定义
public void recodingTimeTag(String tag) {
// 检查是否保存过相同的tag
if (mTimeTag.get(tag) != null) {
mTimeTag.remove(tag);
}
long time = System.currentTimeMillis() - mStartTime;
mTimeTag.put(tag, time);
}
public void end(String tag, boolean writeLog) {
recodingTimeTag(tag);
end(writeLog);
}
public void end(boolean writeLog) {
if (writeLog) {
// 写入文件
}
testShowData();
}
public void testShowData() {
if (mTimeTag.size() <= 0) {
return;
}
Iterator iterator = mTimeTag.keySet().iterator();
while (iterator.hasNext()) {
String tag = (String) iterator.next();
Log.d(TAG, tag + ":" + mTimeTag.get(tag));
}
}
public HashMap<String, Long> getTimeTag() {
return mTimeTag;
}
}
package com.errarehest.android.androidlib.utils.timemonitor;
/**
* Created by luohao on 2017/11/7.
* Created date 2017/11/7
*/
public class TimeMonitorConfig {
public static final int TIME_MONITOR_ID_APPLICATION_START = 1;
public static final int TIME_MONITOR_ID_RV_TEST = 2;
}
package com.errarehest.android.androidlib.utils.timemonitor;
import android.content.Context;
import java.util.HashMap;
/**
* Created by luohao on 2017/11/7.
* Created date 2017/11/7
*/
public class TimeMonitorManager {
private static TimeMonitorManager sTimeMonitorManager = null;
private static Context sContext = null;
private HashMap<Integer, TimeMonitor> mTimeMonitorList = null;
public synchronized static TimeMonitorManager getInstance() {
if (sTimeMonitorManager == null) {
sTimeMonitorManager = new TimeMonitorManager();
}
return sTimeMonitorManager;
}
public TimeMonitorManager() {
mTimeMonitorList = new HashMap<>();
}
public void resetTimeMonitor(int id) {
// Debug模式不需要计数
if (mTimeMonitorList.get(id) != null) {
mTimeMonitorList.remove(id);
}
getTimeMonitor(id);
}
public TimeMonitor getTimeMonitor(int id) {
TimeMonitor monitor = mTimeMonitorList.get(id);
if (monitor == null) {
monitor = new TimeMonitor(id);
mTimeMonitorList.put(id, monitor);
}
return monitor;
}
}
public class RvTest extends AppCompatActivity {
RecyclerView mRvTest;
Button mBtSet;
Button mBtAdd;
Button mBtClear;
TestAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TimeMonitorManager.getInstance().resetTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_RV_TEST);
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_RV_TEST).startMoniter();
setContentView(R.layout.activity_rv_test);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
mAdapter = new TestAdapter();
mRvTest = (RecyclerView) findViewById(R.id.rvTest);
mBtSet = (Button) findViewById(R.id.btSetData);
mBtAdd = (Button) findViewById(R.id.btAddData);
mBtClear = (Button) findViewById(R.id.btClearData);
mBtSet.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mAdapter.set(getData());
}
});
mBtAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mAdapter.add(getData());
}
});
mBtClear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mAdapter.notifyData(getData2());
}
});
mRvTest.setAdapter(mAdapter);
mRvTest.setLayoutManager(new MyLinearLayoutManager(this));
}
@Override
protected void onResume() {
super.onResume();
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_RV_TEST).recodingTimeTag("onResume:");
}
@Override
protected void onDestroy() {
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_RV_TEST).end(false);
super.onDestroy();
}
}
合理的刷新机制
减少刷新次数
- 控制刷新频率
- 避免没有必要的刷新 首先需要判断是否需要刷新,比如数据没有变化,需要刷新的控件(View)不在可见区域,就没有必要刷新。如果一个View从不可见到可见,一定要刷新一次。
避免后台线程影响
在列表滚动的时候停止图片加载, example:ImageLoader加载图片
缩小刷新区域
在以下两个场景下可以采用局部刷新的方法来节省更多的资源:
- 自定义VIew中:
自定义View一般采用invalidata方法刷新。如果需要更新的数据只是在某一个区域内改变,在调用invalidata方法更新这个区域的时,也会更新整个视图,这就浪费了不需要更新的区域资源。Android系统提供了两个局部更新数据的方法:
- invalidate(Rect dirty);
- invalidate(int left, int top, int right, int bottom);
- 第二种就是容器中的某个Item发生了变化,只需要更新这个Item即可。比如在ListView中,如果是单条操作,就必须调用Adatper的notifyDataSetChanged()刷新。
Android tools
一开始不明白,后来删掉这个属性之后发现会出现一个提示:
pick preview layout from the "Fragment Layout" context menu
原来tools:layout仅仅是告诉编辑器,Fragment在程序预览的时候该显示成什么样,并不会对apk产生实际作用,是为开发者设计的。
一般来说被xmlns:tools="http://schemas.android.com/tools" 声明的tools作为前缀的属性都不会被编译进去。这个跟上面代码中tools:context是一样的。
内存优化之Android内存管理机制
Java对象的生命周期
-
创建阶段(Created) 为对象分配存储空间 开始构造对象 从父类到子类对static成员进行初始化 父类成员变量按照顺序初始化,递归调用父类的构造方法 子类成员变量按照顺序初始化,子类构造方法调用 一旦对象被创建,并有某个引用指向它,这个对象的状态就切换到了应用阶段(In Use)
-
应用阶段(In Use) 对象至少被一个强引用持有并且对象在作用域内
-
不可见阶段(Invisible) 程序本身不再持有该对象的任何强引用,但是这些引用可能还存在着; 一般具体是指程序的执行已经超过该对象的作用域了
-
不可达阶段(Unreachable) 该对象不再被任何强引用所持有; 可能仍被JVM等系统下的某些已经装载的惊天变灵或者线程或JNI所持有,这些特殊的强引用被称为GC root,这种情况容易导致内存泄露,无法被回收
-
收集阶段(Collected) 对象不可达,并且GC已经准备好对该对象占用的内存空间重新分配的时候,处于手机阶段。 如果重写了finazlie()方法,则会去执行该方法。
尽量不要重写finazlie()方法,因为有可能影响JVM的对象分配与回收速度或者可能造成该对象的再次复活
-
终结阶段 当对象执行完finalize()方法之后,仍然处于不可达状态时,则该对象进入终结阶段。在这个阶段,内存空间等待GC进行回收
-
对象空间的重新分配 GC对该对象占有的内存空间进行回收或者再分配,该对象彻底消失
注意:
1.软引用可以加速虚拟机对垃圾内存的回收速度,更可靠地维护系统的运行安全,防止内存益出(Out Of Memory)等问题产生。 2.创建对象以后,在确定不需要使用该对象时,使对象置空,这样更符合垃圾回收标准,比如Object = null,可以提高内存使用效率,并且不要采用过深的继承层次。访问本地变量优于访问类中的变量。
内存分配
内存回收机制
GC类型
内存分配工具
Memory Monitor
Heap Viewer
Allocation Tracker
常见内存泄漏场景
资源性对象未关闭
资源性对象(比如Cursor,File文件等)往往都使用类一些缓冲,在不使用的时候,应该及时关闭它们,以便它们的缓存数据能够及时回收。它们的缓存不只是存在于Java虚拟机内,还存在于Java虚拟机外。如果仅仅是把它们引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如SQLite Curosr(在析构函数finalize()中,如果没有关闭它,它自己会调用close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样效率太低了。所以应该调用colse函数,将其关闭,然后设置为null。
注册对象未反注册(比如广播等)
类的静态变量持有大数据对象
非静态内部类的静态实例
Handler 临时性内存泄漏
Handler通过发送Message与主线程交互,Message发出之后存储在MessageQueue中,有些Message也不是马上就被处理到,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler非静态的,则会导致Activity或者Service不被回收。
- 1.改成静态
- 2.内部弱引用外部的
- 3.外部销毁时Handler调用removeCallbacksAndMessage(null) 注:AsyncTask内部也是Handler机制,所以要注意,但是这种一般都是临时的。
容器中的对象没清理造成的内存泄漏
如果对象的集合是一个static的,情况就严重了。
WebView
内存泄漏检测工具
LeakCanary
实现监控
自定义处理结果
优化内存空间
对象引用
- 强引用
- 软引用
- 弱引用
- 虚引用 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 可能需要运行多次GC,才能找到并释放弱引用对象。 问题:之前遇到函数参数是一个接口的匿名实现,在函数内部采用弱引用,几乎每次弱引用取出来的值都为null,终于找到原因了,要用也该用软引用。
减少不必要的内存开销
AutoBoxing
内存复用
使用最优的数据类型
- 1.HashMap与ArrayMap
HashMap是一个散列表,想HashMap中put元素时,先根据key的HashCode重新计算hash值,根据hash值得到这个元素在数组中的位置,如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置。HashMap会配置一个大的数组来减少潜在的冲突,并且会有其他的逻辑防止链接算法和一些冲突的发生。
ArrayMap
总结:以下情况优先考虑ArrayMap:
- 当对象的数目非常小(1000以内),但是访问特别多或者删除和插入频率不高时。
- 当有映射容器,有映射发生,并且所有映射的容器也是ArrayMap时。?????
- 2.枚举类型 Android系统启动应用后,会在应用单独分配一块内存,应用的dex code, Heap 以及运行时的内存都会在此分配。枚举类型的dex size是普通常量定义的13倍以上(dex code增加);同时运行时,一个enum值的声明会消耗至少20byte,这还没有算上其中的对象数组需要保持对enum值的引用。每个枚举项都会被声明成一个静态变量,并被赋值。
枚举的最大优点就是类型安全,但在Android平台上,枚举的内存开销是直接定义常量的3倍以上,所以尽量不使用枚举,目前提供了int型和String型两种注解方式:IntDef和StringDef,
public static final int UI_PERF_LEVEL_0 = 0;
public static final int UI_PERF_LEVEL_1 = 1;
@IntDef({UI_PERF_LEVEL_0, UI_PERF_LEVEL_1})
@Retention(RetentionPolicy.SOURCE)
public @interface PER_LEVEL {
}
public static int getLevel(@PER_LEVEL int level) {
...
}
使用IntDef和StringDef需要在Gradle配置中引入:compilecom.android.support:support-annotations:22.0.0
- 3.LruCache
图片内存优化(未完成)
注:图片加载方案 #70
提升动画性能
帧动画
补间动画
属性动画
硬件加速
卡顿监控方案与实现
存储优化
存储方式
- SharedPreference 一个轻量级数据存储类,使用于保存软件配置参数。使用SharedPreferences保存参数,最终是使用xml格式文件存放数据,文件存放在/data/data/<pagename_name>/shared_prefs目录下。 (1)特点 用Map数据结构来存储,以键值(key-value)的方式存储。 (2) 存储路径 (3) 创建模式 Activity.MODE_APPEND:如果文件存在,在末尾增加; Activity.MODE_PRIVATE:默认模式,只能由创建该文件的应用程序调用,即为私有的; Activity.MODE_WORLD_PEADABLE:所有读取和创建 Activity.MODE_WORLD_WRITEABLE:所有写入,访问和创建的文件权限;
- SQLite
- File
- ContentProvider
序列化
- Serializable 在序列化的时候会产生大量的临时变量,在序列化的过程中会消耗更多的内存, 从而引起频繁的GC。
- Parcelable 不能使用在要将数据存储在磁盘上的情况。Parcel本质为了更好的实现对象在IPC之间的传递,并不是一个通用的序列化机制,改变Parcel中任何数据的底层实现都可能导致之前的数据不可读取。
- Gson
- fastJason
- Nano Proto Buffers
- FlatBuffers
SharedPreferences优化
SharedPreferences实际上是对一个XML文件存储key-value键值对,每次的commit和apply操作都是一次I/O写操作。大家都知道I/O操作是最慢的操作之一,在主线程操作会导致主线程慢。SharedPreferences性能优化的主要两个方面: 1.IO性能 2.同步锁问题
A. 当SharedPreferences文件还没有被加载到内存的时候,调用getSharedPreferences方法会初始化文件并读入内存,这容易导致耗时增加。 B.Editor的commit或者apply方法每次执行时,同步写入磁盘耗时较长。
需要说明的是,editor的commit和apply方法的区别在于同步写入和异步写入,以及是否需要返回值。在不需要返回值的情况下,使用apply方法可以极大提高性能。
另一个方面就是同步锁的问题,SharedPreferences类中的commitToMemory()方法会锁定SharedPreferences对象,Put()和getEditor()方法会锁定Editor对象,在写入磁盘是更会锁定一个写入锁。 因此最好的优化方法就是避免频繁地读写SharedPreferences,减少无谓的调用,如下,为代码在同一生命周期内,SharedPreferences读一次即可。 if(开关未读 & 没有发生过变化) { int state = sp.getUserId(); } 而对于批量操作,最好先获取一个editor,进行批量操作,然后调用apply方法。
注:跨进程读写SharedPreferences需要用到ContentProvider方案支持,对所有SP操作套上了ContentProvider进行访问,耗时增加3倍左右,因此尽量避免跨进程进程操作。
数据库使用及优化
1.SQLiteStatement
SQLiteStatement只能插入具体的一个表中的数据,在插入之前记得先清除上一次的数据;
2.使用事务
3.使用索引
4.异步线程,写数据库统一管理
双缓冲机制:常用的放内存。
5.提高查询性能
查询数据量的大小
查询数据的列数(查询需要的,需要避免不需要的数据)
排序的复杂度
代码静态扫描工具
1.checkstyle 2.FindBugs 3.PMD 4.Android Lintn
Crash 监控
Java层Crash监控
Native层监控
Crash上报机制
下次正常后上传