《阿里巴巴 Android 开发手册》(四)
相关文章链接
- 《阿里巴巴 Android 开发手册》(一)
- 《阿里巴巴 Android 开发手册》(二)
- 《阿里巴巴 Android 开发手册》(三)
- 《阿里巴巴 Android 开发手册》(四)
- 《阿里巴巴 Android 开发手册》(五)
- 《阿里巴巴 Android 开发手册》(六)
进程、线程与信息通信
1、【强制】不要通过 Intent 在 Android 基础组件之间传递大数据(binder transaction 缓存为 1MB),可能导致 OOM。
2、【强制】在 Application 的业务初始化代码中加入进程判断,确保只在自己需要的进程初始化。特别是后台进程减少不必要的业务初始化。
正例:
public class MyApplication extends Application {
@Override
public void onCreate() { //在所有进程中初始化 .... //仅在主进程中初始化
if (mainProcess) {
...
}
//仅在后台进程中初始化
if (bgProcess) {
...
}
}
}
3、【强制】新建线程时,必须通过线程池提供(AsyncTask 或者 ThreadPoolExecutor 或者其他形式自定义的线程池),不允许在应用中自行显式创建线程。
说明:
使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过渡切换”的问题。另外创建匿名线程不便于后续的资源使用分析,对性能分析造成困扰。
正例:
int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
int KEEP_ALIVE_TIME = 1;
TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>();
ExecutorService executorService = new ThreadPoolExecutor(NUMBER_OF_CORES, NUMBER_OF_CORES * 2, KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, taskQueue, new BackgroundThreadFactory(), new DefaultRejectedExecutionHandler());
//执行任务
executorService.execute(new Runnnable() {
...
});
反例:
new Thread(new Runnable() {
@Override
public void run() { //操作语句
...
}
}).start();
扩展参考:
https://blog.mindorks.com/threadpoolexecutor-in-android-8e9d22330ee3
4、【强制】线程池不允许使用 Executors 去创建,而通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险,
说明:
Executors 返回的线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM;
- CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
正例:
int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
int KEEP_ALIVE_TIME = 1;
TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>();
ExecutorService executorService = new ThreadPoolExecutor(NUMBER_OF_CORES, NUMBER_OF_CORES * 2, KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, taskQueue, new BackgroundThreadFactory(), new DefaultRejectedExecutionHandler());
反例:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
扩展参考:
http://dev.bizo.com/2014/06/cached-thread-pool-considered-harmlful.html
5、【强制】子线程中不能更新界面,更新界面必须在主线程中进行,网络操作不能在主线程中调用。
我怎么记得 SurfaceView 是通过子线程更新 UI 的呢???
6、【强制】不要在非 UI 线程中初始化 ViewStub,否则会返回 null。
7、【推荐】尽量减少不同 APP 之间的进程间通信及拉起行为。拉起导致占用系统资源,影响用户体验。
如果不是需求让拉起其他 APP,我自己绝对不会这么做!!!
8、【推荐】新建线程时,定义能识别自己业务的线程名称,便于性能优化和问题排查。
正例:
public class MyThread extends Thread {
public MyThread() {
super.setName("ThreadName");
...
}
}
9、【推荐】ThreadPoolExecutor 设置线程存活时间(setKeepAliveTime),确保空闲时线程能被释放。
10、【推荐】禁止在多进程之间用 SharedPreferences 共享数据,虽然可以(MODE_MULTI_PROCESS),但官方已不推荐。
11、【推荐】谨慎使用 Android 的多进程,多进程虽然能够降低主进程的内存压力,但会遇到如下问题:
- 不能实现完全退出所有 Activity 功能;
- 首次进入新启动进程的页面时会有延时的现象(有可能黑屏、白屏几秒,是白屏还是黑屏和新 Activity 的主题有关);
- 应用内多进程时,Application 实例化多次,需要考虑各个模块是否都需要在所有进程中初始化;
- 多进程间通过 SharedPreferences 共享数据时不稳定。
文件与数据库
1、【强制】任何时候不要硬编码文件路径,请使用 Android 文件系统 API 访问。
说明:
Android 应用提供内部和外部存储,分别用于存放应用自身数据以及应用产生的用户数据。可以通过相关 API 接口获取对应的目录,进行文件操作。
android.os.Environment#getExternalStorageDirectory()
android.os.Environment#getExternalStoragePublicDirectory()
android.content.Context#getFilesDir()
android.content.Context#getCacheDir
正例:
public File getDir(String alName) {
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), alName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
反例:
public File getDir(String alName) {
// 任何时候都不要硬编码文件路径,这不仅存在安全隐患,也让 app 更容易出现适配问题 File file = new File("/mnt/sdcard/Download/Album", alName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
扩展参考:
- https://developer.android.com/training/data-storage/files.html
- https://developer.android.com/reference/android/os/Environment.html#getExternalStorageDirectory()
2、【强制】当使用外部存储时,必须检查外部存储的可用性。
正例:
// 读/写检查
public boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
// 只读检查
public boolean isExternalStorageReadable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}
3、【强制】应用间共享文件时,不要通过放宽文件系统权限的方式去实现,而应使用 FileProvider。
正例:
<!-- AndroidManifest.xml -->
<manifest>
...
<application>
...
<provider
android:authorities="com.example.fileprovider"
android:exported="false"
android:grantUriPermissions="true"
android:name="android.support.v4.content.FileProvider">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
...
</application>
</manifest>
<!-- res/xml/provider_paths.xml -->
<paths>
<files-path
path="album/"
name="myimages"/>
</paths>
void getAlbumImage(String imagePath) {
File image = new File(imagePath);
Intent getAlbumImageIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Uri imageUri = FileProvider.getUriForFile(
this, "com.example.provider", image);
getAlbumImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(takePhotoIntent, REQUEST_GET_ALBUMIMAGE);
}
反例:
void getAlbumImage(String imagePath) {
File image = new File(imagePath);
Intent getAlbumImageIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); //不要使用 file://的 URI 分享文件给别的应用,包括但不限于 Intent
getAlbumImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(image));
startActivityForResult(takePhotoIntent, REQUEST_GET_ALBUMIMAGE);
}
4、【推荐】SharedPreference 中只能存储简单数据类型(int、boolean、String)等,复杂数据类型建议使用文件、数据库等其他方式存储。
正例:
public void updateSettings() {
SharedPreferences mySharedPreferences = getSharedPreferences("settings", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = mySharedPreferences.edit();
editor.putString("id", "foo");
editor.putString("nick", "bar");
//不要把复杂数据类型转成 String 存储
editor.apply();
}
5、【推荐】SharedPreference 提交数据时,尽量使用 Editor#apply(),而非 Editor#commit()。一般来讲,仅当需要确定提交结果,并据此有后续操作时,才使用 Editor#commit()。
说明:
SharedPreference 相关修改使用 apply 方法进行提交会先写入内存,然后异步写入磁盘,commit 方法是直接写入磁盘。如果操作频繁的话 apply 的性能会优于 commit,apply 会将最后修改内容写入磁盘。但是如果希望立刻获取存储操作的结果,并据此做相应的其他操作,应当使用 commit。
正例:
public void updateSettingsAsync() {
SharedPreferences mySharedPreferences = getSharedPreferences("settings", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = mySharedPreferences.edit();
editor.putString("id", "foo");
editor.apply();
}
public void updateSettings() {
SharedPreferences mySharedPreferences = getSharedPreferences("settings", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = mySharedPreferences.edit();
editor.putString("id", "foo");
if (!editor.commit()) {
Log.e(LOG_TAG, "Failed to commit setting changes");
}
}
反例:
editor.putLong("key_name", "long value");
editor.commit();
扩展参考:
https://developer.android.com/reference/android/content/SharedPreferences.Editor.html#apply()
6、【强制】数据库 Cursor 必须确保使用后关闭,以避免内存泄漏。
说明:
Cursor 是对数据库查询结果集管理的一个类,当查询的结果集较小时,消耗内存不易察觉。但是当结果集较大,长时间重复操作会导致内存消耗过大,需要开发者在操作完成后手动关闭 Cursor。
数据库 Cursor 在创建及使用时,可能会发生各种异常,无论程序是否正常结束,必须在最后确保 Cursor 正确关闭,以避免内存泄漏。同时,如果 Cursor 的使用还牵扯多线程场景,那么需要自行保证让操作同步。
正解:
public void handlePhotos(SQLiteDatabase db, String userId) {
Cursor cursor;
try {
cursor = db.query(TUserPhoto, new String[]{"userId", "content"}, "userId=?", new String[]{userId}, null, null, null);
while (cursor.moveToNext()) { // TODO
}
} catch (Exception e) { // TODO
} finally {
if (cursor != null) {
cursor.close();
}
}
}
反例:
public void handlePhotos(SQLiteDatabase db, String userId) {
Cursor cursor = db.query(TUserPhoto, new String[]{"userId", "content"}, "userId=?", new String[]{userId}, null, null, null);
while (cursor.moveToNext()) { // TODO
}
// 不能放任 cursor 不关闭 }
}
7、【强制】多线程操作写入数据库时,需要使用事务,以避免出现同步问题。
说明:
Android 的通过 SQLiteOpenHelper 获取数据库 SQLiteDatabase 实例,Helper 中会自动缓存已经打开的 SQLiteDatabase 实例,单个 App 中应用使用 SQLiteOpenHelper 的单例模式确保数据库连接唯一。由于 SQLite 自身是数据库级锁,单个数据库操作时保证线程安全的(不能同时写入),transaction 时一次原子操作,因此处于事务中的操作时线程安全的。
若同时打开多个数据库连接,并通过多线程写入数据库,会导致数据库异常,提示数据库已被锁住。
正例:
public void insertUserPhoto(SQLiteDatabase db, String userId, String content) {
ContentValues cv = new ContentValues();
cv.put("userId", userId);
cv.put("content", content);
db.beginTransaction();
try {
db.insert(TUserPhoto, null, cv); // 其他操作
db.setTransactionSuccessful();
} catch (Exception e) {
// TODO
} finally {
db.endTransaction();
}
}
反例:
public void insertUserPhoto(SQLiteDatabase db, String userId, String content) {
ContentValues cv = new ContentValues();
cv.put("userId", userId);
cv.put("content", content);
db.insert(TUserPhoto, null, cv);
}
扩展参考:
- https://nfrolov.wordpress.com/2014/08/16/android-sqlitedatabase-locking-and-multi-threading/
- https://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#beginTransaction())
- https://www.androiddesignpatterns.com/2012/05/correctly-managing-your-sqlite-database.html
- https://www.jianshu.com/p/57eb08fe071d
8、【推荐】大数据写入数据库时,请使用事务或其他能够提高 I/O 效率的机制,保证执行速度。
正例:
public void insertBulk(SQLiteDatabase db, ArrayList<UserInfo> users) {
db.beginTransaction();
try {
for (int i = 0; i < users.size; i++) {
ContentValues cv = new ContentValues();
cv.put("userId", users[i].userId);
cv.put("content", users[i].content);
db.insert(TUserPhoto, null, cv);
}
// 其他操作
db.setTransactionSuccessful();
} catch (Exception e) {
// TODO
} finally {
db.endTransaction();
}
}
9、【强制】执行 SQL 语句时,应使用 SQLiteDatabase#insert()、update()、delete(),不要使用 SQLiteDatabase#execSQL(),以避免 SQL 注入风险。
正例:
public int updateUserPhoto(SQLiteDatabase db, String userId, String content) {
ContentValues cv = new ContentValues();
cv.put("content", content);
String[] args = {String.valueOf(userId)};
return db.update(TUserPhoto, cv, "userId=?", args);
}
反例:
public void updateUserPhoto(SQLiteDatabase db, String userId, String content) {
String sqlStmt = String.format("UPDATE %s SET content=%s WHERE userId=%s", TUserPhoto, userId, content);
//请提高安全意识,不要直接执行字符串作为 SQL 语句
db.execSQL(sqlStmt);
}
10、【强制】如果 ContentProvider 管理的数据存储在 SQL 数据库中,应该避免将不受信任的外部数据直接拼接在原始 SQL 语句中,可使用一个用于将 ?作为可替换参数的选择子句以及一个单独的选择参数数组,会避免 SQL 注入。
正例:
String mSelectionClause = "var = ?";
String[] selectionArgs = {""};
selectionArgs[0] = mUserInput;
反例:
// 拼接用户输入内容和列名
String mSelectionClause = "var = " + mUserInput;