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_content
ListView
的设计决定了如果其宽度或高度被设置为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