阿里Android 26条规范经验及优化

时间:2021-03-01 18:41:36

阿里Android 26条规范经验及优化

1、Activity 间的数据通信,对于数据量比较大的,避免使用 Intent Parcelable的方式,可以考虑 EventBus 等替代方案,以免造成 TransactionTooLargeException

2、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) {
 try {
 startActivity(intent);
 } catch (ActivityNotFoundException e) {
 if (Config.LOGD) {
 Log.d(LOGTAG, "activity not found for "   mimeType   " over "  
Uri.parse(url). getScheme(), e);
 }
 }
 }
}

3、避免在 BroadcastReceiver#onReceive()中执行耗时操作,如果有耗时工作,应该创建 IntentService 完成,而不应该在 BroadcastReceiver 内创建子线程去做。

说明:

由于该方法是在主线程执行,如果执行耗时操作会导致 UI 不流畅。可以使用IntentService 、 创 建 HandlerThread 或者调用 Context#registerReceiver
(BroadcastReceiver, IntentFilter, String, Handler)方法等方式,在其他 Wroker 线程执行 onReceive 方法。BroadcastReceiver#onReceive()方法耗时超过 10 秒钟,可能会被系统杀死

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

说明:

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

Intent intent = new Intent("my-sensitive-event");
intent.putExtra("event", "this is a testevent");
LocalBroadcastManager.getInstance(this).sendBroadcast(intent

5、不要在 Activity#onDestroy()内执行释放资源的工作,例如一些工作线程的销毁和停止,因为 onDestroy()执行的时机可能较晚。可根据实际需要,在Activity#onPause()/onStop()中结合 isFinishing()的判断来执行。

6、如非必须,避免使用嵌套的 Fragment。

说明:

嵌套 Fragment 是在 Android API 17 添加到 SDK 以及 Support 库中的功能,Fragment 嵌套使用会有一些坑,容易出现 bug,比较常见的问题有如下几种:

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

非必须的场景尽可能避免使用嵌套 Fragment,如需使用请注意上述问题。

7、当前Activity的onPause方法执行结束后才会执行下一个Activity的onCreate方法,所以在 onPause 方法中不适合做耗时较长的工作,这会影响到页面之间的跳转效率

8、不要在 Android 的 Application 对象中缓存数据。基础组件之间的数据共享请使用 Intent 等机制,也可使用 SharedPreferences 等数据持久化机制

class MyApplication extends Application {
 String username;
 String getUsername() {
 return username;
 }
 void setUsername(String username) {
 this.username = username;
 }
}

9、使用 Toast 时,建议定义一个全局的 Toast 对象,这样可以避免连续显示Toast 时不能取消上一次 Toast 消息的情况(如果你有连续弹出 Toast 的情况,避免使用 Toast.makeText)

10、使用 Adapter 的时候,如果你使用了 ViewHolder 做缓存,在 getView()的方法中无论这项 convertView 的每个子控件是否需要设置属性(比如某个 TextView设置的文本可能为 null,某个按钮的背景色为透明,某控件的颜色为透明等),都需要为其显式设置属性(Textview 的文本为空也需要设置 setText(""),背景透明也需要设置),否则在滑动的过程中,因为 adapter item 复用的原因,会出现内容的显示错乱

11、Activity或者 Fragment 中动态注册BroadCastReceiver 时,registerReceiver()和 unregisterReceiver()要成对出现。

说明:

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

部分华为的机型会对 receiver 进行资源管控,单个应用注册过多 receiver 会触发管控模块抛出异常,应用直接崩溃。

12、布局中不得不使用 ViewGroup 多重嵌套时,不要使用 LinearLayout 嵌套,改用 RelativeLayout,可以有效降低嵌套数。

说明:

Android 应用页面上任何一个 View 都需要经过 measure、layout、draw 三个步骤才能被正确的渲染。从 xml layout 的顶部节点开始进行 measure,每个子节点都需要向自己的父节点提供自己的尺寸来决定展示的位置,在此过程中可能还会重新measure(由此可能导致 measure 的时间消耗为原来的 2-3 倍)。节点所处位置越深,套嵌带来的 measure 越多,计算就会越费时。这就是为什么扁平的 View 结构会性能更好。

同时,页面拥上的 View 越多,measure、layout、draw 所花费的时间就越久。要缩短这个时间,关键是保持 View 的树形结构尽量扁平,而且要移除所有不需要渲染的View。理想情况下,总共的 measure,layout,draw 时间应该被很好的控制在 16ms以内,以保证滑动屏幕时 UI 的流畅。

要找到那些多余的 View(增加渲染延迟的 view),可以用 Android Studio Monitor里的 Hierarachy Viewer 工具,可视化的查看所有的 view

13、在 Activity 中显示对话框或弹出浮层时,尽量使用 DialogFragment,而非
Dialog/AlertDialog,这样便于随Activity生命周期管理对话框/弹出浮层的生命周期

14、文本大小使用单位 dp,view 大小使用单位 dp。对于 Textview,如果在文字大小确定的情况下推荐使用 wrap_content 布局避免出现文字显示不全的适配问题。

15、线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:

Executors 返回的线程池对象的弊端如下:

1)FixedThreadPool 和 SingleThreadPool : 允 许 的 请 求 队 列 长 度 为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM;
2)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();

16、当使用外部存储时,必须检查外部存储的可用性

// 读/写检查
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;
}

