Android 开发过程中的坑和小技巧

空指针问题

NullPointerException 绝对是开发人员遇到最多的问题,也是 Android 开发过程中一个大坑,总是在你意料不到的时候出现。要解决这个问题,关键地方不要吝啬 if 语句,需要在用到某一对象的时候多想想有没什么可能会导致对象没有初始化或者被指向为null,下面是一些比较有代表性的例子:

  • Fragment的Handler 中调用 getActivity(),需要判空和判断 activity 是否销毁
1
2
3
4
5
6
7
8
9
10
private static class MyHandler extends Handler {
...
@Override
public void handleMessage(Message msg) {
// 需要判空
if(null != mFragment.getActivity() && !mFragment.getActivity().isDestroyed()) {
// TODO
}
}
}
  • 使用 Cursor 时没有判空,没有在 finally 代码块中关闭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Cursor cursor = null;
try {
cursor = contentResolver.query(uri, null, selection, selectionArgs, null);
// cursor可能为空
if(null != cursor && cursor.moveToFirst()) {
// TODO
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != cursor) {
cursor.close();
}
}

内存泄漏问题

内存泄漏是开发中特别需要注意的一个问题,程序中很多莫名其妙的 OutOfMemoryError 都是由此引起的。虽然现在有 MAT 和 leakcanary 方便分析内存泄漏的工具,但是我们在编程时就先避免的话无疑会好很多。

我觉得内存泄漏可以大致分为这几类:(1)某个对象的引用的生命周期大于对象本身的生命周期,导致该对象无法被回收,简称引用未释放 (2)资源对象没有关闭或者销毁

引用未释放

Context 泄漏

Context 泄漏是 Android 程序中非常容易出现的问题,我列出两种隐晦的 Context 泄漏情况:(1) 静态变量持有 context 的引用未释放 (2) 匿名内部类隐式地持有 context 引用。

  • 静态变量持有 context 的引用未释放

例如把一个 TextView 背景图片的 Drawable 对象设为静态变量,在设置 Drawable 为背景图片时 drawable 会调用 setCallback 方法而持有 TextView 的引用,TextView 又持有 Context 的引用,所以就会导致 Context 泄漏。可以看 Android 官方文档的详细例子:avoiding-memory-leaks

但是在 Android 3.0(API 11) 之后,Drawable 的 setCallback 方法改为弱引用的方式mCallback = new WeakReference<Callback>(cb);,所以在 3.0 以后不会因为这个问题产生内存泄漏,但是我们还是需要注意静态变量的使用。

  • 匿名内部类隐式地持有 context 引用

在 Android 中经常使用 Handler 处理异步消息,一般比较懒的做法是直接使用匿名内部类,但是这样的话 Handler 会隐式地持有外部类即 Activity 的引用,而 Handler 的生命周期可能会比 Activity 更长,就会导致 context 泄漏。更多关于 Handler 内部类导致泄漏的问题,可以看 Android 中由 Handler 和内部类引起的内存泄漏。对于 Handler,推荐使用下面的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SampleActivity extends Activity {
/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (null != activity) {
// ...
}
}
}
private final MyHandler mHandler = new MyHandler(this);
@Override
protected void onDestroy() {
super.onDestroy();
// 避免因为 delay 消息导致的内存泄漏
mHandler.removeCallbacksAndMessages(null);
}
}

避免 Context 泄漏(即 Activity 内存泄漏)须谨记:

  1. 不要让生命周期长于 Activity 的对象持有 Activity 的强引用
  2. 尽量使用 Application 的 Context 而不是 Activity 的 Context
  3. 尽量不要在 Activity 中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用

注册没及时取消

static 关键字误用

资源未关闭

WebView 对象调用 destroy() 方法释放内存

Cursor 或 File 没有关闭

Bitmap 没有及时 recycle

性能问题

忽视循环体效率

尽量不要在循环中包含内存分配操作,把 try-catch 放到循环体外面。(编程规范中建议不要在循环内调用同步方法和使用 try-catch 块)

避免创建不必要的对象

在拼接字符串的时候,不要直接用加号连接符,这会创建多余的对象效率也不高,应该优先考虑用 StringBuffer 或者 StringBuilder 来拼接。

选择 static 而不是 virtual

如果你不需要访问一个对象的值,请保证这个方法是 static 类型的,这样方法调用将快 15% - 20%。这是一个好的习惯,因为你可以从方法声明中得知调用无法改变这个对象的状态。

使用 LocalBroadcastManager 代替普通的 BroadcastReceiver,效率和安全性都更高

关于 LocalBroadcastManager 的实现原理,推荐看 LocalBroadcastManager 的实现原理,还是 Binder?

for 循环的使用

