0%

介绍

TransactionTooLargeException 表示在 RPC 调用中传输的数据过大(超过 1MB), 导致 RPC 调用失败.

TooLargeTool 是一款可以方便打开 Bundle 大小的工具, 可以借鉴里面的代码实现.

背景

除了常见的 Bundle 传输数据过大导致 startActivity 失败外, 经常还见到另一个堆栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Caused by:
android.os.TransactionTooLargeException:data parcel size 942884 bytes
android.os.BinderProxy.transactNative(Native Method)
android.os.BinderProxy.transact(BinderProxy.java:621)
android.app.IActivityClientController$Stub$Proxy.activityStopped(IActivityClientController.java:1459)
android.app.ActivityClient.activityStopped(ActivityClient.java:112) android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:135)
android.os.Handler.handleCallback(Handler.java:958)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loopOnce(Looper.java:224)
android.os.Looper.loop(Looper.java:318)
android.app.ActivityThread.main(ActivityThread.java:8669)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:561)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1013)

同时 App 为单 Activity 多 Fragment 架构

复现路径

我们找到了一个复现路径:

  1. 同一个Activity打开多个 Fragment, 尽量不少于 20 个
  2. App 退后台
  3. 此时可以在 Logcat 看到 TransactionTooLargeException

排查过程

  1. 查看代码, 退后台时会调用 Fragment 的 onSaveInstanceState(Bundle) , 这是用来保存状态的方法, 尝试在 Fragment 的 onSaveInstanceState 方法中打印 Bundle 的大小, 结果只有大概 1kb .

  2. 除了 Fragment 的 `onSaveInstanceState ,还有 Activity 的 onSaveInstanceState 方法, 打印出来. 两个 Fragment 大约有 100kb, 如果再开一个 Fragment, 还会增加 70kb. 以此类推, 大约16个 Fragment, 就会超过 1MB 的限制. 所以原因就是** Activity 调用 onSaveInstanceState 时 Bundle 太大了**.

    1
    2
    3
    4
    5
    6
    activity onSaveInstanceState: com.example.activity.MainActivity@7db60c7 size = Bundle62666159 contains 5 keys and measures 108.3 KB when serialized as a Parcel
    * com.google.app_measurement.screen_service = 0.2 KB
    * android:viewHierarchyState = 0.3 KB
    * androidx.lifecycle.BundlableSavedStateRegistry.key = 107.3 KB
    * android:lastAutofillId = 0.1 KB
    * android:fragments = 0.4 KB bundle size 5

    Bundle 中什么东西占用了空间

    通过查看源码和结合工具打印, Bundle 本质也是 Key-Value 的形式, 我们可以一层一层解析 Bundle 中的数据和大小.

  3. androidx.lifecycle.BundlableSavedStateRegistry.key 存放的是 Bundle, 里面占用大小最大的是 android:support:fragments 这个 key 存放的 Bundle.
    源码:

    日志:

    1
    2
    3
    4
    5
    6
    androidx.lifecycle.BundlableSavedStateRegistry.key size = 101 KB , map size 5
    key: androidx:appcompat size = 4 B
    key: android:support:lifecycle size = 4 B
    key: androidx.lifecycle.internal.SavedStateHandlesProvider size = 4 B
    key: android:support:activity-result size = 7.4 KB bundle
    key: android:support:fragments size = 93 KB bundle map size 3
  4. android:support:fragments key 中存放的是 Bundle, 里面是每个 Fragment 的 FragmentState 对象.
    源码:

    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
    ​``` FragmentManager
    public abstract class FragmentManager implements FragmentResultOwner {
    static final String SAVED_STATE_TAG = "android:support:fragments";

    // 找到 android:support:fragments 的 key
    SavedStateRegistry registry =
    ((SavedStateRegistryOwner) mHost).getSavedStateRegistry();
    registry.registerSavedStateProvider(SAVED_STATE_TAG, () -> {
    return saveAllStateInternal();
    }
    );

    // 找到 fragment_ 的key
    static final String FRAGMENT_STATE_TAG = "state";
    static final String FRAGMENT_NAME_PREFIX = "fragment_";
    private final FragmentStore mFragmentStore = new FragmentStore();

    // saveAllStateInternal() 方法
    @NonNull
    Bundle saveAllStateInternal() {
    ...
    // First save all active fragments.
    ArrayList<String> active = mFragmentStore.saveActiveFragments();

    ...
    for (FragmentState state : savedState) {
    Bundle fragmentBundle = new Bundle();
    fragmentBundle.putParcelable(FRAGMENT_STATE_TAG, state);
    bundle.putBundle(FRAGMENT_NAME_PREFIX + state.mWho, fragmentBundle);
    }

    return bundle;
    }
    }

    // FragmentStore
    class FragmentStore {
    private final HashMap<String, FragmentState> mSavedState = new HashMap<>();

    @NonNull
    ArrayList<FragmentState> getAllSavedState() {
    return new ArrayList<>(mSavedState.values());
    }
    }

    日志:

    1
    2
    3
    4
    android:support:fragments size = 93 KB bundle map size 3
    key: fragment_cc246ae6-d05a-44c1-9a07-e6b6e466cd03 size = 40 KB
    key: fragment_98067ccf-09ad-415b-b937-bb79c097db81 size = 52 KB

  5. FragmentState 可以直接在源码中找到, 由于其他都是基础类型, 最大的怀疑对象只有 mSavedFragmentState.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    final class FragmentState implements Parcelable {
    final String mClassName;
    final String mWho;
    final boolean mFromLayout;
    final int mFragmentId;
    final int mContainerId;
    final String mTag;
    final boolean mRetainInstance;
    final boolean mRemoving;
    final boolean mDetached;
    final Bundle mArguments;
    final boolean mHidden;
    final int mMaxLifecycleState;

    Bundle mSavedFragmentState;
    }

    尝试反射打印一下大小也能印证:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    name: mArguments              size 4 B
    name: mClassName size 49 B
    name: mContainerId size 82 B
    name: mDetached size 48 B
    name: mFragmentId size 82 B
    name: mFromLayout size 48 B
    name: mHidden size 48 B
    name: mMaxLifecycleState size 82 B
    name: mRemoving size 48 B
    name: mRetainInstance size 48 B
    name: mSavedFragmentState size 40 KB
  6. mSavedFragmentState 因为也是 Bundle, 可以打印里面的 value 的大小.

    1
    2
    3
    4
    5
    6
    7
    onSaveInstanceState of: Bundle121352603 contains 6 keys and measures 57.4 KB 
    * fragmentation_arg_container = 0.1 KB
    * android:support:fragments = 16.3 KB
    * fragmentation_invisible_when_leave = 0.1 KB
    * fragmentation_state_save_animator = 0.2 KB
    * androidx.lifecycle.BundlableSavedStateRegistry.key = 0.2 KB
    * android:view_state = 40.5 KB

    源码可以看到 android:view_state 就是一个 SparseParcelableArray

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class FragmentStateManager {
    private static final String VIEW_STATE_TAG = "android:view_state";

    private Bundle saveBasicState() {
    Bundle result = new Bundle();
    // Fragment 保存状态
    mFragment.performSaveInstanceState(result);
    ...
    if (mFragment.mSavedViewState != null) {
    if (result == null) {
    result = new Bundle();
    }
    result.putSparseParcelableArray(
    VIEW_STATE_TAG, mFragment.mSavedViewState);
    }
    ...
    return result;
    }
    }
  7. 打印 android:view_state , Array 中存放的数据比较多

    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
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    index: 9 value: CompoundButton.SavedState{5d3a096 checked=false} size 212 B
    index: 10 value: CompoundButton.SavedState{d4417 checked=false} size 212 B
    index: 11 value: CompoundButton.SavedState{12f3004 checked=false} size 212 B
    index: 12 value: CompoundButton.SavedState{8d0d9ed checked=false} size 212 B
    index: 13 value: CompoundButton.SavedState{da13022 checked=false} size 212 B
    index: 14 value: CompoundButton.SavedState{6980ab3 checked=false} size 212 B
    index: 15 value: CompoundButton.SavedState{b714870 checked=false} size 212 B
    index: 16 value: CompoundButton.SavedState{9ee27e9 checked=false} size 212 B
    index: 17 value: CompoundButton.SavedState{1c62c6e checked=false} size 212 B
    index: 18 value: CompoundButton.SavedState{dfe9f0f checked=false} size 212 B
    index: 19 value: CompoundButton.SavedState{e7a7b9c checked=false} size 212 B
    index: 20 value: CompoundButton.SavedState{27859a5 checked=false} size 212 B
    index: 21 value: CompoundButton.SavedState{32ca17a checked=false} size 212 B
    index: 22 value: CompoundButton.SavedState{1901d2b checked=false} size 212 B
    index: 23 value: CompoundButton.SavedState{d81b588 checked=false} size 212 B
    index: 24 value: CompoundButton.SavedState{4b9eb21 checked=false} size 212 B
    index: 25 value: CompoundButton.SavedState{5535b46 checked=false} size 212 B
    index: 26 value: CompoundButton.SavedState{8716107 checked=false} size 212 B
    index: 27 value: CompoundButton.SavedState{640a234 checked=false} size 212 B
    index: 28 value: CompoundButton.SavedState{329185d checked=false} size 212 B
    index: 29 value: CompoundButton.SavedState{2d9e5d2 checked=false} size 212 B
    index: 30 value: CompoundButton.SavedState{d3906a3 checked=false} size 212 B
    index: 31 value: CompoundButton.SavedState{65fada0 checked=false} size 212 B
    index: 32 value: CompoundButton.SavedState{763dd59 checked=false} size 212 B
    index: 33 value: CompoundButton.SavedState{ecc8d1e checked=false} size 212 B
    index: 34 value: CompoundButton.SavedState{34b69ff checked=false} size 212 B
    index: 35 value: CompoundButton.SavedState{ba203cc checked=false} size 212 B
    index: 36 value: CompoundButton.SavedState{cebf615 checked=false} size 212 B
    index: 37 value: CompoundButton.SavedState{eb05d2a checked=false} size 212 B
    index: 38 value: CompoundButton.SavedState{5f6a71b checked=false} size 212 B
    index: 39 value: CompoundButton.SavedState{ad190b8 checked=false} size 212 B
    index: 40 value: CompoundButton.SavedState{ba2de91 checked=false} size 212 B
    index: 41 value: CompoundButton.SavedState{94f21f6 checked=false} size 212 B
    index: 42 value: CompoundButton.SavedState{d4e99f7 checked=false} size 212 B
    index: 43 value: CompoundButton.SavedState{96b0064 checked=false} size 212 B
    index: 44 value: CompoundButton.SavedState{e05d2cd checked=false} size 212 B
    index: 45 value: CompoundButton.SavedState{6436782 checked=false} size 212 B
    index: 46 value: CompoundButton.SavedState{4c8de93 checked=false} size 212 B
    index: 47 value: CompoundButton.SavedState{609bed0 checked=false} size 212 B
    index: 48 value: CompoundButton.SavedState{f29cec9 checked=false} size 212 B
    index: 49 value: CompoundButton.SavedState{a4479ce checked=false} size 212 B
    index: 50 value: CompoundButton.SavedState{f98d0ef checked=false} size 212 B
    index: 51 value: CompoundButton.SavedState{b93f7fc checked=false} size 212 B
    index: 52 value: CompoundButton.SavedState{c778e85 checked=true} size 212 B
    index: 53 value: CompoundButton.SavedState{c3264da checked=false} size 212 B
    index: 54 value: CompoundButton.SavedState{6cb8d0b checked=false} size 212 B
    index: 55 value: CompoundButton.SavedState{b2697e8 checked=false} size 212 B
    index: 56 value: CompoundButton.SavedState{7278e01 checked=false} size 212 B
    index: 57 value: CompoundButton.SavedState{ee1f4a6 checked=false} size 212 B
    index: 58 value: CompoundButton.SavedState{d23eee7 checked=false} size 212 B
    index: 59 value: CompoundButton.SavedState{8c14a94 checked=false} size 212 B
    index: 60 value: CompoundButton.SavedState{17e093d checked=false} size 212 B
    index: 61 value: CompoundButton.SavedState{ba8b532 checked=false} size 212 B
    index: 62 value: CompoundButton.SavedState{4b69283 checked=false} size 212 B
    index: 63 value: CompoundButton.SavedState{6b27c00 checked=false} size 212 B
    index: 64 value: CompoundButton.SavedState{3c6fc39 checked=false} size 212 B
    index: 65 value: CompoundButton.SavedState{9a8f27e checked=false} size 212 B
    index: 66 value: CompoundButton.SavedState{245d3df checked=false} size 212 B
    index: 67 value: CompoundButton.SavedState{bc3582c checked=false} size 212 B
    index: 68 value: CompoundButton.SavedState{51222f5 checked=false} size 212 B
    index: 69 value: CompoundButton.SavedState{dddb88a checked=false} size 212 B
    index: 70 value: CompoundButton.SavedState{a5dcefb checked=false} size 212 B
    index: 71 value: CompoundButton.SavedState{23cb18 checked=false} size 212 B
    index: 72 value: CompoundButton.SavedState{3aef971 checked=false} size 212 B
    index: 73 value: CompoundButton.SavedState{fe6d356 checked=false} size 212 B
    index: 74 value: CompoundButton.SavedState{4305fd7 checked=false} size 212 B
    index: 75 value: CompoundButton.SavedState{d1680c4 checked=false} size 212 B
    index: 76 value: CompoundButton.SavedState{968bbad checked=false} size 212 B
    index: 77 value: CompoundButton.SavedState{f94cee2 checked=false} size 212 B
    index: 78 value: CompoundButton.SavedState{e312273 checked=false} size 212 B
    index: 79 value: CompoundButton.SavedState{b5ce530 checked=false} size 212 B
    index: 80 value: CompoundButton.SavedState{78265a9 checked=false} size 212 B
    index: 81 value: CompoundButton.SavedState{734f72e checked=false} size 212 B
    index: 82 value: CompoundButton.SavedState{7172cf checked=false} size 212 B
    index: 83 value: CompoundButton.SavedState{c63245c checked=false} size 212 B
    index: 84 value: CompoundButton.SavedState{672b365 checked=false} size 212 B
    index: 85 value: CompoundButton.SavedState{69d583a checked=false} size 212 B
    index: 86 value: CompoundButton.SavedState{8bc6ceb checked=true} size 212 B
    index: 87 value: CompoundButton.SavedState{a2c2a48 checked=false} size 212 B
    index: 88 value: CompoundButton.SavedState{56020e1 checked=false} size 212 B
    index: 89 value: CompoundButton.SavedState{2f8be06 checked=false} size 212 B
    index: 90 value: CompoundButton.SavedState{c72ecc7 checked=false} size 212 B
    index: 91 value: CompoundButton.SavedState{9fda2f4 checked=false} size 212 B
    index: 92 value: CompoundButton.SavedState{55cea1d checked=false} size 212 B
    index: 93 value: CompoundButton.SavedState{752b492 checked=false} size 212 B
    index: 94 value: CompoundButton.SavedState{c278e63 checked=false} size 212 B
    index: 95 value: CompoundButton.SavedState{dcbfa60 checked=false} size 212 B
    index: 96 value: CompoundButton.SavedState{c630b19 checked=false} size 212 B
    index: 97 value: CompoundButton.SavedState{1e387de checked=false} size 212 B
    index: 98 value: CompoundButton.SavedState{4faadbf checked=false} size 212 B
    index: 99 value: CompoundButton.SavedState{665c8c checked=false} size 212 B
    index: 100 value: CompoundButton.SavedState{c103fd5 checked=false} size 212 B
    index: 101 value: CompoundButton.SavedState{a1c43ea checked=false} size 212 B
    index: 102 value: CompoundButton.SavedState{bb666db checked=false} size 212 B
    index: 103 value: CompoundButton.SavedState{862b578 checked=false} size 212 B
    index: 104 value: CompoundButton.SavedState{8220451 checked=false} size 212 B
    index: 105 value: CompoundButton.SavedState{b72b4b6 checked=false} size 212 B
    index: 106 value: CompoundButton.SavedState{daa95b7 checked=false} size 212 B
    index: 107 value: CompoundButton.SavedState{dc9b124 checked=false} size 212 B
    index: 108 value: CompoundButton.SavedState{8b1948d checked=false} size 212 B
    index: 109 value: CompoundButton.SavedState{ed6642 checked=false} size 212 B
    index: 110 value: CompoundButton.SavedState{348d653 checked=false} size 212 B
    index: 111 value: CompoundButton.SavedState{e82bb90 checked=false} size 212 B
    index: 112 value: CompoundButton.SavedState{42fec89 checked=false} size 212 B
    index: 113 value: CompoundButton.SavedState{d6fa48e checked=false} size 212 B
    index: 2131427585 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131427586 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131427590 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131427593 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131427598 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131427802 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131427846 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131427848 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131428063 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131428202 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131428413 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131428417 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131428708 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131428779 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131428825 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131428835 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131428906 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131428907 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131428909 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131428910 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131428911 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131429227 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131429229 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131429267 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131429275 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131429278 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131429311 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131429320 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131429738 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131429739 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131429740 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131429898 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131429900 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430063 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430064 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430125 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430163 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430164 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430167 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430169 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430170 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430171 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430172 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430345 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430360 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430362 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430428 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430442 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430607 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430664 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430668 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430669 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430703 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430842 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430851 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430853 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430905 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430908 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430909 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131430910 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131431039 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131431050 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131431070 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131431386 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131431572 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131431609 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131432508 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131432857 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131432984 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131433180 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131433181 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131433191 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131433196 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131433204 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131433205 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131433790 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131433829 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434147 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434173 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434226 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434416 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434417 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434418 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434419 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434420 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434421 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434422 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434429 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434430 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434431 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434432 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434433 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434434 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434435 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434443 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434444 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434445 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434446 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434447 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434448 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434449 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434450 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434451 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434468 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434469 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434810 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434828 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434835 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434848 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131434968 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131435021 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131435050 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131435129 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131435286 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131435294 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131435455 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131435466 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131435526 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131435880 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131435998 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131436152 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131436176 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131436177 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131436178 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131436680 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131436789 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131436791 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131436929 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131436931 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131437119 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131437153 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131437154 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131437155 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131437837 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131438026 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131438505 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131438874 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439071 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439124 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439161 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439162 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439163 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439165 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439190 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439210 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439213 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439246 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439648 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439692 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439741 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439790 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439839 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439840 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439962 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439964 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439970 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439973 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439974 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439975 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439976 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439977 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439978 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439979 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439980 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439981 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439982 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439983 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439984 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439985 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131439986 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131440068 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131440074 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131440103 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131440153 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131440171 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131440239 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131440562 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131440686 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131441148 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131441578 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131441589 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131441992 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131442003 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131442375 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131442378 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131442382 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131442383 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131442598 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131442726 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131442795 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131443036 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131443037 value: android.view.AbsSavedState$1@8084af size 68 B
    index: 2131442292 value: androidx.recyclerview.widget.RecyclerView$SavedState@203fa43 size 316 B
    index: 2131439968 value: androidx.recyclerview.widget.RecyclerView$SavedState@a7dbafd size 316 B
    index: 2131432685 value: androidx.recyclerview.widget.RecyclerView$SavedState@d21c845 size 316 B
    index: 2131435295 value: androidx.recyclerview.widget.RecyclerView$SavedState@f9ba3c1 size 316 B
    index: 2131435292 value: androidx.coordinatorlayout.widget.CoordinatorLayout$SavedState@8aa6ca8 size 212 B
    index: 2131429908 value: FragmentPager.SavedState{d8000bc position=0} size 176 B
    index: 2131435148 value: FragmentPager.SavedState{edabccb position=0} size 176 B
    index: 2131435999 value: FragmentPager.SavedState{96fb766 position=0} size 176 B
    index: 2131440220 value: androidx.appcompat.widget.Toolbar$SavedState@32fe3f2 size 172 B
    index: 2131442599 value: HorizontalScrollView.SavedState{4c428c0 scrollPosition=0} size 128 B
    index: 2131435051 value: HorizontalScrollView.SavedState{cc57b9a scrollPosition=0} size 128 B
    index: 2131436299 value: android.widget.ProgressBar$SavedState@d565aa7 size 112 B
    index: 2131436874 value: android.widget.ProgressBar$SavedState@18dab54 size 112 B

    一共 310 个对象, 全部加起来的话
    105 * 212 + 192 * 68 + 4 * 316 + 212 + 3 * 176 + 172 + 128 * 2 + 112 * 2 = 37972 / 1024 = 37kb

