Android5.0适配(API21)
Android5.0适配(API21)
Android5(API21)
OutlineProvider
见 [[Android实现圆角#ViewOutlineProvider]]
Android5.1 及以下 onVisibilityChanged 访问成员变量空指针
问题描述
问题现象:Android5.1 会出现 npe,高版本不会
问题代码:
1
2
3
4
5
6
7
8
public class GiftBox extends FrameLayout implements ViewLifecycleOwner {
private ViewLifecycleOwnerDelegate mViewLifecycleOwnerDelegate = new ViewLifecycleOwnerDelegate(this);
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
mViewLifecycleOwnerDelegate.onVisibilityChanged(visibility);
}
}
问题堆栈:
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
java.lang.RuntimeException:Unable to start activity ComponentInfo{qsbk.app.remix/qsbk.app.live.ui.LivePushActivity}: android.view.InflateException: Binary XML file line #33: Error inflating class qsbk.app.live.widget.gift.LiveGiftBox
2 android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2675)
3 ......
4 Caused by:
5 java.lang.NullPointerException:Attempt to invoke virtual method 'void qsbk.app.core.ui.ViewLifecycleOwnerDelegate.onVisibilityChanged(int)' on a null object reference
6 qsbk.app.live.widget.gift.GiftBox.onVisibilityChanged(GiftBox.java:418)
7 android.view.View.dispatchVisibilityChanged(View.java:8895)
8 android.view.ViewGroup.dispatchVisibilityChanged(ViewGroup.java:1178)
9 android.view.View.setFlags(View.java:9990)
10 android.view.View.<init>(View.java:4231)
11 android.view.ViewGroup.<init>(ViewGroup.java:529)
12 android.widget.FrameLayout.<init>(FrameLayout.java:133)
13 android.widget.FrameLayout.<init>(FrameLayout.java:129)
14 qsbk.app.live.widget.gift.GiftBox.<init>(GiftBox.java:110)
15 qsbk.app.live.widget.gift.GiftBox.<init>(GiftBox.java:106)
16 qsbk.app.live.widget.gift.LiveGiftBox.<init>(LiveGiftBox.java:89)
17 java.lang.reflect.Constructor.newInstance(Native Method)
18 java.lang.reflect.Constructor.newInstance(Constructor.java:288)
19 android.view.LayoutInflater.createView(LayoutInflater.java:607)
20 android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:743)
21 android.view.LayoutInflater.rInflate(LayoutInflater.java:806)
22 android.view.LayoutInflater.rInflate(LayoutInflater.java:809)
23 android.view.LayoutInflater.inflate(LayoutInflater.java:504)
24 android.view.LayoutInflater.inflate(LayoutInflater.java:414)
25 android.view.LayoutInflater.inflate(LayoutInflater.java:365)
26 com.android.internal.policy.impl.PhoneWindow.setContentView(PhoneWindow.java:435)
27 android.app.Activity.setContentView(Activity.java:2203)
28 qsbk.app.core.ui.base.BaseActivity.onCreate(BaseActivity.java:59)
29 qsbk.app.live.ui.StreamMediaActivity.onCreate(StreamMediaActivity.java:35)
30 qsbk.app.live.ui.LiveBaseActivity.onCreate(LiveBaseActivity.java:433)
31 qsbk.app.live.ui.LivePushActivity.onCreate(LivePushActivity.java:167)
32 android.app.Activity.performCreate(Activity.java:6251)
33 android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1112)
34 android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2622)
35 android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2766)
36 android.app.ActivityThread.access$1000(ActivityThread.java:197)
37 android.app.ActivityThread$H.handleMessage(ActivityThread.java:1611)
38 android.os.Handler.dispatchMessage(Handler.java:111)
39 android.os.Looper.loop(Looper.java:224)
40 android.app.ActivityThread.main(ActivityThread.java:5958)
41 java.lang.reflect.Method.invoke(Native Method)
42 java.lang.reflect.Method.invoke(Method.java:372)
43 com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1113)
44 com.android.internal.os.ZygoteInit.main(ZygoteInit.java:879)
问题复现
- 一个对象实例化过程为:初始化成员变量 ->调用 init 函数 ->调用构造函数
- 一个子类对象实例化过程为:父类初始化成员变量 ->父类 init 函数 ->父类构造函数→子类初始化成员变量 ->子类调用 init 函数 ->子类调用构造函数
现在复现问题:
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
30
31
32
33
34
35
36
37
38
abstract class Parent {
var field: String = getField22()
init {
println("Parent init---")
}
constructor() {
println("Parent constructor---")
method1()
}
private fun getField22(): String {
println("Parent field init---")
return "hacket-Parent"
}
abstract fun method1()
}
class Child : Parent {
private var str = "nihao hacket."
var field2: String = getField232()
init {
println("--->>> Child init---")
}
constructor() {
println("--->>> Child constructor---")
}
private fun getField232(): String {
println("--->>> Child field init---")
return "hacket-Child"
}
override fun method1() {
println("--->>> Child method1 str=$str")
str.toUpperCase()
}
}
fun main() {
Child()
}
结果:
1
2
3
4
5
6
7
8
9
10
Parent field init---
Parent init---
Parent constructor---
--->>> Child method1 str=null
Exception in thread "main" kotlin.TypeCastException: null cannot be cast to non-null type java.lang.String
at me.hacket.hello.Child.method1(Test2.kt:43)
at me.hacket.hello.Parent.<init>(Test2.kt:12)
at me.hacket.hello.Child.<init>(Test2.kt:33)
at me.hacket.hello.Test2Kt.main(Test2.kt:48)
at me.hacket.hello.Test2Kt.main(Test2.kt)
原因:父类构造方法调用了子类需要重写的方法,子类在这方法调用了成员变量,而此时子类还没有成员变量初始化,就会 NPE
结论:避免在父类的成员变量、构造方法、init 方法中调用子类需要重写的方法;如果不可避免,需要避免子类在该重写的方法中调用成员变量,因为此时子类的成员变量还未初始化,可能导致不可预测的结果
onVisibilityChanged 源码追溯
看 Android5.1.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// View Android5.1.1_r38 https://cs.android.com/android/platform/superproject/+/android-5.1.1_r38:frameworks/base/core/java/android/view/View.java;bpv=0;bpt=1
public View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
// ...
if (viewFlagMasks != 0) {
setFlags(viewFlagValues, viewFlagMasks);
}
// ...
}
void setFlags(int flags, int mask) {
if ((changed & VISIBILITY_MASK) != 0) {
// ...
dispatchVisibilityChanged(this, newVisibility);
// ...
}
}
protected void dispatchVisibilityChanged(@NonNull View changedView,
@Visibility int visibility) {
onVisibilityChanged(changedView, visibility);
}
在构造方法中,调用 setFlags,然后在 setFlags 调用 dispatchVisibilityChanged,所以就会出现上诉问题。
现在看 Android6.0.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// View Android6.0.1_r67 https://cs.android.com/android/platform/superproject/+/android-6.0.1_r67:frameworks/base/core/java/android/view/View.java;l=10646;bpv=0;bpt=0
void setFlags(int flags, int mask) {
if ((changed & VISIBILITY_MASK) != 0) {
// ...
if (mAttachInfo != null) {
dispatchVisibilityChanged(this, newVisibility);
notifySubtreeAccessibilityStateChangedIfNeeded();
}
}
}
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
// Send onVisibilityChanged directly instead of dispatchVisibilityChanged.
// As all views in the subtree will already receive dispatchAttachedToWindow
// traversing the subtree again here is not desired.
onVisibilityChanged(this, visibility);
}
在 setFlags 回调 onVisibilityChanged 之前会判断 mAttachInfo 是否为空,而 mAttachInfo 赋值的时机是该 View 被添加到窗口,即绘制第一帧时,且赋值后会回调 onAttachedToWindow,在 dispatchAttachedToWindow 才会回调 onVisibilityChanged。
可见在 6.0,谷歌官方已经修复了这个可能导致开发者使用时崩溃的设计不合理的问题:不应该在构造方法中调用一个可被重写的方法。
问题解决
- 未 attach 的不执行逻辑
1
2
3
4
5
6
7
override fun onVisibilityChanged(changedView: View, visibility: Int) {
super.onVisibilityChanged(changedView, visibility)
if (!isAttachedToWindow) {
return
}
...
}
- 判空处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GiftBox extends FrameLayout implements ViewLifecycleOwner {
private ViewLifecycleOwnerDelegate mViewLifecycleOwnerDelegate;
private ViewLifecycleOwnerDelegate getViewLifecycleOwnerDelegate() {
if (mViewLifecycleOwnerDelegate == null) {
mViewLifecycleOwnerDelegate = new ViewLifecycleOwnerDelegate(this);
}
return mViewLifecycleOwnerDelegate;
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
getViewLifecycleOwnerDelegate().onVisibilityChanged(visibility);
}
}
本文由作者按照 CC BY 4.0 进行授权