前言
如果应用耗电量过大,就会消耗用户手机过多的电量,甚至发热,现在一般手机都会有耗电量过大的提醒,当用户看到后,也可能会把你的应用卸载掉,因此需要对其进行优化,让应用的耗电量尽可能小。
检测工具
Battery Historian:https://github.com/google/battery-historian
如果应用耗电量过大,就会消耗用户手机过多的电量,甚至发热,现在一般手机都会有耗电量过大的提醒,当用户看到后,也可能会把你的应用卸载掉,因此需要对其进行优化,让应用的耗电量尽可能小。
Battery Historian:https://github.com/google/battery-historian
Android中使用SQLite做为数据库,当数据量很大的情况下,就可能会出现性能问题,比如查询速度过慢,更新速度过慢等,因此需要对其进行优化。
使用事务
每次事务都会修改rollback journal文件,因此比较耗时,对于批量操作,应该合并到同一个事务中。
建立索引
对查询多,更新少的字段建立索引,但索引会占用空间,而且会降低更新速度。
使用SQLiteStatement
对于循环执行数据库操作,应该在循环外预编译好sql语句得到SQLiteStatement对象,然后循环内bindXXX,而不是直接在循环内调用execSQL方法,这样会导致创建大量的SQLiteStatement临时对象,而且每次都会重新编译sql语句。
缓存getColumnIndex返回结果
对于循环执行数据库操作,getColumnIndex方法应该放到循环前调用,而不是每次循环都调用一次,虽然该方法的本质是从Map中查找index,但对性能还是有一定的影响的,放在循环前调用会稍微快一些。
Cursor优化
正如微信的WCDB所说,在合适的场景下,直接把Cursor的共享内存缓存去掉,会提高数据查询的速度。
分表、分库
SQLite的同步锁是表级别的,因此为了避免等待,可以按照具体场景拆分为多个表,或者分库。
SQL语句拼接
不要用+进行连接,用StringBuilder,有循环的最后放到循环外部,不要每次循环都去生成sql语句字符串。
SQL语句优化
适用所有数据库优化,不仅仅只是SQLite,见下部分。
APK包过大,可能会导致用户不想下载,或者消耗用户更多的流量,需要更多的下载等待时间,因此需要对APK进行瘦身,使得APK包尽量精简。
so优化
so裁剪:避免为了使用一个小功能引入一个大的so,删除用不到的so代码和依赖。
只保留armeabi或者armeabi-v7a下的so文件:armeabi属于比较老的CPU架构,不支持硬件浮点运算,所以速度较慢,但兼容设备更多,而现在大部分首页都是支持armeabi-v7a架构的。
分渠道打包so:不同渠道的用户手机CPU架构不同,为了避免同时加入多种so,可以对不同渠道放入对应的so。
依赖库
这里主要指第三方的jar,aar等依赖库,引入的库应该尽可能精简。
res优化
只保留一套图片: 对于用到的图片,一般放到xxhdpi目录就可以,具体放哪里,要看目标用户手机主要的分辨率是多少。
动态加载:不是很重要的图,或者非首屏显示的图,改为动态加载。
图片压缩:tinypng,用webp代替png。
无用资源:用AS删除无用资源。
矢量图:用矢量图代替真实图片,矢量图代码占用空间小,而且不会失真。
多语言:对国内用户只打中文包,resConfigs指定为zh-rCN。
使用AndResGuard开源库:微信出的工具,用于压缩资源路径,同时也起到混淆资源文件名的作用。
精简字体文件:字体文件只包含需要用到的文字、数字或者符合等,而不是直接引入整个字体文件。
assets优化
跟res优化差不多,删除无用资源,压缩资源,动态下载。
代码优化
打开minifyEnabled:代码压缩,编译时会自动移除没有被使用到的代码,可以通过keep来强制指定不移除某些代码。
打开shrinkResources:资源压缩,必须在minifyEnabled为true的前提下使用,编译时会自动移除没有被使用到的资源,也可以自定义指定哪些资源不移除。
proguard代码混淆:这个正式包都有,代码混淆能够减少类名、方法名等长度,移除没有被使用到的代码。
注意minifyEnabled和proguard的区别:minifyEnabled只能移除无用代码,速度较快;而proguard除了能移除无用代码,还可以进行名称混淆,优化代码,速度较慢。参考文章:https://stackoverflow.com/questions/37007485/whats-the-difference-between-minifyenabled-and-useproguard-in-the-android-p
代码动态化:组件化,代码动态下载。
单个枚举大概占用1~1.4KB,用常量代替枚举。
我们知道,Cursor使用结束后,如果没有调用Cursor的close方法,就可能会导致内存泄露,但其原因是什么呢?究竟是什么对象泄露了?下面通过源码分析其原因。
SQLiteDatabase执行查询操作很简单,直接调用query方法:
CustomSQLiteDatabase helper = new CustomSQLiteDatabase(this);
SQLiteDatabase db = helper.getWritableDatabase();
Cursor cursor = db.query("COMPANY", null, null, null, null, null, null);
内部会创建一个SQLiteDirectCursorDriver对象,然后调用其query方法:
public Cursor query(CursorFactory factory, String[] selectionArgs) {
final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal);
final Cursor cursor;
try {
// 建立index->列名映射,最终放在Object数组中
query.bindAllArgsAsStrings(selectionArgs);
if (factory == null) {
cursor = new SQLiteCursor(this, mEditTable, query);
} else {
cursor = factory.newCursor(mDatabase, this, mEditTable, query);
}
} catch (RuntimeException ex) {
query.close();
throw ex;
}
mQuery = query;
return cursor;
}
一般我们不会指定CursorFactory,所以factory为null,这种情况下创建的是一个SQLiteCursor对象,也就是外部的query方法返回的其实就是这个SQLiteCursor对象。
另外注意到这里调用query方法时,只是做了一些初始化操作,简单返回一个SQLiteCursor对象而且,并不会真的去执行数据库查询,因此这个query执行起来不耗时。
接着我们拿到这个SQLiteCursor对象后,调用moveToPosition(0)方法把游标移动到第一条记录,这个操作就比较耗时了,SQLiteCursor继承自AbstractWindowedCursor,看一下AbstractWindowedCursor的moveToPosition方法:
@Override
public final boolean moveToPosition(int position) {
// Make sure position isn't past the end of the cursor
final int count = getCount();
if (position >= count) {
mPos = count;
return false;
}
// Make sure position isn't before the beginning of the cursor
if (position < 0) {
mPos = -1;
return false;
}
// Check for no-op moves, and skip the rest of the work for them
if (position == mPos) {
return true;
}
boolean result = onMove(mPos, position);
if (result == false) {
mPos = -1;
} else {
mPos = position;
}
return result;
}
这里的getCount方法和onMove方法,都可能会触发执行fillWindow方法,而fillWindow方法就是真正去查询数据库,然后把数据填充到共享内存中。对于这块共享内存,默认大小为2MB,只有当真正查询数据库时才会被创建,然后填充查询结果数据,假如结果数据过大填充不下,就只填充一部分,如果下次读取到填充之外的数据,就需要重新执行数据库查询,清空原有共享内存缓存数据,然后把新数据填充到共享内存中。
继续看fillWindow方法:
private void fillWindow(int requiredPos) {
clearOrCreateWindow(getDatabase().getPath());
try {
Preconditions.checkArgumentNonnegative(requiredPos,
"requiredPos cannot be negative, but was " + requiredPos);
if (mCount == NO_COUNT) {
mCount = mQuery.fillWindow(mWindow, requiredPos, requiredPos, true);
mCursorWindowCapacity = mWindow.getNumRows();
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "received count(*) from native_fill_window: " + mCount);
}
} else {
int startPos = mFillWindowForwardOnly ? requiredPos : DatabaseUtils
.cursorPickFillWindowStartPosition(requiredPos, mCursorWindowCapacity);
mQuery.fillWindow(mWindow, startPos, requiredPos, false);
}
} catch (RuntimeException ex) {
// Close the cursor window if the query failed and therefore will
// not produce any results. This helps to avoid accidentally leaking
// the cursor window if the client does not correctly handle exceptions
// and fails to close the cursor.
closeWindow();
throw ex;
}
}
一开始就调用clearOrCreateWindow方法,如果共享内存已经存在就清空,没有就创建,这里其实就是创建一个CursorWindow对象,当然它只是一个壳而已,最终对共享内存的操作都是通过native层实现的。
共享内存准备好之后,就可以调用mQuery的fillWindow方法查询数据库填充数据了,mQuery的类型为SQLiteQuery,fillWindow方法会连接到数据库查询数据,然后填充到共享内存中。
LeakCanary是square推出的用于检测Android内存泄漏的开源工具,使用起来十分简单,接入成本低,项目地址:https://github.com/square/leakcanary。
// module的build.gradle添加依赖
implementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
LeakCanary.install(this);
}
Android中内存优化是一个比较重要的话题,因为内存会直接影响到程序的运行性能,在开发中经常会出现一些内存相关的问题,包括内存泄露,内存抖动,占用过多内存等,因此优化的目的,就是让减少这几类问题的出现,或者彻底解决,让程序运行起来更加流畅,性能更高效。
LeakCanary
用于检测内存泄露,如果发生内存泄露,会给出泄露对象的引用链,其内部是通过WeakReferences的回收队列实现内存泄露检测的。
Android Studio的Profiler工具
可查看对象占用内存情况,只能查看debuggable应用。
adb命令
执行命令adb shell dumpsys meminfo,可以查看所有应用的内存信息。常用的参数有–package,用于指定要查看的包名的所有进程的内存信息,比如我们要查看微信的内存信息,执行下面的命令:
adb shell dumpsys meminfo --package com.tencent.mm
这里就会微信所有进程的内存信息,如果我们只需要看主进程的信息,可以直接通过pid指定,所以先执行命令ps获取到pid:
adb shell ps -A | grep com.tencent.mm
打印结果如下:
u0_a170 3525 633 2068856 21644 0 0 S com.tencent.mm:appbrand1
u0_a170 19179 633 2577364 112404 0 0 S com.tencent.mm
u0_a170 19404 633 2240664 11740 0 0 S com.tencent.mm:appbrand0
u0_a170 26971 633 1965968 5312 0 0 S com.tencent.mm:exdevice
u0_a170 28292 633 2003436 5296 0 0 S com.tencent.mm:push
看到主进程的pid为19179,然后执行命令:
adb shell dumpsys meminfo 19179
打印结果如下:
Applications Memory Usage (in Kilobytes):
Uptime: 138531332 Realtime: 241333585
** MEMINFO in pid 19179 [com.tencent.mm] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 40613 40564 0 72101 164480 132995 31484
Dalvik Heap 19065 19016 0 8873 52069 27493 24576
Dalvik Other 11200 11196 0 893
Stack 100 100 0 16
Ashmem 4 4 0 0
Gfx dev 6432 6432 0 0
Other dev 28 0 28 0
.so mmap 6138 440 4328 3370
.jar mmap 28 28 0 336
.apk mmap 1157 88 672 160
.ttf mmap 60 0 0 0
.dex mmap 22720 0 19372 28
.oat mmap 2520 0 4 0
.art mmap 10700 9644 260 3067
Other mmap 336 0 324 4
EGL mtrack 9248 9248 0 0
GL mtrack 5436 5436 0 0
Unknown 2666 2664 0 4830
TOTAL 232129 104860 24988 93678 216549 160488 56060
App Summary
Pss(KB)
------
Java Heap: 28920
Native Heap: 40564
Code: 24932
Stack: 100
Graphics: 21116
Private Other: 14216
System: 102281
TOTAL: 232129 TOTAL SWAP PSS: 93678
Objects
Views: 1388 ViewRootImpl: 1
AppContexts: 7 Activities: 1
Assets: 7 AssetManagers: 0
Local Binders: 157 Proxy Binders: 98
Parcel memory: 82 Parcel count: 327
Death Recipients: 8 OpenSSL Sockets: 0
WebViews: 0
SQL
MEMORY_USED: 528
PAGECACHE_OVERFLOW: 64 MALLOC_SIZE: 117
DATABASES
pgsz dbsz Lookaside(b) cache Dbname
4 436 97 25/40/10 /data/user/0/com.tencent.mm/databases/beacon_tbs_db
4 108 109 27/42/16 /data/user/0/com.tencent.mm/databases/google_app_measurement.db
4 36 99 4/34/6 /data/user/0/com.tencent.mm/databases/tes_db
Asset Allocations
: 132K
对于第一部分,一般只需要关注前两列就行,Pss Total表示该进程占用内存大小,包括了按比例计算进程间共享内存,而Private Dirty就只是该进程占用内存大小。然后对应到具体的行,Native Heap为本地堆占用内存大小,Dalvik Heap为Java堆占用内存大小,Dalvik Other为JIT和垃圾回收记录占用内存大小,.so mmap为映射的so代码占用的内存大小,.dex mmap为映射的dex代码占用的内存大小。
对于第三部分,表示在当前进程中,某些重要类型的实例对象数量,比如ViewRootImpl就是窗口数量,AppContexts就是Context数量,而Activities为Activity的数量。