Android Tips

前言

摘自阿里 Android 开发指南。

正文

1. 大分辨率图片建议统一放在 xxhdpi 目录下管理,否则将导致占有内存成倍数增加

说明:

根据当前的设备屏幕尺寸和密度,将会寻找最匹配的资源,如果将高分辨率图片放在低密度目录,将会造成低端机加载大图片资源,有可能造成 OOM,同时也是浪费资源,没有必要在低端机使用大图。

扩展参考:支持多种屏幕

2. Activity 间的数据通信,对于数据量比较大的,避免使用 Intent + Parcelable 的方式,可以考虑 EventBus 等替代方案,以免造成 TransactionTooLargeException。
    /**
     * 需要注意 resolveActivity 方法的第二个参数,必须为 MATCH_DEFAULT_ONLY
     * 使用这个标记位的原因在于只要这个方法不返回 null,startActivity 一定能成功
     * 如果不用这个标记位,就可以把那些 intent-filter 中 category 不含 DEFAULT 的 Activity 给匹配出来
     * 从而导致可能 startActivity 失败
     * 因为不含 DEFAULT 的 category 的 Activity 是无法接收隐式 Intent 的
     */
    public void startActivity(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) {
            try {
                startActivity(intent);
            } catch (ActivityNotFoundException e) {

            }
        }
    }
3. Activity#onSaveInstanceState() 方法不是 Activity 生命周期方法,也不保证一定会被调用。它是用来在 Activity 被意外销毁时保存 UI 状态的,只能用于保存临时性数据,例如 UI 控件的属性等,不能跟数据的持久化混为一谈。持久化存储应该在 Activity#onPause / onStop 中进行。
4. Activity 间通过隐式 Intent 的跳转,在发出 Intent 之前必须通过 resolveActivity 检查,避免找不到合适的调用组件,造成 ActivityNotFoundException。

示例:

5. 避免使用隐式 Intent 广播敏感信息,信息可能被其他注册了对应的 BroadcastReceiver 的 App 接收。

说明:

通过 Context#sendBroadcast() 发送的隐式广播会被所有感兴趣的 receiver 接收,恶意应用注册监听该广播的 receiver 可能获取到 Intent 中传递的敏感信息,并进行其他危险操作。如果发送的广播是使用 Context#sendOrderedBroadcast() 方法发送的有序广播,优先级较高的恶意 receiver 可能直接丢弃该广播,造成服务不可用,或者向广播结果塞入恶意数据。

如果广播仅限于应用内,则可以使用 LocalBroadcastManager#sendBroadcast() 实现,避免敏感信息外泄和 Intent 拦截的风险。

6. 添加 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.commitAllowingStateLoss() 或者直接使用 try-catch 避免 crash,这不是问题的根本解决之道,当切仅当你确认 Activity 重建、恢复状态时,本次 commit 丢失不会造成影响时才可以这么做。

https://developer.android.com/reference/android/app/FragmentTransaction#commit

7. 不要在 Activity#onDestory() 内执行释放资源的工作,例如一些工作线程的销毁和停止,因为 onDestory() 执行的时机可能较晚。可根据实际需要,在 Activity#onPasue / onStop 中结合 isFinishing()来判断执行。
8. 如非必须,避免使用嵌套的 Fragment。

说明:

Fragment 嵌套使用可能引起以下问题:

  1. onActivityResult() 方法的处理混乱,内嵌的 Fragment 可能收不到该方法的回调,需要由宿主 Fragment 进行转发处理
  2. 突变动画效果
  3. 被继承的 setRetainInstance(),导致在 Fragment 重建时多次触发不必要的逻辑

扩展参考:https://blog.csdn.net/megatronkings/article/details/51417510

9. 总是使用显示 Intent 启动或者绑定 Service,且不要为服务声明 Intent Filter 保证应用的安全性。如果确实需要使用隐式调用,则可为 Service 提供 Intent Filter 并从 Intent 中排除相应的组件名称,但必须搭配使用 Intent#setPackage() 方法设置 Intent 的指定包名,这样可以充分消除目标服务的不确定性。

扩展参考:https://developer.android.com/guide/components/services