17、SharedPreference 提 交 数 据 时 , 尽 量 使 用 Editor#apply() ,而非Editor#commit()。一般来讲,仅当需要确定提交结果,并据此有后续操作时,才使用 Editor#commit()。

说明:

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

18、数据库 Cursor 必须确保使用完后关闭,以免内存泄漏。
多线程操作写入数据库时,需要使用事务,以免出现同步问题。
执行 SQL 语句时,应使用 SQLiteDatabase#insert()、update()、delete(),不要使用 SQLiteDatabase#execSQL(),以免 SQL 注入风险

19、加载大图片或者一次性加载多张图片,应该在异步线程中进行。图片的加载,涉及到 IO 操作,以及 CPU 密集操作,很可能引起卡顿。

20、在 ListView,ViewPager,RecyclerView,GirdView 等组件中使用图片时,应做好图片的缓存,避免始终持有图片导致内存泄露,也避免重复创建图片,引起性 能 问 题 。 建 议 使 用 Fresco (https://github.com/facebook/fresco )Glide(https://github.com/bumptech/glide)等图片库

21、png 图片使用 tinypng 或者类似工具压缩处理,减少包体积

22、在 Activity.onPause()或 Activity.onStop()回调中,关闭当前 activity 正在执行的的动画。

23、使用 inBitmap 重复利用内存空间,避免重复开辟新内存

private static void addInBitmapOptions(BitmapFactory.Options options,
 ImageCache cache) {
 // inBitmap 只处理可变的位图,所以强制返回可变的位图
 options.inMutable = true;
 if (cache != null) {
 Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
 if (inBitmap != null) {
 options.inBitmap = inBitmap;
 }
 }
}

24、使用 ARGB_565 代替 ARGB_888 大多数场景使用的是
ARGB_8888 和 RGB_565,RGB_565 能够在保证图片质量的情况下大大减少内存的开销,是解决 oom 的一种方法。

但是一定要注意 RGB_565 是没有透明度的,如果图片本身需要保留透明度,那么就不能使用 RGB_565

1)Glide 默认的 Bitmap 格式是 RGB_565 格式,而 Picasso 默认的是 ARGB_8888 格式,这个内存开销要小一半
2)在磁盘缓存方面,Picasso 只会缓存原始尺寸的图片,而 Glide 缓存的是多种规格,也就意味着 Glide 会根据你 ImageView 的大小来缓存相应大小的图片尺寸,比如你 ImageView 大小是200200,原图是 400400 ,而使用 Glide 就会缓存 200200 规格的图,而 Picasso 只会缓存 400400 规格的。这个改进就会导致 Glide 比 Picasso 加载的速度要快,毕竟少了每次裁剪重新渲染的过程。
3)最重要的一个特性是 Glide 支持加载 Gif 动态图,而 Picasso 不支持该特性。

25、图片压缩,减少内存占用

使用 inSampleSize 采样率压缩

public static Bitmap getFitSampleBitmap(String file_path, int width, int height) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file_path, options);
options.inSampleSize = getFitInSampleSize(width, height, options);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(file_path, options);
}
public static int getFitInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options) {
int inSampleSize = 1;
if (options.outWidth > reqWidth || options.outHeight > reqHeight) {
int widthRatio = Math.round((float) options.outWidth / (float) reqWidth);
int heightRatio = Math.round((float) options.outHeight / (float) reqHeight);
inSampleSize = Math.min(widthRatio, heightRatio);
}
return inSampleSize;
}

使用矩阵

大图小用用采样,小图大用用矩阵。

还是用前面模糊图片的例子,我们不是采样了么?内存是小了,可是图的尺寸也小了啊,我要用 Canvas 绘制这张图可怎么办?当然是用矩阵了

Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0, 0);
canvas.drawBitmap(bitmap, matrix, paint);

这样,绘制出来的图就是放大以后的效果了,不过占用的内存却仍然是我们采样出来的大小。如果我要把图片放到 ImageView 当中呢?一样可以,请看:

Matrix matrix = new Matrix();
matrix.postScale(2, 2, 0, 0);
imageView.setImageMatrix(matrix);
imageView.setScaleType(ScaleType.MATRIX);
imageView.setImageBitmap(bitmap);

合理选择Bitmap的像素格式

其实前面我们已经多次提到这个问题。ARGB8888格式的图片,每像素占用 4 Byte,而 RGB565则是 2 Byte。我们先看下有多少种格式可选:

格式:描述
ALPHA_8:只有一个alpha通道
ARGB_4444:这个从API 13开始不建议使用,因为质量太差
ARGB_8888:ARGB四个通道,每个通道8bit
RGB_565:每个像素占2Byte,其中红色占5bit,绿色占6bit,蓝色占5bit

这几个当中,ALPHA8 没必要用,因为我们随便用个颜色就可以搞定的。ARGB4444 虽然占用内存只有 ARGB8888 的一半,不过已经被官方嫌弃,失宠了。。『又要占省内存,又要看着爽,臣妾做不到啊T T』。ARGB8888 是最常用的,大家应该最熟悉了。RGB565 看到这个,我就看到了资源优化配置无处不在,这个绿色。。(不行了,突然好邪恶XD),其实如果不需要 alpha 通道,特别是资源本身为 jpg 格式的情况下,用这个格式比较理想。

26、当 View Animation 执行结束时,调用 View.clearAnimation()释放相关资源。

好了,文章到这里就结束了如果你觉得文章还算有用的话,不妨把它们推荐给你的朋友。