背景
2018春节余味尚未消,阿里巴巴为移动开发者们准备了一份迟到的新年礼物——《阿里巴巴Android开发手册》1.0.1版本。
在此写下我的阅读笔记,记录下自己平时没有注意的一些问题,规范自己。
正文
1.【强制】Activity 间通过隐式 Intent 的跳转,在发出 Intent 之前必须通过 resolveActivity 检查,避免找不到合适的调用组件,造成 ActivityNotFoundException 的异常。
public void viewUrl(String url, String mimeType) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(url), mimeType);
if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ ONLY) != null) {
startActivity(intent);
} else {
// 找不到指定的 Activity
}
}
2.【强制】避免在 BroadcastReceiver#onReceive()中执行耗时操作,如果有耗时工作, 应该创建 IntentService 完成,而不应该在 BroadcastReceiver 内创建子线程去做。
说明:
由于该方法是在主线程执行,如果执行耗时操作会导致 UI 不流畅。可以使用IntentService 、 创 建 HandlerThread 或 者 调 用 Context#registerReceiver (BroadcastReceiver, IntentFilter, String, Handler)方法等方式,在其他 Wroker 线程执行 onReceive 方法。BroadcastReceiver#onReceive()方法耗时超过 10 秒钟,可能会被系统杀死。
3.【 推 荐 】 添 加 Fragment 时 , 确 保 FragmentTransaction#commit() 在 Activity#onPostResume()或者 FragmentActivity#onResumeFragments()内调用。 不要随意使用 FragmentTransaction#commitAllowingStateLoss()来代替,任何 commitAllowingStateLoss()的使用必须经过 code review,确保无负面影响。
说明:
Activity 可能因为各种原因被销毁,Android 支持页面被销毁前通过Activity#onSaveInstanceState() 保 存 自 己 的 状 态 。 但 如 果FragmentTransaction.commit()发生在 Activity 状态保存之后,就会导致 Activity 重 建、恢复状态时无法还原页面状态,从而可能出错。为了避免给用户造成不好的体验,系统会抛出 IllegalStateExceptionStateLoss 异常。推荐的做法是在 Activity 的onPostResume() 或 onResumeFragments() ( 对 FragmentActivity ) 里 执 行 FragmentTransaction.commit(),如有必要也可在 onCreate()里执行。不要随意改用FragmentTransaction.commitAllowingStateLoss() 或 者 直 接 使 用 try-catch 避 免 crash,这不是问题的根本解决之道,当且仅当你确认 Activity 重建、恢复状态时,本次 commit 丢失不会造成影响时才可这么做。
4.【推荐】不要在 Activity#onDestroy()内执行释放资源的工作,例如一些工作线程的 销毁和停止,因为 onDestroy()执行的时机可能较晚。可根据实际需要,在 Activity#onPause()/onStop()中结合 isFinishing()的判断来执行。
5.【推荐】总是使用显式Intent启动或者绑定Service,且不要为服务声明IntentFilter, 保证应用的安全性。如果确实需要使用隐式调用,则可为 Service 提供 Intent Filter 并从 Intent 中排除相应的组件名称,但必须搭配使用 Intent#setPackage()方法设置 Intent 的指定包名,这样可以充分消除目标服务的不确定性。
6.【推荐】对于只用于应用内的广播,优先使用 LocalBroadcastManager 来进行注册 和发送,LocalBroadcastManager 安全性更好,同时拥有更高的运行效率。
说明:
对于使用 Context#sendBroadcast()等方法发送全局广播的代码进行提示。如果该广播仅用于应用内,则可以使用 LocalBroadcastManager 来避免广播泄漏以及广播被拦截等安全问题,同时相对全局广播本地广播的更高效。
7.【推荐】当前 Activity 的 onPause 方法执行结束后才会创建(onCreate)或恢复 (onRestart)别的 Activity,所以在 onPause 方法中不适合做耗时较长的工作,这 会影响到页面之间的跳转效率。
8.【推荐】文本大小使用单位 dp,View 大小使用单位 dp。对于 TextView,如果在文 字大小确定的情况下推荐使用 wrap_content 布局避免出现文字显示不全的适配问 题。
说明:
之所以文本大小也推荐使用 dp 而非 sp,因为 sp 是 Android 早期推荐使用的,但其 实 sp 不仅和 dp 一样受屏幕密度的影响,还受到系统设置里字体大小的影响,所以使用 dp 对于应用开发会更加保证 UI 的一致性和还原度。
9.【推荐】使用 Toast 时,建议定义一个全局的 Toast 对象,这样可以避免连续显示 Toast 时不能取消上一次 Toast 消息的情况。即使需要连续弹出 Toast,也应避免直 接调用 Toast#makeText。
10.【强制】线程池不允许使用 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();
11.【推荐】ThreadPoolExecutor 设置线程存活时间(setKeepAliveTime),确保空闲时 线程能被释放.
12. 【推荐】禁止在多进程之间用 SharedPreferences 共享数据,虽然可以 (MODE_MULTI_PROCESS),但官方已不推荐。
13. 【强制】任何时候不要硬编码文件路径,请使用 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;
}
14.【强制】当使用外部存储时,必须检查外部存储的可用性
正例:
// 读/写检查
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_READ_ONLY.equals(state)) {
return true;
}
return false;
}
15.【强制】应用间共享文件时,不要通过放宽文件系统权限的方式去实现,而应使用 FileProvider。
16.【强制】如果 ContentProvider 管理的数据存储在 SQL 数据库中,应该避免将不受 信任的外部数据直接拼接在原始 SQL 语句中。
???这是个什么梗,都没说清楚???
正例:
// 使用一个可替换参数
String mSelectionClause = "var = ?"; String[] selectionArgs = {""}; selectionArgs[0] = mUserInput;
反例:
// 拼接用户输入内容和列名
String mSelectionClause = "var = " + mUserInput;
17.【强制】png 图片使用 TinyPNG 或者类似工具压缩处理,减少包体积。
18.【推荐】应根据实际展示需要,压缩图片,而不是直接显示原图。手机屏幕比较小,直接显示原图,并不会增加视觉上的收益,但是却会耗费大量宝贵的内存。
正例:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
// 首先通过 inJustDecodeBounds=true 获得图片的尺寸
final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 然后根据图片分辨率以及我们实际需要展示的大小,计算压缩率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 设置压缩率,并解码
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
19.【强制】在 Activity#onPause()或 Activity#onStop()回调中,关闭当前 activity 正在执 行的的动画。
正例:
public class MyActivity extends Activity {
ImageView mImageView;
Animation mAnimation;
Button mBtn;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mImageView = (ImageView) findViewById(R.id.ImageView01);
mAnimation = AnimationUtils.loadAnimation(this, R.anim.anim);
mBtn = (Button) findViewById(R.id.Button01);
mBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mImageView.startAnimation(mAnimation);
}
};
}
@Override
public void onPause() {
//页面退出,及时清理动画资源
mImageView.clearAnimation();
}
}
20.【推荐】使用 RGB_565 代替 RGB_888,在不怎么降低视觉效果的前提下,减少内存占用。
说明:
android.graphics.Bitmap.Config 类中关于图片颜色的存储方式定义:
- ALPHA_8 代表 8 位 Alpha 位图;
- ARGB_4444 代表 16 位 ARGB 位图;
- ARGB_8888 代表 32 位 ARGB 位图;
- RGB_565 代表 8 位 RGB 位图。
位图位数越高,存储的颜色信息越多,图像也就越逼真。大多数场景使用的是ARGB_8888 和 RGB_565,RGB_565 能够在保证图片质量的情况下大大减少内存的开销,是解决 OOM 的一种方法。
但是一定要注意 RGB_565 是没有透明度的,如果图片本身需要保留透明度,那么就不能使用 RGB_565。
正例:
Config config = drawableSave.getOpacity() != PixelFormat.OPAQUE ? Config.ARGB_8565 : Config.RGB_565;
Bitmap bitmap = Bitmap.createBitmap(w, h, config);
反例:
Bitmap newb = Bitmap.createBitmap(width, height, Config.ARGB_8888);
21.【推荐】在有强依赖 onAnimationEnd 回调的交互时,如动画播放完毕才能操作页面,onAnimationEnd 可能会因各种异常没被回调(参考: https://stackoverflow.com/questions/5474923/onanimationend-is-not-getting-calle d-onanimationstart-works-fine ), 建 议 加 上 超 时 保 护 或 通 过 postDelay 替 代onAnimationEnd。
正例:
View v = findViewById(R.id.xxxViewID);
final FadeUpAnimation anim = new FadeUpAnimation(v);
anim.setInterpolator(new AccelerateInterpolator());
anim.setDuration(1000);
anim.setFillAfter(true);
new Handler().postDelayed(new Runnable() {
public void run() {
if (v != null) {
v.clearAnimation();
}
}
}, anim.getDuration());
v.startAnimation(anim);
22.【推荐】当 View Animation 执行结束时,调用 View.clearAnimation()释放相关资源。
正例:
View v = findViewById(R.id.xxxViewID);
final FadeUpAnimation anim = new FadeUpAnimation(v);
anim.setInterpolator(new AccelerateInterpolator());
anim.setDuration(1000);
anim.setFillAfter(true);
anim.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationEnd(Animation arg0) {
//判断一下资源是否被释放了
if (v != null) {
v.clearAnimation();
}
});
v.startAnimation(anim);
总结
说真的,这手册总结得挺好的,虽然内容少了点,但是才1.0.1版本,还会继续修改完善的。
我觉得上面的第8点写得不太合理:
8.【推荐】文本大小使用单位 dp,View 大小使用单位 dp。对于 TextView,如果在文 字大小确定的情况下推荐使用 wrap_content 布局避免出现文字显示不全的适配问 题。
说明:
之所以文本大小也推荐使用 dp 而非 sp,因为 sp 是 Android 早期推荐使用的,但其 实 sp 不仅和 dp 一样受屏幕密度的影响,还受到系统设置里字体大小的影响,所以使用 dp 对于应用开发会更加保证 UI 的一致性和还原度。
我觉得:如果用户设置了系统字体大小,那么肯定是希望系统整体字体变大或变小,而你的APP却不怎么变,这看起来一来不协调,二来没有达到用户修改系统字体大小的目的,感觉这样的做法有点破坏系统的生态,不推荐这样做。