将从以下几个方面总结Android的应用性能优化
性能
- 框架API
- UI 性能
- I/O性能
- 屏幕滚动性能
内存
- Android 如何管理内存
- OOM终结 & 低内存终结
- 应用内存使用监测
- 识别内存泄露
- 最佳实践
糟糕的用户体验
- Activity 启动时间过长
- 应用无反应(ANR)
- 帧速率差
关于帧
帧速率
- 为了保证能达到60fps,最多只有16ms去处理每一帧
- 而保证能达到24fps,最多只有41ms去处理每一帧
常见操作耗时
- Binder RPC 调用大约花费 0.12ms
- 从闪存读取一个字节大约花费 0.0x ~ 5ms(一个文件只读一个字节有可能大于1ms)
- 写内容到闪存大约花费 1-100ms(一个文件只写一个字节有可能小于1ms)
- TCP 初始化加上HTTP提取通常花费秒级的时间(此处指的是建立链接并获取链接返回的数据的时间)
读写操作在性能差一些的机子上可能时间会有出入
往磁盘写内容的时候,会随着磁盘的剩余空间的较少而导致写速率不断减低
永远不要做阻塞UI线程的事情,用一个新的线程去做可能会影响UI体验的事情
四种可以异步的实现:
- Runnable
- Thread
- Future
- ExecutorService
- 使用Thread
new Thread(new Runnable() {
@Override
public void run() {
// do some heavy work
}
}).start();
- 使用内置AsyncTask
new AsyncTask<URL, Integer, Integer>() {
protected Long doInBackground(URL... urls) {
final int count = urls.length;
for ( int i = 0; i < count; i++ ) {
Downloader.download(url);
publishProgress(i);
}
return count;
}
protected void onProgressUpdate(Integer... progress) {
setProgress(progress[0]);
}
protected void onPostExecute(Integer result) {
showDialog(“Downloaded “ + result + “ files”);
}
}
- 使用HandlerThread
HandlerThread mHandlerThread = new HandlerThread("WorkerThread");
Handler handler = new Handler(mHandlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case JOB_1:
// do job #1
break;
case JOB_2:
// do job #2
break;
}
}
};
handler.sendEmptyMessage(JOB_1);
handler.sendEmptyMessage(JOB_2);
handler.post(new Runnable() {
@Override
public void run() {
// do more work
}
});
@Override
protected void onDestroy() {
mHandlerThread.quit();
super.onDestroy();
}
- 使用AsyncQueryHandler
new AsyncQueryHandler(getContentResolver()) {
@Override
protected void onQueryComplete(int token, Object cookie,
Cursor cursor) {
if (token == 0) {
// get data from cursor
}
}
}.startQuery(0, // token
null, // cookie
RawContacts.CONTENT_URI, null, // projection
RawContacts.CONTACT_ID + "<?", // selection
new String[] { "888" }, // selectionArgs
RawContacts.DISPLAY_NAME_PRIMARY + " ASC" // orderby
);
- 使用IntentService
public class WorkerService extends IntentService {
public WorkerService() {
super("WorkerThread");
}
@Override
protected void onHandleIntent(Intent intent) {
String action = intent.getAction();
if ("com.test.DO_JOB_1".equals(action)) {
// do job #1
}
}
}
startService(new Intent("com.test.DO_JOB_1"));
UI线程性能总结
- Activity or Fragment
- AsyncTask
- Handler,HandlerThread
- AsyncTaskLoader
- ContentProvider
- AsyncQueryHandler
- CursorLoader
- Service
- IntentService
- Parcel.writeStrongBinder(IBinder binder)
View Hierarchy
- Measure
- Layout
- Draw
- Key Events
- Trackball Events
- Touch Evnets
Tips:
降低布局层次结构的复杂性
使用层次结构查看器来检查是否存在瓶颈
使用RelativeLayout或者GridLayout来简化复杂布局的层次嵌套
使用<merge />标签来较少布局层次
-
使用<ViewStub />标签来延迟该标签下的布局的渲染
<ViewStub android:id="@+id/stub_import" android:inflatedId="@+id/panel_import" android:layout="@layout/progress_overlay" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" />
((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE); // or View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();
使用layoutopt检测常见问题
I/O性能优化
-
异步写SharedPreferences
SharedPreferences.Editor.apply(); // 异步 SharedPreferences.Editor.commit(); // 同步
数据库query查询语句中的 * 替换成具体的列值
使用TraceView配置您的数据库查询
使用LIMIT子句减少选择行
最小化完整窗口时间
使用索引优化数据库查询
预编译常用的SQL语句
String sql = “INSERT INTO table VALUES (?, ?)”;
SQLiteStatement stmt = mDatabase.compileStatement(sql);
DatabaseUtils.bindObjectToProgram(stmt, 1, 1);
DatabaseUtils.bindObjectToProgram(stmt, 2, 2);
stmt.execute();
stmt.close();
//或者使用 PreparaStatement
- 推迟ContentObserver.onChange()中的自动重新检查
getContentResolver().registerContentObserver(uri, true,
new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
mDirty = true;
}
});
@Override
protected void onResume() {
super.onResume();
if (mDirty) {
// start query again
mDirty = false;
}
}
-
在事务中使用批量操作
- ContentProviderOperation!
- ContentProviderOperation.Builder!
- ContentResolver.applyBatch()
-
在一个比较长的事务中允许偶尔的事务提前
SQLiteDatabase.yieldIfContendedSafely()
-
使用事件日志调试
adb logcat -b events content_query_sample:I *:S
adb logcat -b events content_update_sample:I *:S
adb logcat -b events db_sample:I *:S
滑动性能优化(List)
-
ListView : 通过复用view来避免不必要的inflate操作
@Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = mInflater.inflate(R.layout.main, parent, false); } // .... }
-
通过ViewHolder缓存v试图,而避免不必要的findViewByI'd
@Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = mInflater.inflate(R.layout.main, parent, false); ViewHolder holder = new ViewHolder(); holder.img = (ImageView) convertView.findViewById(R.id.image); holder.txt = (TextView) convertView.findViewById(R.id.text); convertView.setTag(holder); } ViewHolder holder = (ViewHolder) convertView.getTag(); holder.img.setImageResource(R.drawable.icon); holder.txt.setText(R.string.hello); return convertView; } private static class ViewHolder { ImageView img; TextView txt; }
-
避免view的不必要绘制(例如背景的重复绘制)
Android 中 会绘制每一个父view即使它被覆盖在一个不透明的子view之下
当你有一个父view并且是永远不可见的,那么不要绘制它(包括他的背景)
-
大多数的情况下你不需要绘制window的背景
//Activity中 getWindow().setBackgroundDrawable(null); //style中 android:windowBackground="@null"
避免在运行时进行图片缩放(特殊业务需求除外)
-
避免在视图(ListView等)滚动的时候进行动画,如果业务要求使用动画,那么请关闭绘制缓存
ListView.setDrawableCacheEnabled(false)
使用Allocation Tracker(内存分配追踪器)检测并避免频繁的垃圾回收
考虑使用Object Pool ,StringBuilder等封装类型
缓存的时候考虑使用SoftReference
-
在调试模式的时候启用StrictMode(可以检查大部分不规范,不安全操作)
public void onCreate() { if (DEVELOPER_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .penaltyDeath() .build()); } super.onCreate(); }
-
检查主线程Looper是否有不必要的活动
Looper.setMessageLogging();
Memory
在系统级别,Android使用低内存驱动程序运行修改过的OOM Killer
包括:
- Linux OOM killer
- OOM_ADJ
- Android Low Memory Killer
Android中的低内存阈值(init.rc中)
# Define the memory thresholds at which the above process classes will
# be killed. These numbers are in pages (4k).
setprop ro.FOREGROUND_APP_MEM 2048
setprop ro.VISIBLE_APP_MEM 3072
setprop ro.PERCEPTIBLE_APP_MEM 4096
setprop ro.HEAVY_WEIGHT_APP_MEM 4096
setprop ro.SECONDARY_SERVER_MEM 6144
setprop ro.BACKUP_APP_MEM 6144
setprop ro.HOME_APP_MEM 6144
setprop ro.HIDDEN_APP_MEM 7168
setprop ro.EMPTY_APP_MEM 8192
OOM_ADJ基于重要性级别(init.rc中)
# Define the oom_adj values for the classes of processes that can be
# killed by the kernel. These are used in ActivityManagerService.
setprop ro.FOREGROUND_APP_ADJ 0
setprop ro.VISIBLE_APP_ADJ 1
setprop ro.PERCEPTIBLE_APP_ADJ 2
setprop ro.HEAVY_WEIGHT_APP_ADJ 3
setprop ro.SECONDARY_SERVER_ADJ 4
setprop ro.BACKUP_APP_ADJ 5
setprop ro.HOME_APP_ADJ 6
setprop ro.HIDDEN_APP_MIN_ADJ 7
setprop ro.EMPTY_APP_ADJ 15
进程重要性级别
- Persistent(持续存在)
- OOM_ADJ < 0
- system_server (-16) , com.android.phone (-12)
- Foreground(前台进程)
- FOREGROUND_APP_ADJ = 0
- 运行前台Activity
- 运行一个Service,执行onCreate(),onStartCommand(),onDestroy()
- 托管由前台Activity或前台进程绑定的Service
- 运行一个BroadcastReceiver,执行onReceive()
- 托管由持续或前台进程使用的ContentProvider
- Visible(可见进程)
- VISIBLE_APP_ADJ = 1
- 运行可见的Activity(不在前台,也就是,不是正在和用户交互的)
- 运行由startService()启动的Service,Service使用startForeground()
使自己处于前台状态
- Service(服务进程)
- SECONDARY_SERVER_ADJ = 4
- 运行由startService()启动Service,并且不是可见进程
- Background(后台进程)
- HIDDEN_APP_MIN_ADJ (7) .. EMPTY_APP_ADJ (15)
- 不包含任何活动应用程序组件的进程
Low Memory 回调
Activity.onLowMemory()
Fragment.onLowMemory()
Activity.onSaveInstanceState(Bundle)
Service.onLowMemory()
ContentProvider.onLowMemory()
在应用程序级别,Android限制了多少内存可以分配给每个应用程序。
Android为每个应用程序定义了一个堆限制,并指示何时抛出OutOfMemoryError
Android Studio 中 Heap窗口中的相关术语
术语 | 解释 |
---|---|
Heap limit | 应用在Dalvik堆中的最大允许占用空间 |
Heap size | 当前Dalvik堆的大小 |
Allocated | 应用在Dalvik堆上分配的字节总数 |
Free | Heap size – Allocated |
% Used | Allocated / Heap size * 100% |
External allocation (3.0之前) | Bitmap byte[] |
ActivityManager.getMemoryClass() 可以查看当前应用Heap size limit
OOM 发生的情形
- 2.3之前
Heap size + external allocation + new allocation request >= Heap limit
- 2.3(包括)之后
Heap size + new allocation request >= Heap limit
new allocation request : 新的内存开辟请求大小
不代表进程内存使用的情形
- 每个进程从zygote fork出来后,都会有2mb以上的开销
- 在使用native的时候会开辟更多的内存:
- Android应用程序运行在Dalvik VM中,同时通过JNI加载本地库
- 由应用程序调用的Dalvik级API可以代表申请人使用本机库。
- 如果你启用了硬件加速(4.0中默认开启),那么会多有8mb的内存去使用OpenGL
查看内存使用情况
- 根据进程内存使用情况排序:
adb shell procrank -p
PID Vss Rss Pss Uss cmdline!
3156 80272K 80220K 59228K 57624K com.htc.launcher
1455 94540K 58728K 37488K 36060K system_server
9000 55224K 55200K 33900K 32412K com.roguso.plurk
6713 47912K 47880K 27719K 26788K tw.anddev.aplurk
1624 44804K 44760K 24954K 24200K android.process.acore
2081 44992K 44960K 23205K 21628K com.htc.android.mail
1604 41288K 41248K 22393K 21752K com.htc.android.htcime
1594 40912K 40844K 21588K 20284K com.htc.weatheridlescreen
1622 39904K 39872K 21297K 20696K com.android.phone
VSS(Virtual Set Size):进程可以访问的页面总数
RSS(Resident Set Size): RAM中进程可以访问的页总数
PSS(Proportion Set Size):进程在RAM中使用的页面总数,其中每个页面的大小是页面总数除以共享它的进程数
USS(Unique Set Size):进程可以访问的非共享页面的数量
-
列出进程的虚拟内存区域
adb shell procmem -p <pid>
Vss Rss Pss Uss ShCl ShDi PrCl PrDi Name
------- ------- ------- ------- ------- ------- ------- -------
4K 4K 0K 0K 4K 0K 0K 0K /system/bin/app_process
4K 4K 0K 0K 4K 0K 0K 0K /system/bin/app_process
13908K 13908K 11571K 11508K 2400K 0K 11508K 0K [heap]
0K 0K 0K 0K 0K 0K 0K 0K [heap]
4K 4K 4K 4K 0K 0K 4K 0K [heap]
36K 36K 0K 0K 0K 36K 0K 0K /dev/__properties__
.......
adb shell dumpsys meminfo <pid>
Applications Memory Usage (kB):
Uptime: 89133197 Realtime: 106110266
** MEMINFO in pid 11961 [com.htc.friendstream] **
native dalvik other total limit bitmap nativeBmp
size: 15032 8535 N/A 23567 32768 N/A N/A
allocated: 14565 5697 N/A 20262 N/A 4669 1918
free: 162 2838 N/A 3000 N/A N/A N/A
(Pss): 4105 2550 13952 20607 N/A N/A N/A
(shared dirty): 2440 1928 5532 9900 N/A N/A N/A
(priv dirty): 4044 708 12716 17468 N/A N/A N/A
Objects
Views: 0 ViewRoots: 0
AppContexts: 0 Activities: 0
Assets: 7 AssetManagers: 7
Local Binders: 11 Proxy Binders: 15
Death Recipients: 1
OpenSSL Sockets: 0!
Private Dirty = USS
无法分页到磁盘并且不与任何其他进程共享的进程内部RAM量
当进程消失时,系统可以使用的RAM
- 一些重要的虚拟内存区域
/dev/ashmem/dalvik-heap : 在Dalvik级别为堆分配的匿名页面
[heap], [anonymous] : 由malloc()在本机级别分配的匿名页面
/system/framework/*.odex (release build)
/data/dalvik-cache/*.odex (debug build) : 文件支持的mmap页面
Garbage collection(垃圾收集)
-
2.3之前的GC
收集垃圾的时候会停止其他所有的工作
对整个堆进行收集
造成的暂停时间一般都大于100ms
-
2.3及其之后
不会暂停其他工作,而是与其他工作同时进行(绝大部分是这样的)
一次垃圾收集只是对堆的一部分而已
造成的暂停时间一般小于5ms
Memory leaks(内存泄露)
- GC并不能避免内存泄露
- 有一个指向长期存在的且未使用的对象的应用,导致这个不被使用的对象不能被回收
- 在Android中,通常发生内存泄露的是对Context或者Activity的引用
常见的因为Context 或着 Activity造成的内存泄露
-
在Activity中存在长期存在的指向非静态内部类实例对象的引用
public class TestActivity extends Activity{ static LeakyTest leaky = null; class LeakyTest{ void doSoming(){ //doing } } @Override protected void onCreate(Bundle saveInstanceStates){ super.onCreate(saveInstanceStates); if(leaky==null) leaky = new LeakyTest(); //.... } //..... }
-
在Activity中有超出Activity生命周期且长期存活的线程
new Thread(new Runnable(){ @Override public void run(){ //do long-live works } }).start();
有用的方法
- 使用logcat检查是否有内存随着时间的推移而不断增加(尤其注意某些方法的执行步骤!)
例如得到的日志信息:
D/dalvikvm(9050):GC_CONCURRENT free 2049k, 65% free 3571k/9991k, external 4703k/5261k, paused 2ms+2ms
D/dalvikvm(9050): <u>GC_CONCURRENT</u> free 2049k, 65% free 3571k/9991k, external 4703k/5261k, paused 2ms+2ms
下划线处GC的原因:
GC_CONCURRENT
GC_FOR_MALLOC
GC_EXTERNAL_ALLOC
GC_HPROF_DUMP_HEAP
GC_EXPLICIT
D/dalvikvm(9050): GC_CONCURRENT <u>free 2049k</u>, 65% free 3571k/9991k, external 4703k/5261k, paused 2ms+2ms
下划线处GC的原因:
内存释放
D/dalvikvm(9050): GC_CONCURRENT free 2049k, <u>65% free 3571k/9991k</u>, external 4703k/5261k, paused 2ms+2ms
下划线处GC的原因:
内存释放
堆进行信息统计
D/dalvikvm(9050): GC_CONCURRENT free 2049k, 65% free 3571k/9991k, <u>external 4703k/5261k,</u> paused 2ms+2ms
下划线处GC的原因:
内存释放
堆进行信息统计
内部内存进行信息统计
D/dalvikvm(9050): GC_CONCURRENT free 2049k, 65% free 3571k/9991k, external 4703k/5261k, <u>paused 2ms+2ms</u>
下划线处GC的原因:
内存释放
堆进行信息统计
内部内存进行信息统计
时间暂停
- 使用分配跟踪器查看是否有随着时间分配未预料的对象(Android Studio中的logcat窗口中有对应的按钮)
- 使用(Histogram view)直方图视图查看活动实例的数量。 有多于一个Activity的一个实例,那么这是一个强烈的Activity / Context泄露的迹象。
- 按保留大小排序的Dominator Tree视图有助于识别保留了大量的内存且不能被释放的对象。 他们通常是找到内存泄漏的好起点。
强烈推荐郭神关于内存泄露分析的文章:
其他优化建议
-
造成OutOfMemoryError的原因通常是Bitmap或者对象进行了太多的内存分配
加载图片的时候尽可能不要加载原尺寸的大图,可以使用缩略图
回收已经不使用的Bitmap资源bitmap.recycle().
2.3(包括)之前,Bitmap的引用是放在堆中的,而Bitmap的数据部分是放在栈中的,需要用户调用recycle方法手动进行内存回收 ,2.3之后,整个Bitmap,包括数据和引用,都放在了堆中,这样,整个Bitmap的回收就全部交给GC了,这个recycle方法就再也不需要使用了。
列表中加载图片注意使用小图,以及做好缓存工作
尽可能的避免碎片化
减少Java在应用堆空间堆快满的时候再堆分配
缓存中使用SoftReference
使用WeakReference避免堆存泄露
//缩放图片
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, opts);
final int originalWidth = opts.outWidth;
final int originalHeight = opts.outHeight;
final int originalDim = Math.max(originalWidth, originalHeight);
opts = new BitmapFactory.Options();
opts.inSampleSize = 1;
while ( originalDim > MAX_IMAGE_DIM ) {
opts.inSampleSize *= 2;
originalDim /= 2;
}
return BitmapFactory.decodeFile(path, opts);
//对比之间的例子,这里改成了静态内部类
public class MainActivity extends Activity {
static Leaky leak = null;
static class Leaky {
void doSomething() {
//doing
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (leak == null) {
leak = new Leaky();
}
}
}
public class MainActivity extends Activity {
static Leaky leak = null;
static class Leaky {
private final Context mContext; //final修饰
public Leaky(Context context) {
super();
mContext = context;
doSomethingWithOuterInstance();
}
void doSomethingWithOuterInstance() {
String text = mContext.getString(R.string.hello);
System.out.println(text);
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (leak == null) {
leak = new Leaky(this);
}
}
}
public class MainActivity extends Activity {
static Leaky leak = null;
static class Leaky {
private final WeakReference<Context> mContext;//使用了弱引用
public Leaky(Context context) {
super();
mContext = new WeakReference<Context>(context);
doSomethingWithOuterInstance();
}
void doSomethingWithOuterInstance() {
Context context = mContext.get();
if (context != null) {
String text = context.getString(R.string.hello);
System.out.println(text);
}
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (leak == null) {
leak = new Leaky(this);
}
}
}
更多优化建议,请移步郭神博客