本文记录一些Android开发中遇到的坑。掉坑里的原因有时是API文档对某个细节语焉不详,有时就直接是Android系统的bug。这些坑在被发现之后,大多能通过搜索找到解决办法,但有时也需要开发者深入源码去探究问题的根源。

addOnDrawListener的问题

View在attach到window之前调用getViewTreeObserver().addOnDrawListener将不会进入回调。因为ViewTreeObserver的merge方法中没有处理OnDrawListener。详见源码。

onSizeChanged方法中改变布局参数需要注意

onSizeChanged方法中直接改变布局参数不会生效。原因是这个方法是在布局过程中被调用的。详见这里

chrisbanes/Android-PullToRefresh闪烁问题

Android-PullToRefresh是一个开源下拉刷新控件。项目中使用这个控件时发现,在控件尺寸发生改变时,列表内容会闪烁一下。原因是其在onSizeChanged方法中通过post调用requestLayout,布局变化在第二次layout pass中才生效。修复commit在此。结合上一个问题看,深入理解View绘制过程对解决自定义控件中的问题是很有必要的。

动态注册广播接收器

IntentService中动态注册的广播接收器在任务执行完后会失效。详见这里

SharedPreferences的bug

保存字符串如果末尾是换行符,在下次读取时可能会多四个空格。详见这里

RemoteViews的bug

在通知栏通知中显示自定义视图,需要用到RemoteViews这个类。版本上线后发现在Android 2.3及以下系统,收到的通知是空白的。搜索发现这是一个谷歌官方记录在案的bug。解决办法是用以下方法绕过去:

notification = builder.build();
notification.contentView = contentView;

此bug在官方文档中没有说明。对于这类问题,目前看来只能依靠开发者的经验加全面的测试来尽可能地避免。

convertView类型错误

同样是低版本上会出现的问题。Adapter有如下方法:

public abstract int getViewTypeCount ()
public abstract int getItemViewType (int position)
public abstract View getView (int position, View convertView, ViewGroup parent)

根据文档,在正确地重写了前两个方法后,第三个方法的第二个参数传入的convertView应该有正确的类型。但是测试发现在低版本系统上传入的convertView类型是错误的。此时必须检查传入参数的类型。造成此现象的原因是低版本ListView相关类中复用视图的实现有问题。

ViewFlipper的bug

ViewFlipper是SDK提供的一个可以显示多个视图来回切换的类。它会在屏幕熄灭时暂停切换动画,等到下次屏幕点亮时再继续。但是SDK的实现有一个bug,如果屏幕再次点亮时手机没有锁屏(一种情况是用户没有设置锁屏,也可能是屏幕熄灭时间很短,还没有触发锁屏),ViewFlipper就不再播放切换动画。这也是一个登记在案的bug,解决办法是手动维护ViewFlipper动画的开始、暂停和继续。

不要对ListView使用wrap_content

ListView的设计决定了如果其宽度或高度被设置为wrap_content,它会在layout过程中调用getView方法获取所有子视图(不止一遍)以决定自己的大小,这会带来很多不好的副作用(性能问题只是之一)。可以看看ListView作者的说法

RelativeLayout中使用<include/>

<include/>提供了一种复用布局文件的方法,但是在RelativeLayout中使用它,会出现android:layout_below等布局代码失效的现象。解决办法是在include元素上添加android:layout_widthandroid:layout_height属性。从谷歌官网上的讨论来看,最早提出这一解决办法的开发者是通过分析Android源码得到的,可见在必要的时候能够深入源码还是有很大用处的。

方法调用、返回的顺序,同步vs异步

一种很常见的场景是一个方法的调用会触发另一个方法,为了说明方便我们设调用a方法会触发b方法。在不同的实现下,b方法的调用时机会有微妙的差异。一种情况是b方法的调用完全包含在a方法的调用之内,a方法返回前b方法已经调用完毕,我们称之为同步的调用;另一种情况是b方法的调用在a方法返回之后,我们称之为异步的调用。异步调用下,b方法甚至不会在a方法返回后立即被调用,而是要等到下一轮消息队列被处理时。同步调用的例子可以看View类上的performClick()方法:

public boolean performClick() {
  sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
  ListenerInfo li = mListenerInfo;
  if (li != null && li.mOnClickListener != null) {
    playSoundEffect(SoundEffectConstants.CLICK);
    li.mOnClickListener.onClick(this);
    return true;
  }
  return false;
}

可以发现调用performClick()所触发的onClick(View)方法是同步的。再看看ViewPagersetAdapter(PagerAdapter)方法,调用它会触发PagerAdapter上的instantiateItem(ViewGroup, int)方法。这个调用是同步还是异步的呢?阅读ViewPager源码,可以看到在setAdapter(PagerAdapter)内部使用的populate()方法中,有这样的语句:

void populate() {
  ...
  // Also, don't populate until we are attached to a window.  This is to
  // avoid trying to populate before we have restored our view hierarchy
  // state and conflicting with what is restored.
  if (getWindowToken() == null) {
    return;
  }
  ...
}

