文章

线上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)

系统版本分布和设备

a3uuz

f8ipa

从系统版本分布可得知,这大概率是一个 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 值的变更不是原子操作,可能出现不一致的情况,导致数组越界了。

Android9 Typeface create

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 进行授权