线上crash案例
线上crash案例
版本兼容 Crash 问题
Toast 问题
Toast 在 Android 7.x 崩溃
Typeface.create ArrayIndexOutOfBoundsException
错误堆栈
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
45
46
Caused by java.lang.ArrayIndexOutOfBoundsException: length=3; index=3
at android.util.ContainerHelpers.binarySearch(ContainerHelpers.java:47)
at android.util.LongSparseArray.get(LongSparseArray.java:113)
at android.util.LongSparseArray.get(LongSparseArray.java:104)
at android.graphics.Typeface.create(Typeface.java:744)
at android.graphics.Typeface.create(Typeface.java:710)
at android.widget.TextView.setTypefaceFromAttrs(TextView.java:2151)
at android.widget.TextView.<init>(TextView.java:1657)
at android.widget.Button.<init>(Button.java:166)
at android.widget.Button.<init>(Button.java:141)
at androidx.appcompat.widget.AppCompatButton.<init>(AppCompatButton.java:80)
at androidx.appcompat.widget.AppCompatButton.<init>(AppCompatButton.java:75)
at androidx.appcompat.app.AppCompatViewInflater.createButton(AppCompatViewInflater.java:211)
at androidx.appcompat.app.AppCompatViewInflater.createView(AppCompatViewInflater.java:129)
at androidx.appcompat.app.AppCompatDelegateImpl.createView(AppCompatDelegateImpl.java:1565)
at androidx.appcompat.app.AppCompatDelegateImpl.onCreateView(AppCompatDelegateImpl.java:1616)
at android.view.LayoutInflater$FactoryMerger.onCreateView(LayoutInflater.java:189)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:783)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:741)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:874)
at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:835)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:877)
at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:835)
at android.view.LayoutInflater.inflate(LayoutInflater.java:515)
at android.view.LayoutInflater.inflate(LayoutInflater.java:423)
at androidx.databinding.DataBindingUtil.inflate(DataBindingUtil.java:126)
at androidx.databinding.ViewDataBinding.inflateInternal(ViewDataBinding.java:1409)
at com.xxx.xxx.databinding.SiGuideDialogDefaultSettingBinding.inflate(SiGuideDialogDefaultSettingBinding.java:91)
at com.xxx.xxx.databinding.SiGuideDialogDefaultSettingBinding.inflate(SiGuideDialogDefaultSettingBinding.java:77)
at com.xxx.xxx.FirstInstallConfirmDefaultDialog.getView(FirstInstallConfirmDefaultDialog.kt:63)
at com.xxx.base.uicomponent.dialog.BaseBottomSheetDialog.onCreateView(BaseBottomSheetDialog.kt:46)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:3104)
at androidx.fragment.app.DialogFragment.performCreateView(DialogFragment.java:510)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:524)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1890)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1814)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1751)
at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:538)
at android.os.Handler.handleCallback(Handler.java:795)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:166)
at android.app.ActivityThread.main(ActivityThread.java:6861)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:450)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
系统版本分布和设备
从系统版本分布可得知,这大概率是一个 Android8.0 及以下的系统 bug
分析
从堆栈可以看出来,在 TextView 的 setTypefaceFromAttrs 中调用了 Typeface create(String familyName, @Style int style)
1
2
3
4
5
6
7
8
9
private void setTypefaceFromAttrs(@Nullable Typeface typeface, @Nullable String familyName,
@XMLTypefaceAttr int typefaceIndex, @Typeface.Style int style,
@IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int weight) {
if (typeface == null && familyName != null) {
// Lookup normal Typeface from system font map.
final Typeface normalTypeface = Typeface.create(familyName, Typeface.NORMAL);
}
// ...
}
下面我们从 Android8 和 Android9 来分析一下:
Android8 Typeface
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// TODO: Unify with Typeface.sTypefaceCache.
@GuardedBy("sLock")
private static final LongSparseArray<SparseArray<Typeface>> sTypefaceCache = new LongSparseArray<>(3);
public static Typeface create(String familyName, int style) {
if (sSystemFontMap != null) {
return create(sSystemFontMap.get(familyName), style);
}
return null;
}
public static Typeface create(Typeface family, int style) {
if (style < 0 || style > 3) {
style = 0;
}
long ni = 0;
if (family != null) {
// Return early if we're asked for the same face/style
if (family.mStyle == style) {
return family;
}
ni = family.native_instance;
}
Typeface typeface;
SparseArray<Typeface> styles = sTypefaceCache.get(ni);
if (styles != null) {
typeface = styles.get(style);
if (typeface != null) {
return typeface;
}
}
typeface = new Typeface(nativeCreateFromTypeface(ni, style));
if (styles == null) {
styles = new SparseArray<Typeface>(4);
sTypefaceCache.put(ni, styles);
}
styles.put(style, typeface);
return typeface;
}
// LongSparseArray
public E get(long key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}
static int binarySearch(long[] array, int size, long value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final long midVal = array[mid];
if (midVal < value) {
lo = mid + 1;
} else if (midVal > value) {
hi = mid - 1;
} else {
return mid; // value found
}
}
return ~lo; // value not present
}
从报错堆栈来看,是在 LongSparseArray 进行 get 时,数组越界了,那么我们可以大概猜测的是在 22 行 sTypefaceCache.get(ni);
导致的,而 sTypefaceCache
是一个初始最大容量为 3 的 LongSparseArray。
在操作 LongSparseArray 有多线程操作,导致 mKeys, mSize 值的变更不是原子操作,可能出现不一致的情况,导致数组越界了。
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
private static final Object sStyledCacheLock = new Object();
/**
* Cache for Typeface objects for weight variant. Currently max size is 3.
*/
@GuardedBy("sWeightCacheLock")
private static final LongSparseArray<SparseArray<Typeface>> sWeightTypefaceCache =
new LongSparseArray<>(3);
public static Typeface create(String familyName, @Style int style) {
return create(sSystemFontMap.get(familyName), style);
}
public static Typeface create(Typeface family, @Style int style) {
if ((style & ~STYLE_MASK) != 0) {
style = NORMAL;
}
if (family == null) {
family = sDefaultTypeface;
}
// Return early if we're asked for the same face/style
if (family.mStyle == style) {
return family;
}
final long ni = family.native_instance;
Typeface typeface;
synchronized (sStyledCacheLock) {
SparseArray<Typeface> styles = sStyledTypefaceCache.get(ni);
if (styles == null) {
styles = new SparseArray<Typeface>(4);
sStyledTypefaceCache.put(ni, styles);
} else {
typeface = styles.get(style);
if (typeface != null) {
return typeface;
}
}
typeface = new Typeface(nativeCreateFromTypeface(ni, style));
styles.put(style, typeface);
}
return typeface;
}
结论
- Android8 的版本在读写
sTypefaceCache
的时候没有加锁,而 sTypefaceCache 是一个静态变量,所有线程通过 Typeface.create 创建的 Typeface 会被缓存在 sTypefaceCache,如果有多线程访问的话,会有多线程安全问题 - Android9 的版本就用
sStyledCacheLock
对象锁加锁了 - 而项目中用到异步 inflate,如果设置加粗字体,会在子线程设置 Typeface,在 Android8 及以下可能会导致线程安全问题,多线程访问了 sTypefaceCache,多线程访问了就数组越界了?
本文由作者按照 CC BY 4.0 进行授权