这说明如果ViewPager当前没有被添加到Window中,在setAdapter(PagerAdapter)返回前instantiateItem(ViewGroup, int)方法不会被执行。这种情况下调用是异步的。如果在instantiateItem(ViewGroup, int)方法中做一些初始化工作,在setAdapter(PagerAdapter)方法返回后这些初始化代码有可能还没有被执行,不能对调用顺序做错误的假设。一个被触发的方法调用是同步的还是异步的,一般在文档中没有说明。如果在实现中需要知道这些细节,阅读源码是最好的选择。涉及到 View 变化的方法,大多是异步的,下面代码中的layout将不起作用:

LinearLayout root = (LinearLayout) findViewById(R.id.root);
TextView label = new TextView(this);
label.setText("some text");
root.addView(label);
label.layout(0, 0, width, height);

因为 addView(View) 是异步的,调用时会在主线程消息队列中插入下一轮 layout 任务,等到其执行时会以视图自身的参数再调用一次 layout。

ListView在Motorola Android 2.3手机上背景为灰色

这个问题只在Motorola Android 2.3系统的手机上出现,即在ListView自身的高度大于其含有的所有行的总高度时,无法显示通过android:background属性(或者通过代码)指定的背景,总是显示带阴影效果的灰色背景。比如下面的layout

<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#90EE90" />

设置了背景色,正确的效果是这样的:

在Motorola Android 2.3的手机上效果为:

这是因为Motorola修改了Android系统内置主题,必须要额外设置ListView的属性

android:overScrollFooter="@null"

才能显示出开发者设定的背景。根据stackoverflow上的讨论,最早是有人在Motorola的开发者论坛提问后,Motorola的工程师给出的其中原因和解决办法。

单个点9图应放入drawable-nodpi文件夹

点9图如果放入drawable文件夹,系统会默认其为mdpi资源,在高dpi手机上会把它先按比例放大再使用,从而破坏点9图效果。其实对于一般的点9图使用场景(如圆角按钮背景图),严谨的做法仍然是做一套适配多屏幕密度的资源,分别放入对应的资源文件夹。

使用inflate方法创建视图要传入父视图

LayoutInflater等类提供了若干从布局文件创建视图的方法。如:

public View inflate (int resource, ViewGroup root)
public View inflate (int resource, ViewGroup root, boolean attachToRoot)

在创建的视图有明确的父视图的情况下,必须传入父视图(上述方法的第二个参数,它会在inflate过程中起作用),不能传null。比如在各种Adapter实现类的getView方法中:

// 错误的写法:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
  if (convertView == null) {
    // 错误:没有传入父视图
    convertView = inflater.inflate(R.layout.list_item, null);
    ...
  }
  return convertView;
}
// 正确的写法:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
  if (convertView == null) {
    // 传入父视图
    convertView = inflater.inflate(R.layout.list_item, parent, false);
    ...
  }
  return convertView;
}

不传入父视图会导致布局文件中的某些属性失效。在视图比较简单的情况下甚至可能不会显现出来,但其实这时已经埋下了隐患。

移动网络特有的问题

在某些移动网络下,因为运营商网关的运行方式,客户端发起的 HTTP 请求中的 Host 头可能会被丢弃。此时需要加上额外的头信息:

X-Online-Host: www.example.com

才能在服务器端收到正确的 Host 头。详情参见这里这里

在某些移动网络下,读流的时候本来应该是 EOF 的地方会抛一个 IOException,需要做特殊处理。

为解决 WebView 在 CMWAP 接入点下联网失败的问题,需要在 WebView 所处 Activity 或者 Fragment 的生命周期方法内调用适当的方法,如下:

@Override
protected void onResume() {
  super.onResume();
  WebView.enablePlatformNotifications();
}

@Override
protected void onStop() {
  super.onStop();
  WebView.disablePlatformNotifications();
}

这2个方法在高版本 SDK 中被移除了,如果想使用高版本 SDK,需要使用反射:

@Override
protected void onResume() {
  super.onResume();
  try {
    WebView.class.getMethod("enablePlatformNotifications").invoke(null);
  } catch (Exception ignore) {
  }
}

@Override
protected void onStop() {
  super.onStop();
  try {
    WebView.class.getMethod("disablePlatformNotifications").invoke(null);
  } catch (Exception ignore) {
  }
}

实际上在高版本手机 ROM 内的 WebView 仍然有这2个方法。

JSON序列化要注意某些特殊类型的字段

序列化 android.graphics.Bitmap 类型的字段,在三星 S4、note 等机型上将失败。对这类不需要序列化的字段,应使用注释忽略掉。

Application作为单例来使用的问题

有时程序的某些组件(如推送 Service)会放到单独的一个进程中。每个进程都会创建自己的 Application,如果在 Application 初始化时做了一些只应该做一次的工作,要注意去重。

comments powered by Disqus