原因总结

  1. View 会保存自己的状态, 用于状态恢复.
  2. Fragment 会保存所有 View 的状态(一个 Fragment 大约40k).
  3. Activity 会保存 Activity 下 attach 的所有 Fragment 的状态.
  4. 如果 Activity 下嵌套 Fragment 过多, 保存状态时容易超过 1MB.

解决方法

  1. 不保存 Fragment 的 view 状态, 也不做恢复

    即清除 Bundle 中数据

    注意这里由于 Fragment 先调用 onSaveInstanceState 后保存 view_state, 所以我们要在 Activity 的 onSaveInstanceState 中清除 Bundle 中的数据

    大概代码如下:

    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
       class BundleClipHelper {

    companion object {

    private const val TAG = "BundleClipHelper"

    /**
    * Bundle 中这两个 key 比较占内存, 暂时只删除这两个 key
    */
    private const val BUNDLE_KEY_FRAGMENTS = "android:support:fragments"
    private const val BUNDLE_KEY_VIEWS = "android:view_state"

    }

    /**
    * 待清理 Bundle 列表
    */
    private val pendingClearBundleList = mutableListOf<Bundle>()

    /**
    * fragment 生命周期回调
    * 在 Fragment 调用 onSaveInstanceState 后记录需要清理的 Bundle
    */
    private val fragmentLifecycleCallbacks: FragmentManager.FragmentLifecycleCallbacks =
    object : FragmentManager.FragmentLifecycleCallbacks() {
    override fun onFragmentSaveInstanceState(fm: FragmentManager, f: Fragment, outState: Bundle) {
    super.onFragmentSaveInstanceState(fm, f, outState)
    pendingClearBundleList.add(outState)
    }
    }

    /**
    * 注册 fragment 生命周期回调
    */
    fun register(activity: FragmentActivity?) {
    activity?.supportFragmentManager?.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
    }

    /**
    * 反注册 fragment 生命周期回调
    */
    fun unRegister(activity: FragmentActivity?) {
    activity?.supportFragmentManager?.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks)
    }

    /**
    * 裁剪 Bundle
    * 在 Activity 的 onSaveInstanceState 中调用
    */
    fun clipBundle(activity: Activity<*>?) {
    if (activity == null || !activity.needClipBundleWhenSaveInstanceState()) {
    pendingClearBundleList.clear()
    return
    }
    if (pendingClearBundleList.isNotEmpty()) {
    Log.i(TAG, "need clear fragment size: " + pendingClearBundleList.size)
    pendingClearBundleList.forEach {
    it.remove(BUNDLE_KEY_FRAGMENTS)
    it.remove(BUNDLE_KEY_VIEWS)
    }
    pendingClearBundleList.clear()
    }
    }
    }
  2. 保存 Bundle 到内存, 并在 Activity 创建前替换, 缺点是不适用于首页(首页重启进程的话内存缓存也会丢失). 可以参考 掘金

  3. 改为单 Activity 框架

