背景
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 却不怎么变, 这看起来一来不协调, 二来没有达到用户修改系统字体大小的目的, 感觉这样的做法有点破坏系统的生态, 不推荐这样做
来源: https://juejin.im/post/5aa404b6f265da23945f191c