10. 当前 Activity 的 onPause 方法执行结束后才会执行下一个 Activity 的 onCreate 方法,所以在 onPause 方法中不适合做耗时较长的工作,这会影响到页面之间的跳转效率。
11. 不要在 Android 的 Application 对象中缓存数据,基础组件之间的数据共享请使用 Intent 等机制,也可以使用 SharedPreferences 等数据持久化机制。
12. 使用 Toast 时,建议定义一个全局的 Toast 对象,这样可以避免连续显示 Toast 时不能取消上一次 Toast 消息的情况。
13. 使用 Adapter 的时候,如果使用了 ViewHolder 做缓存,在 getView() 的方法中无论这项 convertView 的每个子控件是否需要设置属性(比如某个 TextView 的文本不需要设置值),都要为其显式设置属性,否则在滑动的过程中,因为 adapter item 复用的原因,会出现内容的显示混乱。
14. Activity 或者 Fragment 中动态注册 BroadcastReceiver 时,registerReceiver 和 unregisterReceiver 要成对出现。

说明:

如果没有成对出现,则可能导致已经注册的 receiver 没有在合适的时机注销,导致内存泄漏,占用内存空间,加重 SystemServer 负担。

    /**
     * 正例
     * 在 onResume 中注册,在 onPause 中注销
     */
    @Override
    protected void onResume() {
        super.onResume();
        registerReceiver(mMyReceiver, mIntentFilter);
    }

    @Override
    protected void onPause() {
        super.onPause();
        unregisterReceiver(mMyReceiver);
    }

    /**
     * 反例
     * Activity 的生命周期不对应,可能出现多次 onResume 造成 receiver 注册多个
     * 但最终只注销一个,其余 receiver 产生内存泄漏
     */
    @Override
    protected void onResume() {
        super.onResume();
        registerReceiver(mMyReceiver, mIntentFilter);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mMyReceiver);
    }
15. 在 Activity 中显示对话框或弹出浮层时,尽量使用 DialogFragment,而非 Dialog / AlertDialog,这样便于随 Activity 生命周期管理对话框的生命周期。
    private void showDialog(String text){
        DialogFragment dialogFragment=new MyDialogFragment();
        dialogFragment.show(getSupportFragmentManager(),"TAG");
    }

    /**
     * 如果是内部类的写法,必须是 public static 的
     * 这是为了系统能够重新实例化它们,它也是 Fragment
     * 从官方文档可以看出::每个片段都必须有一个空的构造函数,这样就可以在恢复其活动状态时实例化它
     * 强烈建议子类不要有带有参数的其他构造函数,因为在重新实例化片段时不会调用这些构造函数
     * 调用者可以用setArguments(Bundle)提供参数,然后通过片段getArguments()检索参数
     */

    public static class MyDialogFragment extends DialogFragment {
        @Nullable
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
            View view = inflater.inflate(R.layout.activity_main, container);
            return view;
        }
    }
16. 不能在 Activity 完全没有显示时显示 PopupWindow 和 Dialog。

说明:

PopupWindow 通常情况下是需要依靠某个 View 来进行定位的,在 Activity 没有完全显示的时候可能该 View 还没创建好。

但是 Dialog 为什么也不行呢?这这就触及到我的知识盲区了。

17. 尽量不要使用 AnimationDrawable,它在初始化的时候就将所有图片加载到内存中,特别占内存,可能会导致 OOM,并且还不能释放,释放之后下次进入再次加载时会报错。
18. 不能使用 ScrollView 包裹 ListView / GridView / ExpandableList,因为这样会把 ListView 的所有 Item 都加载到内存中。

说明:

ScrollView 中嵌套 List 或 RecyclerView 的做法官方明确禁止,这种做法对性能有较大损耗,为了良好的用户体验,推荐使用 NestedScrollView。

19. 不要通过 Intent 在 Android 基础组件之间传递大数据( binder transaction 缓存为 1 MB ),可能导致 OOM。
20. 在 Application 的业务初始化代码加入进程判断,确保只在自己需要的进程初始化,特别是后台进程减少不必要的业务初始化。
public class MyApplication extends Application {

    private boolean isMainProcess;
    private boolean isBgProcess;

    @Override
    public void onCreate() {
        super.onCreate();
        
        //在所有进程中初始化
        //...
        
        //仅在主进程中初始化
        if (isMainProcess){
            //...
        }
        
        //仅在后台进程初始化
        if (isBgProcess){
            //...
        }
    }
}
21. 新建线程时,必须通过线程池提供( AsyncTask 或者 ThreadPoolExecutor 或者其他形式自定义的线程池 ),不允许在应用中自行显式创建线程。

说明:

使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

    /**
     * 反例
     */
    private void startThread(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                
            }
        }).start();
    }