需求场景

通过固定的 url 加载图片, 在 url 不变的情况下, 服务器可能会更新 url 对应的资源, 客户端需要能够及时检测到图片的更新并加载最新资源.

服务器能够提供的交互

  1. 在正常情况下, 加载成功后, 返回 200 成功, Response Body 返回对应的图片资源, 同时 Response Header 中带上 etag 表示该 url 对应资源的特征值. 如:

    1
    etag:"bd6d0ff25778318b238ac530cb147247"
  2. 后续的请求, Request Header 带上 if-none-match 给服务器检查是否更新, 如果图片资源未更新, 直接返回 304, 否则正常返回.

    1
    2
    3
    4
    5
    6
    7
    // Request Header
    if-none-match: "bd6d0ff25778318b238ac530cb147247"
    //

    // Response (未改变)
    Statue Code : 304
    //

客户端实现

客户端需要

  1. 本地缓存 url 对应的 etag, 同时在后续请求的时候带上.
  2. 加载图片的时候需要支持给 url 设置 Header
  3. 在图片加载成功后, 如果是远程 url 也需要读取 Response 的 Header.

由于客户端一般都有通用的图片加载库, 所以 2,3 点需要图片库支持

本地缓存 url 对应的 etag

这个使用通用的 Key-Value 缓存都能实现, 可以用 SharedPrference, sqlite, MMKV 或 DataStore 都能实现, 我比较推荐用 MMKV 的形式, 最主要的特点是不需要切线程, 同时也是属于能够接受丢失的数据.

图片库的支持

这里以 Glide 举例, 常见的写法是

1
2
3
4
5
Glide.with()
.asDrawable()
.load(url)
.signature(etag)
.into()
  1. 设置 Header, 这个比较简单, 可以通过自带的 GlideUrl 设置
    1
    2
    3
    4
    5
    val header = LazyHeaders.Builder()
    .addHeader("if-none-match", etag)
    .build()
    val glideUrl = GlideUrl(url, header)
    Glide.load(glideUrl)
  2. Glide 取出 Http Response Header 中的 etag.
    我这边想到的办法比较麻烦:
    1. 调整 Glide 的网络库实现为 OKHttp
    2. 给 Glide 设置 OKHttpClient 时添加自定义的 Interceptor , 然后就可以取出 Response 中的 etag.