传统的 fori 循环不要把获得数组长度的代码写在循环当中:for(int i = 0; i < array.size(); i ++),JDK 1.5 后使用 for-each 循环遍历实现 Iterable 接口的集合和数组更方便,不过对于 ArrayList 使用传统的循环效率更高。

尽量多使用自带库函数,减少开发成本而且还有汇编级别的优化

使用 标签重用布局文件,使用 标签减少布局层级,用 ViewStub 按需载入布局

尽量用 IntDef 代替 Enum

Enum 的内存消耗通常是 static constants 的 2 倍。

避免 Bitmaps 的浪费,缩小到你需要的分辨率(小于设备分辨率)

使用 Android Framework 优化过的容器类,例如 SpareArray, SpareBooleanArray 与 LongSpareArray。

应用有多个进程时,新启动一个进程时不会重新创建 Application,但是会重新调用 Application.onCreate(),所以要注意在 onCreate() 中重复你编写初始化的代码

尽量避免给 window 和 Activity 同时设置背景,这样会造成过渡绘制。

避免不必要的异常

不要使用 AsyncTask 处理异步任务,推荐使用 Loader

AysncTask 可以用简短的代码实现异步操作,但是有很多需要注意的问题,可以看 Android 中糟糕的 AsyncTask 进一步了解。

在 Android library project 中不能使用 switch-case 访问资源ID

因为在 library project 中生成的 R.java 资源 ID 不是常量,可以改写成 if-else 语句。

不要在 Activity 没有完全显示时显示 Dialog 或 PopupWindow:Problems creating a Popup Window in Android Activity

多进程间通过 SharedPreferences 共享数据时不稳定,具体可以查阅《Android 开发艺术探索》。

不要通过 Bundle 传递大块数据,否则会有 TransactionTooLargeException 异常:Passing large data to second Activity

数据库问题

数据库 onUpgrade 过程中添加重复字段的问题

假设在数据库版本号为 3 时新增了表 A,在版本 6 时在表 A 中新增一个字段 type,那么从数据库版本为 1 升级到 6 时会出现 type 字段已经存在的错误,这时应该判断下在版本 3 新增表 A 后就不需要新增字段了。

动画问题

android.view.animation.Animation 有可能不会调用 AnimationListener的onAnimtionEnd

Animation 的工作流程是View.draw()—>View.drawAnimation()—>Animation.getTransformation(),但是 View 在 onDetachedFromWindow 会执行mCurrentAnimation = null;,这时如果动画未完成就会就此中断,接下来的动画操作和 listener 回调都不会执行。要解决这个问题有两个方法:(1) 添加 OnAttachStateChangeListener,在 onViewDetachedFromWindow 中执行 onAnimtionEnd 的逻辑 (2) 使用 Animator 替代 Animation

在 android.view.animation.Animation.AnimationListener 的 onAnimationStart, onAnimationEnd, onAnimationRepeat 中不要 addView 或 removeView

这样可能会引起java.lang.NullPointerException: Attempt to read from field ‘int android.view.View.mViewFlags’ on a null object reference,因为这个三个动画的回调方法都是在 View.draw() 中执行的,而在 draw 函数中不要 addView 或 removeView。在 addView 的方法说明中有强调do not invoke this method from {@link #draw(android.graphics.Canvas)},{@link #onDraw(android.graphics.Canvas)},{@link #dispatchDraw(android.graphics.Canvas)} or any related method.

多线程问题

不要在非 UI 线程更新 View 数据显示,特别是从网络拉取回来的数据应该注意所在线程 

View 问题

ListView 的 header,footer,item 设置 Visibility 为 Gone 无效

因为 ListView 对 child view 执行 measure 前,没有判断 Visibility 为 Gone 的情况。解决方法是在 header 外面包一层 parent view (如 FrameLayout),再设置 parent view 里面的 child view 为 Gone。

View 的 post,postDelayed 方法在 DetachedFromWindow (即不在当前的 View 树中)的时候无效

比较简单的修改方法是用 Handler.post()

使用 setEnabled(false) 后没有效果

ListView 的 child view 调用 setEnabled(false) 后仍然会响应 onItemClick,parent view 调用 setEnabled(false) 后 Touch 事件还会传递到 child view。对于这两种问题可以用view.setEnabled(false); view.setClickable(true);解决,具体问题解析可以看 Android View setEnabled false 没有效果的解决方法

最后分享一个记录 Android 的各种坑的平台 https://code.google.com/p/android/issues/list,需要翻越墙壁,是 Android 自己提供的,很多开发者会把踩到的坑提交上去。大家在开发过程中遇到一些坑的时候可以这个平台看看有没天涯沦落人,也可以根据 Star,Priority 查看感兴趣的坑。