View在attach到window之前调用getViewTreeObserver().addOnDrawListener将不会进入回调。因为ViewTreeObserver的merge方法中没有处理OnDrawListener。详见源码。
onSizeChanged方法中直接改变布局参数不会生效。原因是这个方法是在布局过程中被调用的。详见这里。
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的bugViewFlipper是SDK提供的一个可以显示多个视图来回切换的类。它会在屏幕熄灭时暂停切换动画,等到下次屏幕点亮时再继续。但是SDK的实现有一个bug,如果屏幕再次点亮时手机没有锁屏(一种情况是用户没有设置锁屏,也可能是屏幕熄灭时间很短,还没有触发锁屏),ViewFlipper就不再播放切换动画。这也是一个登记在案的bug,解决办法是手动维护ViewFlipper动画的开始、暂停和继续。
ListView使用wrap_contentListView的设计决定了如果其宽度或高度被设置为wrap_content,它会在layout过程中调用getView方法获取所有子视图(不止一遍)以决定自己的大小,这会带来很多不好的副作用(性能问题只是之一)。可以看看ListView作者的说法。
RelativeLayout中使用<include/><include/>提供了一种复用布局文件的方法,但是在RelativeLayout中使用它,会出现android:layout_below等布局代码失效的现象。解决办法是在include元素上添加android:layout_width和android:layout_height属性。从谷歌官网上的讨论来看,最早提出这一解决办法的开发者是通过分析Android源码得到的,可见在必要的时候能够深入源码还是有很大用处的。
一种很常见的场景是一个方法的调用会触发另一个方法,为了说明方便我们设调用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)方法是同步的。再看看ViewPager的setAdapter(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文件夹,系统会默认其为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个方法。
序列化 android.graphics.Bitmap 类型的字段,在三星 S4、note 等机型上将失败。对这类不需要序列化的字段,应使用注释忽略掉。
有时程序的某些组件(如推送 Service)会放到单独的一个进程中。每个进程都会创建自己的 Application,如果在 Application 初始化时做了一些只应该做一次的工作,要注意去重。
comments powered by Disqus