但是这样有个问题, 就是 etag 无法与 Glide 的 Request 进行关联, 导致同一个 url 必定会有两次网络请求.

  1. 第一次加载, 由于没有 etag 缓存, signature 传入的 etag 为 null. 此时 Glide 图片以 url 作为缓存 key.
  2. 第一次加载后, response 中取出 etag , 记录与 url 的映射关系.
  3. 第二次加载, signature 传入 url 对应的 etag, 由于该 etag 缓存 key 未找到对应的缓存图片, 所以又会发起一次网络请求.

所以纯粹修改 Glide 图片库似乎不太好.

先发起一次网络请求

由于图片正常返回时, Http Body 就是所需加载的图片资源 byte, 所以可以先发起一次网络请求, 拿到 etag 后, 设置 signature 并使用 Glide.load(byte[]) 方法直接加载 Http 的 Body, 这样只有一次网络请求.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
// Error
}

override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
return
}
val eTag = response.header("etag") ?: url
Glide.with(getContext()).asDrawable()
.load(response.body?.bytes() ?: ByteArray(0)))
.signature(ObjectKey(etag))
.into()
}

但是这样测试之后, 发现有新的问题, 即第二次请求虽然 etag 都匹配传入了, 但是依然发起了网络请求. 排查之后发现 Glide.load(byte[]) 方法使用的 ByteArrayLoader 中定义的 DataSource 为 Local, 所以使用该方法就不会写入本地缓存, 第二次请求就不会命中缓存.

解决办法也比较简单, 可以仿照 ByteArrayLoader 写一个自定义的 Loader , 然后注册到 Glide 中, 注意替换 DataSource 为 Remote.

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* 主要用于直接加载 HttpBody
* 配合 ETag 使用
*/
class ETagByteModel(val eTag: String, val data: ByteArray): Key {

override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(eTag.toByteArray(Key.CHARSET))
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as ETagByteModel

if (eTag != other.eTag) return false

return true
}

override fun hashCode(): Int {
return eTag.hashCode()
}
}

/**
* 参考 ByteArrayLoader 实现
* 调整 DataSource 为 remote, 这样可以走缓存逻辑
*/
class ETagByteLoader<Data>(val converter: ByteArrayLoader.Converter<Data>) : ModelLoader<ETagByteModel, Data> {

override fun buildLoadData(model: ETagByteModel, width: Int, height: Int, options: Options): ModelLoader.LoadData<Data>? {
return ModelLoader.LoadData(model, Fetcher(model.data, converter))
}

override fun handles(model: ETagByteModel): Boolean {
return true
}

private class Fetcher<Data>
/**
* @param model We really ought to copy the model, but doing so can be hugely expensive and/or
* lead to OOMs. In practice it's unlikely that users would pass an array into Glide and
* then mutate it.
*/ internal constructor(private val model: ByteArray, private val converter: ByteArrayLoader.Converter<Data>) : DataFetcher<Data> {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in Data>) {
val result = converter.convert(model)
callback.onDataReady(result)
}

override fun cleanup() {
// Do nothing.
}

override fun cancel() {
// Do nothing.
}

override fun getDataClass(): Class<Data> {
return converter.dataClass
}

override fun getDataSource(): DataSource {
// 注意 DataSource 的值
return DataSource.REMOTE
}
}

}

/**
* 对应 InputStream 解析
*/
class StreamFactory : ModelLoaderFactory<ETagByteModel, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ETagByteModel, InputStream> {
return ETagByteLoader<InputStream>(
object : ByteArrayLoader.Converter<InputStream> {
override fun convert(model: ByteArray?): InputStream {
return ByteArrayInputStream(model)
}

override fun getDataClass(): Class<InputStream> {
return InputStream::class.java
}
})
}

override fun teardown() {
// Do nothing.
}
}

然后注册到 Glide

1
2
3
4
5
6
7
@GlideModule
public final class EtagGlideModule extends LibraryGlideModule {
@Override
public void registerComponents(Context context, Glide glide, Registry registry) {
registry.replace(ETagByteModel.class, InputStream.class, new StreamFactory());
}
}

然后加载的时候调用

1
2
3
4
5
val eTag = response.header("etag") ?: url
Glide.with(getContext()).asDrawable()
.load(EtagByteModel(etag, response.body?.bytes() ?: ByteArray(0)))
.signature(ObjectKey(etag))
.into()

这样即可以实现 etag 更新, 也可以利用到 Glide 的缓存, 对项目修改比较小.

最终流程

  1. 首先检查是否有 url 对应的 etag 缓存
    • 如果没有缓存, 先发起一次网络请求, 成功之后利用 Glide 设置 signature 加载 http body.
    • 如果有缓存
      1. 先通过Glide 设置 signature 加载 url
      2. 在 Glide 的成功回调里面带上 if-none-match 的 Header 发起网络请求, 用于检查服务器是否更新了图片.

预加载与缓存的坑

如果使用 Glide 的 preload 方法并且缓存了 preload 返回的 Drawable, 需要注意 Drawable 中的 Bitmap 对象在 preload 完成后会释放掉, 此时 Bitmap 会被 Glide 回收后复用给其他请求.
如果自己缓存了该 Bitmap, 会导致两种结果:

  1. Bitmap 错误展示了其他地方请求的结果, 即展示的图片与实际的 url 对不上.
  2. Bitmap 复用 Crash
    1
    2
    3
    4
    5
    6
    7
    8
    9
    java.lang.RuntimeException
    Canvas: trying to use a recycled bitmap android.graphics.Bitmap@111bacc
    android.graphics.BaseCanvas.throwIfCannotDraw(BaseCanvas.java:77)
    android.graphics.MiuiCanvas.throwIfCannotDraw(MiuiCanvas.java:329)
    android.graphics.RecordingCanvas.throwIfCannotDraw(RecordingCanvas.java:277)
    android.graphics.BaseRecordingCanvas.drawBitmap(BaseRecordingCanvas.java:88)
    android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:548)
    android.widget.ImageView.onDraw(ImageView.java:1434)
    ...
    解决办法是自己缓存的场景不要用 preload, preload 的实现也是 CustomTarget, 并且在 preload 成功后 clear 了资源. 所以我们使用 into CustomTarget 即可.
1
2
3
4
5
6
7
8
9
10
.into(object: CustomTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
// 保存 Drawable
}

override fun onLoadCleared(placeholder: Drawable?) {
// 释放 Drawable
}

})

Dialog 使用的常见坑.

  1. dismiss 错误堆栈
1
2
3
4
5
java.lang.IllegalArgumentException View=DecorView@984ba6d[MainActivity] not attached to window manager
android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:619)
android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:511)
android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:200)
android.app.Dialog.dismissDialog(Dialog.java:766)

这里查看堆栈是 DIalog dismiss 的堆栈, 但是一般看不到调用 dialog.dismiss 的业务堆栈.

主要是因为这种情况下一般是子线程调用了 dismiss, 但是 Dialog 的 dismiss 会有一个切线程的操作:

1
2
3
4
5
6
7
8
@Override
public void dismiss() {
if (Looper.myLooper() == mHandler.getLooper()) {
dismissDialog();
} else {
mHandler.post(mDismissAction);
}
}

其中 mHandler 对象是在 Dialog 创建的时候初始化的, 所以 Looper 也是和创建 Dialog 的时候的线程绑定的, 同时因为 Handler 是 private 的, 所以目前想到的 hook 方式是自己同步创建一个 Handler , 在调用 dismiss 时自己切线程, 这样就可以正确 try catch 这个 Exception.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyDialog(context: Context): Dialog(context) {

private val handler = Handler()

override fun dismiss() {
if (Looper.myLooper() == handler.looper) {
superDismiss()
} else {
handler.post { superDismiss() }
}
}

private fun superDismiss() {
try {
super.dismiss()
} catch (e: Exception) {
// catch success
}
}
}

ViewCacheExtension 作为 RecyclerView 中的开发者自定义缓存, 具有以下特点:

  1. 只有从缓存中取, 没有从缓存中读.
  2. 接口需要返回 View.
  3. 接口返回的 View 必须绑定了 ViewHolder .

就应用来说, 我觉得有两个合适的场景

Read more »

分区

根据 “Java 虚拟机规范”, Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域.

而其实一个 Java 程序可以理解为一个进程, 进程里面的资源共享即区分以下区域的私有和共享.