22. ThreadPoolExecutor 设置线程存活时间( setKeepAliveTime ),确保空闲时线程能被释放。
23. 禁止在多进程之间用 SharedPreferences 共享数据,虽然可以 ( MODE_MULTI_PROCESS ),但官方已不推荐。
24. 谨慎使用 Android 多进程,多进程虽然能降低主进程的内存压力,但会遇到如下问题:
  1. 不能实现完全退出所有 Activity 的功能
  2. 首次进入新启动进程的页面时会有延时的现象( 有可能黑屏、白屏几秒,是白屏还是黑屏和 Activity 的主题有关 )
  3. 应用内多进程时,Application 实例化多次,需要考虑各个模块是否都需要在多有进程中初始化
  4. 多进程间通过 SharedPreferences 共享数据时不稳定
26. 任何时候都不要硬编码文件路径,请使用 Android 文件系统 API 访问。

说明:

Android 应用提供内部和外部存储,分别用于存放应用自身数据以及应用产生的用户数据,可以通过相关 API 获取对应的目录,进行文件操作。

    /**
     * 正例
     */
    private File getDir(String name) {
        File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), name);
        if (!file.mkdirs()) {

        }
        return file;
    }

    /**
     * 反例
     * 任何时候都不要硬编码文件路径,这不仅存在安全隐患
     * 而且也让 app 更容易出现适配问题
     */
    private File getDir() {
        File file = new File("/download/pictures", "catch");
        if (!file.mkdirs()) {

        }
        return file;
    }
27. 当使用外部存储时,必须检查外部存储的可用性。
    /**
     * 读/写检查
     */
    private boolean isExternalStorageWritable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            return true;
        }
        return false;
    }

    /**
     * 只读检查
     */
    private boolean isExternalStorageReadable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)
                || Environment.MEDIA_MOUNTED.equals(state)) {
            return true;
        }
        return false;
    }
27. 应用间共享文件时,不要通过放宽文件系统权限的方式去实现,而应该使用 FileProvider。
28. SharedPreference 提交数据时,尽量使用 Editor#apply(),而非 Editor#commit()。一般来讲,仅当需要确定提交结果,并据此有后续操作时,才使用 Editor#commit()。

说明:

SharedPreference 相关修改使用 apply 方法进行提交会先写入内存,然后异步写入磁盘,commit 方法是直接写入磁盘。如果操作频繁的话,apply 的性能会优于 commit,apply 会将最后修改内存写入磁盘。但是如果希望立刻获取存储操作的结果,并据此做相应的其他操作,应当使用 commit。

扩展参考:https://developer.android.com/reference/android/content/SharedPreferences.Editor#apply

29. 数据库 Cursor 必须确保使用完后关闭,以免内存泄漏。

说明:

Cursor 是对数据库查询结果集管理的一个类,当查询的结果集较小时,消耗内存不易察觉。但是当结果集较大,长时间重复操作会导致内存消耗过大,需要在操作完成后手动关闭 Cursor。

30. 多线程操作写入数据库时,需要使用事务,以免出现同步问题,同时开启事务能提高执行效率。

说明:

Android 通过 SQLiteOpenHelper 获取数据库 SQLiteDatabase 实例,Helper 中会自动缓存已经打开的 SQLiteDatabase 实例,单个 APP 中应该使用 SQLiteOpenHelper 的单例模式确保数据库连接唯一。由于 SQLite 自身是数据库级锁,单个数据库操作是保证线程安全的( 不能同时写入 ),transaction 是一次原子操作,因此处于事务中的操作是线程安全的。

若同时打开多个数据库连接,并通过多线程写入数据库,会导致数据库异常,提示数据库已被锁住。

扩展参考:

https://www.jianshu.com/p/57eb08fe071d

https://developer.android.com/reference/android/database/sqlite/SQLiteDatabase#public-methods

31. 执行 SQL 语句时,应该使用 SQLiteDatabase#insert / update / delete ,不要使用 SQLiteDatabase#execSQL,以免 SQL 注入风险。
32. 在 Activity#onPause / onStop 回调中,关闭当前 Activity 正在执行的动画。
33. 使用 inBitmap 重复利用内存空间,避免重复开辟新内存。
34. 使用 ARGB_565 替代 ARGB_8888,在不怎么降低视觉效果的前提下,减少内存占用。但是需要注意 RGB_565 是没有透明度的,如果图片本身需要保留透明度,那么就不能使用 RBG_565。
35. 将 android:allowbackup 属性设置为 false,防止 add backup 导出数据。

说明:

在 AndroidManifest.xml 文件中为了方便对程序数据的备份和恢复在 Android API 8 以后增加了 android:allowBackup 属性值,默认值是 true,当 true 时,即可通过 add backup 和 add restore 来备份和恢复应用程序数据。

36. META-INF 目录中不能包含如 .apk、.so 等敏感文件,该文件夹没有经过签名,容易被恶意替换。

你可能感兴趣的:(Android Tips)