私有:

  • 程序计数器: 当前线程所执行的字节码的行号指示器.
  • 虚拟机栈(VM Stack): 每个方法在执行时会创建一个栈帧来存储局部变量表, 操作数栈, 动态链接, 方法出口等信息. 主要是局部变量表, 存储了各种基本数据类型和对象引用.
  • 本地方法栈(Native Method Stack): 对于 Native 方法的存储.

共享:

  • Java 堆: 几乎所有的对象示例都要在这里分配内存, 也是 GC 的主要管理区域.
  • 方法区: 存储虚拟机加载的类信息, 常量, 静态变量, JIT 编译之后的代码.

回收

GC 方案常见的是 引用计数法根搜索算法.

  • 引用记数法: 给对象添加一个引用计数器, 被引用时计数器 +1, 引用失效时计数器 -1. 如果任何时刻计数器都为0, 那就不可能再被使用, 就可以被回收. 因为引用计数法在循环引用时会导致对象无法回收, 所以目前被弃用了.
  • 可达性算法: 目前认为比较成熟的算法, 即从 GC Roots 的对象作为起点, 向下搜索对象, 走过的路径称为引用链, 如果某个对象到 GC Roots 之间没有任何引用链, 那认为这个对象是不可达的, 它就可以被回收.

GC Roots 有以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象.
  2. 方法区中类静态属性引用的对象.
  3. 方法区中常量引用的对象.
  4. 本地方法栈中 JNI 引用的对象.

对象引用

涉及到内存回收, Java中对对象的引用可以使用以下四种,分别为强引用,软引用,弱引用,虚引用。
参考地址.

  1. 强引用(StrongReference)

强引用是使用最普遍的引用,如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。**(默认的引用方式)**

  1. 软引用(SoftReference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存**(即不抛出OOM错误)**。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中.

  1. 弱引用(WeakReference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此 不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

  1. 虚引用(PhantomReference)

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。 使用虚引用只是为了能够在对象回收时收到通知.

ReferenceQueue的用法(以PhantomReference为例):
ReferenceQueue queue = new ReferenceQueue (); 
PhantomReference pr = new PhantomReference (object, queue);

名字 | 引用方式 | GC是否回收 | 是否 OOM
— | — | — | —
强引用 | 直接调用 | 否 | 是
软引用 | .get | 看内存情况 | 否
弱引用 | .get | 是 | 否
虚引用 | null | 是 | 否

回收

标记-清除算法

即回收算法分为”标记”和”清除”两个阶段, 先扫描一次标记所有需要回收的对象, 再扫描一次回收所有被标记地对象.

缺点:

  • 效率太低
  • 会产生大量不连续地内存碎片, 这些碎片会导致无法给大内存对象分配内存.

复制算法

将可用内存分为大小相等的两块, 每次只使用其中的一块. 回收时把存活地对象复制到另一块区域即可, 其他空间都可以清理掉.

缺点:

  • 可用内存直接变成一半了.

标记-整理算法

先扫描一次标记所有需要回收的对象, 再让所有的对象都向一端移动, 最后清理掉边界以外的内存.

分代回收

现在的商业虚拟机大部分都是分代回收算法, 分为新生代(young gen)和老生代(old gen), 其中 young gen 又分为 eden 区, from survior 和 to survior 区. 新生代的 GC 叫做 Minor GC, 老生代的 GC 叫做 Major GC / Full GC . 数据一开始会分配到 Eden 区(大对象直接进入 old gen), young gen 采用的是复制算法, 因为 young gen 的大部分数据都是回马上死亡的, 所以只需复制少部分存货的对象从 From survior 到 To survior. 当 young gen 的数据经历了几次 GC 后(默认15次), 它会从 young gen 移到 old gen. 而 old gen 采用的是标记-整理算法, 可以应用 old gen 中对象 100% 都存活的情况.

GC 日志

33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]

  • 33.125: Java 虚拟机启动以来经过的秒数.
  • GC 开头: 区分 GC 区域, 像这里的 DefNew 是 Serial 收集器的新生代区域, 还有 Full GC 表示全部 GC.
  • 3324K -> 152K(3712K): GC 前该内存区域的可用容量 -> GC 后该内存区域已使用容量(该内存区域总容量)
  • 0.0025925 secs: 该区域 GC 所占用的时间.
  • 3324K->152K(11904K): GC 前 Java 堆已使用容量 -> GC 后 Java 堆已使用容量(Java 堆总容量)

WorkManager-Guide&Tips

WorkManager 为了方便运行一些不着急的异步的后台任务而诞生. 大部分情况下, 只需要定义好自己想做的任务, 交给 WorkManager 去执行, 剩下就不用管了.

注意一下, 同样是后台线程, WorkManager 的重点在于保证就算 App 关掉之后后台任务也能够被执行. 而那种可以随着 App 退出而关闭的后台任务, 还是更适合使用 ThreadPools.

以前的实现方案

  1. Service: 这是最常见的需要后台运行的方案了. 对比来说, Service 有以下几个问题:

    • 可能会由于开发者的设置而疯狂运行, 这会导致手机电量被疯狂消耗. 对比之下 WorkManager 的同一个周期任务的最小间隔时间是15分钟.
    • targetSdkVersion 为 26 及以上的时候, 在不被允许创建后台服务的情况下, startService() 会抛出 IllegalStateException. 对比之下 WorkManager 会按照设定选择合适的时间运行.
  2. JobScheduler: 这个最关键的就是只有 Android 5.0 以上才能使用, 其实 WorkManager 在 5.0 以上也是用这个实现的.

  3. AlarmManager + BroadcastReceiver. 这个方案也是可以的, WorkManager 在 5.0 以下也是这样实现的, 只是封装了更好用的 API .

如果对更好用的 WorkManager 感兴趣, 就可以继续往下看了. 大概的介绍顺序是:

  1. 导入
  2. 一次性任务的使用
  3. 周期性任务的使用
  4. 任务如何取消
  5. 给任务加上约束条件
  6. 多个任务以特定顺序执行
  7. 相同任务的重复处理策略
  8. 任务的输入和输出
  9. 一些需要注意的点

以下代码都可以在 Demo 中找到.

导入

和其他 JetPack 的组件一样, 在 projectbuild.gradle 文件中添加 google() 源:

1
2
3
4
5
6
allprojects {
repositories {
google()
jcenter()
}
}

然后在 modulebuild.gradle 中添加 WorkManager 的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dependencies {
def work_version = "1.0.0-beta05"

// Java 依赖版本
implementation "android.arch.work:work-runtime:$work_version"

// Kotlin 依赖版本, 和上面的依赖二选一即可
implementation "android.arch.work:work-runtime-ktx:$work_version"

// 可选 RxJava2 支持
implementation "android.arch.work:work-rxjava2:$work_version"

// 可选 测试支持
androidTestImplementation "android.arch.work:work-testing:$work_version"
}

基本使用

使用起来就如上面所说, 首先你需要创建一个 任务 (Worker) , 然后丢给 WorkManager.

1
2
3
4
5
6
7
8
9
10
11
12
13
Kotlin
class TestWorker(context: Context, params: WorkerParameters)
: Worker(context, params) {

override fun doWork(): Result {

// 这里已经是后台线程了, 只需要实现自己的业务逻辑就好了
// return Result.retry(); 重试
// return Result.failure(); 不再重试
return Result.success()
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java
public class TestWorker extends Worker {

public TestWorker(Context context, WorkerParameters params) {
super(context, params);
}

@Override
public Result doWork() {

// 这里已经是后台线程了, 只需要实现自己的业务逻辑就好了
// return Result.retry(); 重试
// return Result.failure(); 不再重试
return Result.success();
}
}

Worker 里面只声明要实现的任务, 其他的约束条件要在 WorkRequest 中设置, 把 Worker 变成 WorkRequest. 再交给 WorkManager 去执行就好了.

一次性任务

1
2
3
Kotlin
val oneTimeWorker = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
WorkManager.getInstance().enqueue(oneTimeWorker)
1
2
3
4
Java
OneTimeWorkRequest oneTimeWorker =
new OneTimeWorkRequest.Builder(TestWorker.class).build();
WorkManager.getInstance().enqueue(oneTimeWorker);

就是这么简单, 接下来 Worker 就会在后台线程运行了.

周期性任务

周期性任务需要更加慎重一点. 开启之后如果不注意, 大部分情况下就会一直运行, 这可能带来很不好的用户体验.

设置周期性任务的时候, 需要设置 repeatInterval(重复区间)flexInterval(弹性区间) 参数, 配合注释说明:

1
2
3
4
5
[  弹性区间外  |  弹性区间内 (flex Interval) ][  弹性区间外  |  弹性区间内 (flex Interval) ]...
[ 任务不运行 | 任务可运行 ][ 任务不运行 | 任务可运行 ]...
\_________________________________________/\________________________________________/...
第一个区间 (repeat Interval) 第二个区间 (repeat Interval) ...(repeat)

repeatInterval 最小值是15分钟, 而 flexInterval 的最小值是5分钟, 如果 flexInterval 大于 repeatInterval, 也会被修改到和 repeatInterval 一样的值.

取消任务

但是从 API 可以看到, WorkManager 是将这个 Worker 入队了, 那既然是以队列维护的异步操作, 肯定会有重复的问题. WorkManager 默认的操作是遇到一样的 Worker 时, 新 Worker 会等旧 Worker 运行完再运行, 即顺序执行.

不过大部分情况下这都不是我们想要的模式, 所以在运行前最好取消相同的任务. 每个 Worker 都有一个唯一标识 UUID, 同时在构建 WorkRequest 的时候还可以添加任意个 Tag, 通过这两个标识都可以取消任务.

1
2
3
4
5
6
7
8
9
10
Kotlin
// UUID 方式
val workId: UUID = oneTimeWorker.getId()
WorkManager.getInstance().cancelWorkById(workId)

// Tag 方式
val oneTimeWorker = OneTimeWorkRequest.Builder(TestWorker::class.java)
.addTag("myTag")
.build()
WorkManager.getInstance().cancelAllWorkByTag("myTag")
1
2
3
4
5
6
7
8
9
10
Java
// UUID 方式
UUID workId = oneTimeWorker.getId();
WorkManager.getInstance().cancelWorkById(workId);

// Tag 方式
OneTimeWorkRequest myTask = new OneTimeWorkRequest.Builder(TestWorker.class)
.addTag("myTag")
.build()
WorkManager.getInstance().cancelAllWorkByTag("myTag")

取消相同的任务已经避免了系统资源不必要的消耗, 不过为了防止 API 的滥用, 还推荐给任务加上一些约束条件, 方便任务在系统资源没那么紧张的时候再执行:

加上约束

所有的约束 Constraints 都是由 Constraints.Builder() 来创建的, Builder 提供了以下的约束方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
Kotlin
// 设置网络类型
setRequiredNetworkType(networkType: NetworkType)
// 是否运行时电量不要太低
setRequiresBatteryNotLow(requiresBatteryNotLow: Boolean)
// 是否在充电时才运行
setRequiresCharging(requiresCharging: Boolean)
// 是否不太剩余存储空间过低时运行
setRequiresStorageNotLow(requiresStorageNotLow: Boolean)
// 是否在设备空闲时运行, 这个最低版本是 23
setRequiresDeviceIdle(requiresDeviceIdle: Boolean)
// 监听一个本地的 Uri, 第二个参数是否监听 Uri 的子节点. 在 Uri 的内容改变时运行任务, 最低版本是 24
addContentUriTrigger(uri: Uri, triggerForDescendants: Boolean)
1
2
3
4
5
6
7
8
9
10
11
12
13
Java
// 设置网络类型
setRequiredNetworkType(NetworkType networkType)
// 是否运行时电量不要太低
setRequiresBatteryNotLow(boolean requiresBatteryNotLow)
// 是否在充电时才运行
setRequiresStorageNotLow(boolean requiresStorageNotLow)
// 是否不太剩余存储空间过低时运行
setRequiresStorageNotLow(requiresStorageNotLow: Boolean)
// 是否在设备空闲时运行, 这个最低版本是 23
setRequiresDeviceIdle(boolean requiresDeviceIdle)
// 监听一个本地的 Uri, 第二个参数是否监听 Uri 的子节点. 在 Uri 的内容改变时运行任务, 最低版本是 24
addContentUriTrigger(Uri uri, boolean triggerForDescendants)

多个任务的执行顺序

WorkManager 提供了相应的 API 使任务可以使一个或多个 OneTimeWorkerRequest 按某个顺序执行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// A, B, C 就会按顺序执行, 如果全部返回成功或者某一个返回失败, 那该任务链就会结束.
WorkManager.getInstance()
.beginWith(workA)
.then(workB)
.then(workC)
.enqueue()

// A, B 一起运行, 虽然这2个的开始顺序不定, 但是 C 一定是在这2个运行后才运行.
WorkManager.getInstance()
.beginWith(Arrays.asList(workA, workB))
.then(workC)
.enqueue()

// B 一定会在 A 后面运行, D 也一定会在 C 后面运行, 但是 AB 与 CD 这两条链的运行顺序不定, 但是 E 一定是在 B 和 D 都结束后才运行.
val chain1 = WorkManager.getInstance()
.beginWith(workA)
.then(workB)
val chain2 = WorkManager.getInstance()
.beginWith(workC)
.then(workD)
val chain3 = WorkContinuation
.combine(Arrays.asList(chain1, chain2))
.then(workE)
chain3.enqueue()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// A, B, C 就会按顺序执行, 如果全部返回成功或者某一个返回失败, 那该任务链就会结束.
WorkManager.getInstance()
.beginWith(workA)
.then(workB)
.then(workC)
.enqueue();

// A, B 一起运行, 虽然这2个的开始顺序不定, 但是 C 一定是在这2个运行后才运行.
WorkManager.getInstance()
.beginWith(Arrays.asList(workA, workB))
.then(workC)
.enqueue();

// B 一定会在 A 后面运行, D 也一定会在 C 后面运行, 但是 AB 与 CD 这两条链的运行顺序不定, 但是 E 一定是在 B 和 D 都结束后才运行.
WorkContinuation chain1 = WorkManager.getInstance()
.beginWith(workA)
.then(workB);
WorkContinuation chain2 = WorkManager.getInstance()
.beginWith(workC)
.then(workD);
WorkContinuation chain3 = WorkContinuation
.combine(Arrays.asList(chain1, chain2))
.then(workE);
chain3.enqueue();

相同任务的重复策略

前面提到对于 Worker 来说, 可以通过 UUID 和 Tag 来保证其唯一性, 这样在需要的时候就可以避免任务重复执行. 但对于连续的任务链, 如果任务多了, 这样的方式会很繁琐. 于是, WorkerManager 也提供了相应的 API 来保证其唯一性.

1
2
3
4
Kotlin
beginUniqueWork(uniqueWorkName: String, existingWorkPolicy: ExistingWorkPolicy, work: OneTimeWorkRequest): WorkContinuation

beginUniqueWork(uniqueWorkName: String, existingWorkPolicy: ExistingWorkPolicy, work: List<OneTimeWorkRequest>): WorkContinuation
1
2
3
4
Java
WorkContinuation beginUniqueWork(String uniqueWorkName, ExistingWorkPolicy existingWorkPolicy, OneTimeWorkRequest work)

WorkContinuation beginUniqueWork(String uniqueWorkName, ExistingWorkPolicy existingWorkPolicy, List<OneTimeWorkRequest> work)

第一个参数就是这一个或者一系列 worker 的名字, 第二个参数就是重复时的操作, 有以下几种模式:

  • ExistingWorkPolicy.APPEND : 如果上一个任务处于等待或者未完成的状态, 则把当前任务添加到其任务链的后面. 这样它就在上一个任务执行完后执行.
  • ExistingWorkPolicy.KEEP : 如果上一个任务处于等待或者未完成的状态, 什么都不做(继续等上一个任务执行).
  • ExistingWorkPolicy.REPLACE : 如果上一个任务处于等待或者未完成的状态, 取消并删除上一个, 执行新的.

输入和输出

Worker 的输入输出是用 Map<String, Object> 来存储的, 用 Data 类封装了一层. 输出用 LiveData 来监听.

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
Kotlin

// 创建输入
val inputData = Data.Builder()
.putInt("KEY_FIRST", firstNumber)
.putInt("KEY_SECOND", secondNumber)
.build()
val worker = OneTimeWorkRequestBuilder<MathWorker>()
.setInputData(inputData)
.build()
WorkManager.getInstance().enqueue(worker)

// Worker 类:
class PlusWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {

override fun doWork(): Result {
val first = inputData.getInt("KEY_FIRST", 0)
val second = inputData.getInt("KEY_SECOND", 0)
val result = first + second // 1 + 2 = 3
val output = Data.Builder()
.putInt("KEY_RESULT", result)
.build()
return Result.success(output)
}
}

// 监听返回
WorkManager.getInstance().getWorkInfoByIdLiveData(worker.id)
.observe(this, Observer { info ->
if (info != null && info.state.isFinished) {
// 获取返回结果, 应该是3
val result = info.outputData.getInt("KEY_RESULT", 0)
}
})

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
Java
// 创建输入
Data inputData = new Data.Builder()
.putInt("KEY_FIRST", 1)
.putInt("KEY_SECOND", 2)
.build();

OneTimeWorkRequest worker = new OneTimeWorkRequest.Builder(PlusWorker.class)
.setInputData(inputData)
.build();
WorkManager.getInstance().enqueue(worker);

// Worker 类:
public class PlusWorker extends Worker {

public PlusWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}

@NonNull
@Override
public Result doWork() {
int first = getInputData().getInt("KEY_FIRST", 0);
int second = getInputData().getInt("KEY_SECOND", 0);
int result = first + second; // 1 + 2 = 3
Data output = new Data.Builder()
.putInt("KEY_RESULT", result)
.build();
return Result.success(output);
}
}

// 监听返回
WorkManager.getInstance().getWorkInfoByIdLiveData(worker.getId())
.observe(lifecycleOwner, info -> {
if (info != null && info.getState().isFinished()) {
// 获取返回结果, 应该是3
int result = info.getOutputData().getInt(KEY_RESULT, 0));
}
});

一些需要注意的地方

  • WorkManager 虽然在设计的时候是为了在 App 没运行的时候也能运行 Worker, 但是目前从 Google Issue Tracker 上的信息来看, 以下几种情况杀掉后任务的存活情况是这样的:

    1. 从任务管理器(最近使用)关掉: 原生的 Android 上 Worker 仍然会运行, 但是在某些把这种操作当做强制停止的厂商一些中国厂商 的机型上, Worker 要等到下次打开 App 才会运行.
    2. 重启手机 (Worker 运行中的状态): 重启后 Worker 会继续运行.
    3. App 信息 -> 强制关闭: Worker 会再下次打开 App 的时候运行.
    4. 重启手机 (App 被强制关闭了): Worker 会再下次打开 App 的时候运行.

目前 Android 没有系统 Api 提供步数信息, 需要自己统计. 而系统能够提供的接口只有 Sensor 了.

方案对比

方案 1

如果为了兼容低版本, 可以从 Sensor.TYPE_ACCELEROMETER(加速度传感器) 开始. 这个传感器从 Android 1.5 (API 3) 就有了, 通过传感器的 x, y, z. 利用算法计算出相应步数. 特点是兼容性强, 缺点是高功耗, 需要进程常驻.

方案 2:

从 Android 4.4 (API 14) 开始, 系统加入了新的传感器: Sensor.TYPE_STEP_COUNTERSensor.TYPE_STEP_DETECTOR.

  • Sensor.TYPE_STEP_COUNTER

    1. 当步数变化时, 返回从开机到现在的总步数, 重启清零.
    2. 这个传感器就是为低功耗设计的, 如果想持续监听步数, 不要反注册
    3. 用来实现健身类 App 统计步数.
  • Sensor.TYPE_STEP_DETECTOR

    1. 走了一步就返回一步, 返回值只有1.
    2. 方便用来统计某一段时间内的步数.

那么为了统计一天的步数, 当然是使用 方案2 中的 Sensor.TYPE_STEP_COUNTER.

具体实现

  1. 注册传感器.

  2. 当手机步数变化时, SensorEventListener#onSensorChanged() 返回手机从开机到现在的总步数:

    • 记录中是否有今日步数?

    • 没有 -> 今日步数为0

    • 有 ->

        1. 如果传感器返回的步数 < 记录的今日步数, 那么意味着重启了手机, 则
                今日步数 = 今日步数 + 传感器返回的步数.


        2. 如果不是大于, 那么正常计步即可.


                 今日步数 = 今日步数 + (传感器返回的步数 - 记录的今日步数)

补充逻辑

因为每天第一次打开 App 时, 步数一定为0, 所以可以监听时间变化广播 Intent.ACTION_DATE_CHANGED , 每天打开一次 App 更新本日步数变化, 也可使用 WorkManager, 这样就算 App 没有开启也能更新.

已知问题

  1. 如果 SensorEventListener 没有收到回调(需要 App 没有被杀并且手机步数有变化), 那么今天的步数就会计入第二天.

距离 Google 发布 Lifecycle 组件已经有一段时间了, 特意尝鲜用了一下, 这里主要记录一下 ViewModel 组件的原理和功能.

ViewModel

用途

ViewModel 用来存储和管理 UI 相关的数据, 在 UI 重建时不需要重建其相应的 ViewModel, 也可以在不同的 Fragment 之间共享一个 ViewModel.

1
2
3
4
5
6
public abstract class ViewModel {
protected void onCleared() {

}
}

它本身就是一个抽象类, 可以实现 onCleared() 方法做一些清理工作

实现细节

获取 ViewModel 的方法很简单:

1
ViewModel viewModel = ViewModelProviders.of(context).get(ViewModel.class);

通过这个方法去追踪实现即可, 实际代码不多, 就贴个实现过程吧.

context 传入 FragmentFragmentActivity, 因为他们都可以添加 Fragment. 这里就以 FragmentActivity 为例来说明.

  1. 创建一个没有 view 的 HolderFragment, HolderFragment 中通过设置 setRetainInstance(true); 实现它在 Activity 重建时能够保存自己的实例
  2. HolderFragmentViewModelStores 会维护一个 HashMap<String, ViewModel> 用于记录其实例化过的 ViewModel 对象, 并在 HolderFragmentonDestroy() 回调中执行 ViewModel 的 onCleared() 方法

这里有一个关于生命周期的细节, 为了防止 Fragment 重复创建或者泄露, Google 对其做了一些保护:

1
private Map<Activity, HolderFragment> mNotCommittedActivityHolders = new HashMap<>();

add Fragment 的流程(以 FragmentActivity 为例):

  1. Application 注册 ActivityLifecycleCallbacks, 在 Activity onDestroy() 时从 mNotCommittedActivityHolders 中移除未 commit 的 HolderFragment.

    1
    2
    3
    4
    5
    6
    activity.getApplication().registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() {
    @Override
    public void onActivityDestroyed(Activity activity) {
    HolderFragment fragment = mNotCommittedActivityHolders.remove(activity);
    }
    });
  2. commit HolderFragment 到对应 Activity

    1
    2
    3
    4
    5
    private static HolderFragment createHolderFragment(FragmentManager fragmentManager) {
    HolderFragment holder = new HolderFragment();
    fragmentManager.beginTransaction().add(holder, HOLDER_TAG).commitAllowingStateLoss();
    return holder;
    }
  3. 在 HolderFragment 的 onCreate() 生命周期中从 mNotCommittedActivityHolders 中移除自己

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void holderFragmentCreated(Fragment holderFragment) {
    Fragment parentFragment = holderFragment.getParentFragment();
    if (parentFragment != null) {
    mNotCommittedFragmentHolders.remove(parentFragment);
    parentFragment.getFragmentManager().unregisterFragmentLifecycleCallbacks(
    mParentDestroyedCallback);
    } else {
    mNotCommittedActivityHolders.remove(holderFragment.getActivity());
    }
    }

这样可以防止重复调用导致多次创建 HolderFragment, 也可以在 Fragment 创建后移除不必要的备份, 更可以防止内存泄露.

App 之间回调是常有的事, 特别是一些提供第三方登录/第三方支付的 App, 更是需要提供调起, 登录/支付, 回调原 App 的功能. 在实现的过程中遇到一些问题, 所以记录一下.

接下来调起界面称为 CallActivity, 调起 App 称为 Call App , 被调起页面称为 PayActivity. 被调起 App 称为 Pay App.

常见方案: startActivityForResult

最常见的当然是 通过 startActivityForResult() 使用隐式 Intent 调起, 然后在 onActivityResult() 中捕捉回调并处理成功失败逻辑了. 这种方法大部分人都会, 说点遇到的问题吧:

  • 问题来了

    • 问题一:
      • 打开 Pay Activity 后可以切到后台, 再切回来, 这样做一方面安全性不够, 另一方面不符合支付工具的特性. 第三方应用调起支付应用后, 对用户的感觉不应该是完整的打开了一个应用, 而应该是仅仅启动了支付的一个功能, 切到后台后应该无法再切回该支付页面, 最近打开的应用页面也不该展示 Pay App.
      • AndroidManifest.xml 中加上 android:excludeFromRecents="true"
    • 问题二:
      • 支付成功后, 按 Home 键切到后台, 再切回 Call App, 这时候没有触发 onActivityResult()
      • 这是系统问题, 正常逻辑, 只能在 onResume() 里面查询后台是否成功.
    • 问题三:
      • 如果已经打开了 Pay App, 然后切到后台, 打开到 Call App, 调起 Pay Activity 后按返回键, 返回到 Pay App 的界面了.
      • 需要指定 PayActivity 的 luanchMode 为 singleInstance
  • 大坑来了

    坑就在于这个 singleInstance, 在 Android 5.0 上一切正常, 但是在 Android 4.4 及以下版本, Call Activity 调用 startActivityForResult() 后, 直接回调了 onActivityResult(), 然后才打开 Pay Activity.

    为此我记录了一下不同 luanchMode 对 Android 4.4 及以下版本回调的影响.

    正常回调 -> Y

    直接回调 -> N

       | Call Activity  | Standard | SingleTop | SingleTask | SingleInstance 
      :---: | :---:  | :---: | :---: | :---: | :---: 
      Pay Activity  |
      Standard | | Y | Y | Y | N 
      SingleTop | | Y | Y | Y | N 
      SingleTask | | N | N | N | N 
      SingleInstance | | N | N | N | N 
  • startActivityForResult 方案不可行

    对于 startActivityForResult() 来说, 想实现对 singleInstance 的回调是不可能了, 同时还有问题二也需要优化, 所以最好还是换个方案来执行.

透明中间页 + 广播方案

之前版本由于已经发布, 需要兼容旧版本, 同时为了优化问题二, 考虑在接入 sdk 中提供一个中间页面 Entry Activity. 另外对于 singleInstance 的问题, 考虑用广播来替代.

Call Activity -> Entry Activity -> Pay Activity

  • 各页面功能如下:

    Call Activity

    • 依旧执行 startActivityForResult 方法, sdk 内部直接调起 Pay Activity 改为调起透明 Entry Activity.

    • 在 onActivityResult 中处理回调逻辑.

      Entry Activity

    • 设置 theme 为透明, 同时取消进入和退出的动画. 设置 luanchMode 为 singleTop

    • 注册广播, 监听 Pay Activity 发来的支付成功的广播

    • 直接通过 startActivity 调起 PayActivity

    • 在第二次进入 onResume() 时判断是否收到了支付成功的广播, 否则当做支付失败处理.

    • 成功及支付都通过 setResult() 的方式回调

      Pay Activity

    • 支付成功使用广播通知.

Android 中单元测试并不常见, 这篇文章就我自己的知识范围来介绍:

JUnit

在学习 Java 时就知道这是一个用来给纯 Java 测试的工具, 在 Android 中一样使用, Android Studio 可以用快捷键 cmd + enter 为每个类自动创建测试类. 在测试类中, 一般使用 Assert 来断言每个条件的对错, 而 JUnit 的注解则为单元测试提供框架.

  • @Test

    定义测试单元(每个方法为一个单元用例), 接受参数有

    • expected 预期会抛出某个异常, 不抛出则报错

      1
      Class<? extends Throwable> expected() default None.class;
    • timeout 超时

      1
      long timeout() default 0L;
  • @Before

    每个 Test 方法执行之前都会调用, 可以做预处理操作, 必须修饰 public 方法

  • @After

    每个 Test 方法执行之后都会调用, 可以做清理操作

  • @BeforeClass

    由于连续测试可能需要共享一个变量, 或者每个测试单元执行前都需要很长时间的准备工作, 可以把这些准备工作移到 BeforeClass 中, 该注解必须注解于一个 public static void没有参数的方法.

  • @AfterClass

    用于 @BeforeClass 的清理工作

  • @Ignore

    用于执行测试的时候忽略某个 @Test 单元

  • @Rule

    定义 @Test 单元执行时的逻辑, org.junit.rules 包定义了一些常用的 Rule 规则, 但我们也可以自定义 Rule.

    1
    2
    3
    4
    5
    6
    7
    8
    public class RuleSample implements TestRule {

    @Override
    public Statement apply(Statement base, Description description) {
    base.evaluate();
    return null;
    }
    }

Mockito

Mock 即[模拟]的意思, 当某些类因为依赖太多等关系难以创造, 或者我们只需要一个类的对象时, Mock 便是一个很好的工具, 可以帮助我们隔离代码进行测试. 而 Mockito 便是一个 Android 常用的 Mock 工具类.

模拟对象

Mockito 支持多种模拟对象方法.

  • @Mock 直接注解对象. 注解对象需要初始化, 可以有四种初始化方法.

    • @Before 注解的初始化方法中使用MockitoAnnotations.initMocks(this);

    • 使用自带的 @Rule 初始化

      1
      2
      @Rule
      public MockitoRule mockitoRule = MockitoJUnit.rule();
    • 使用 @RunWith 注解测试类.

      1
      2
      3
      4
      @RunWith(MockitoJUnitRunner.class)
      public class Test {

      }
    • 使用 Mockito.mock() 初始化对象.

  • @InjectMock 创建一个实例, 其余用 @Mock@Spy 注解创建的 mock 对象将被注入到用该实例中.

设置桩

Mockito 支持以下方法设置桩

1
2
Mockito.doXXX().when(XXX)
Mockito.when(XXX).thenXXX()

举个例子, 如果希望 TextView 在调用 getText() 时返回特定的内容, 可以使用

1
2
Mockito.when(mTextView.getText()).thenReturn("123);
Mockito.doReturn("123").when(mTextView).getText();

桩可以设置多次, 最终只会返回最后一次设置的值.

验证

Mockito 支持以下方法来验证函数执行

1
2
public static <T> T verify(T mock)
public static <T> T verify(T mock, VerificationMode mode)

默认的 VerificationMode 即 times(1), 即方法执行了一次, sdk 也提供了几种默认的验证模式实现, 如 never() , atLeastOnce() 等.

参数匹配

为了模拟某些参数的输入, 可以匹配这些参数的输入, 并返回所需的值. ArgumentMatchers.any() 是一个比较常用的方法.

比如模拟 TextView 的 OnClickListener().

1
2
3
4
5
6
7
8
Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
View.OnClickListener listener = invocation.getArgument(0);
listener.onClick(null);
return listener;
}
}).when(mTextView).setOnClickListener(ArgumentMatchers.any(View.OnClickListener.class));

@Spy

Mockito.spy() 返回的对象, 除非该方法已经有设置桩, 否则会调用该对象的真实方法. 可以用来改变对象特定方法的返回值, 而不改变对象本身. 注意 spy 不能 mock final 方法.

1
2
3
4
5
6
List list = new LinkedList();
List spy = Mockito.spy(list);
spy.add("one");
when(spy.size()).thenReturn(100);
spy.add("two");
System.out.println(spy.size());

JMockit

Mockito 的语法虽然比较简单易懂, 但它支持的功能还是不够多, 有一部分人是配合 PowerMock 一起使用, 但还有一个更强大的 Mock 工具值得使用.

模拟对象

注意 @RunWith 注解初始化测试类.

  • @Mocked 注解对象, 除了基本类型和数组对象, 其余所有都可以 mock, 会 mock 类中全部方法及其父类. 可以指定 stubOutClassInitialization 来决定 mock 对象时是否需要初始化静态变量, 某些 JNI 调用的静态变量在 mock 时初始化可能为 false, 这时候可以指定为 true 来跳过初始化.

  • @Injectable 仅 mock 指定的对象, 对于可以传入的对象, 使用 @Injectable 比 @Mock 更好

  • @Capturing mock 类及其子类, 也可以 mock 接口, 可以指定 maxInstances 来决定需要 mock 多少个对象.

常规使用

JMockit 用法是 录制 - 执行 - 验证 (record - replay - verify).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testMethod() throws Exception {

//record
new Expectations(){{
//执行方法, 可以是构造函数
a.getName();
//指定返回
result = "123";
}};

//run test code

//verify
new Verifications(){{

}};
}

参数匹配

参数匹配有很多, 最宽松的是 anyXXX , 其次是 withXXX.
使用 null 时, 必须有一个 anyXXX 或者 withXXX .

MockUp API

JMockit 我觉得 MockUp Api 特别强大, 几乎可以 Mock 所有的方法, 静态方法, 构造函数, 私有函数都可以, 这也是这个工具强大的地方.

比如有如下测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test {

public Test(String name) {

}

public static String getName() {

}

private String getAnswer() {

}
}

那么可以通过这样去 mock 对应的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new MockUp<Test>() {

@Mock
public void $init(Invocation invocation, String name) {
Test test = invocation.getInvokedInstance();
}

@Mock
public String getName() {
return "Mocked Name";
}

@Mock
private String getAnswer() {
return "Mocked Answer";
}
}

一些 Tip

  • 如果只是某个方法需要某个 mock 过的变量, 可以在测试方法的入参中传入, 不需要写成全局变量.

    1
    2
    3
    4
    @Test
    public void test(@Mocked TextView textView) {

    }