Android 版本适配

前言

前阵子适配 Android 9.0 时,遇到一些坑,感觉适配准备得不够充分就上线,会遇到非常多的问题,而现在再过几周,Android Q 正式版就要发布了,今天 Google 宣布发布最后一个 Beta 测试版 Android Q Beta 6 了,所以适配 Android Q 的工作也要尽早完成,Android Q Beta 6 发布文章

适配 Android 9.0 的坑

实际操作

总之,先搞到 Android Q 的手机或者模拟器先,可以查看获取 Android Q 测试版

成功打开 Android Q 版本的模拟器之后,先打开设置看一看,可以看到当前的系统版本号

支持与版本说明

代号、标记和细分版本号

关于代号官方文档

然后直接在 Android Q 上运行应用,查看应用的运行情况,是否存在打不开,卡顿,闪退等比较明显的现象

上面的操作没问题了,于是直接把 targetSdkVersion 改为 29

如何选择 compileSdkVersion, minSdkVersion 和 targetSdkVersion

编译运行,发现出现了非常多的报错,都是类型错误,原本没有说明是可为空的对象,现在变成了可为空的,比如 Parcel 的 readString() 方法,Class 的 cast() 方法等

使用 Kotlin 开发 Android 应用

Parcel readString 可为空

所以要对这些可为空的对象进行处理,让代码可以正常编译运行,最后可以正常运行在 Android Q 的模拟器上

扫描非 SDK 接口

所谓非 SDK 接口,在 iOS 开发中类似于“私有 API”,通俗的说,两者都是官方提供的接口,但前者明文包含在 SDK 中,允许开发者调用;后者出于各方面原因(比如还在测试中或是这个接口仅供系统内部调用),不鼓励甚至禁止开发者调用

当你调用了非 SDK 接口时,会有类似 Accessing hidden XXX 的日志,还指示了访问方式:直接,通过反射或通过 JNI

1
2
Accessing hidden field Landroid/os/Message;->flags:I (light greylist, JNI)
Accessing hidden method Landroid/app/ActivityThread;->currentActivityThread()Landroid/app/ActivityThread; (dark greylist, reflection)
  • light greylisted :包含方法和字段,它们继续在 Android P 预览版中运行,但 Google 无法保证在未来版本的平台上访问这些方法和字段
  • dark greylist:开发人员预览版本中无法访问的方法,这些日志消息可以使用 adb logcat 访问,并且会显示在正在运行的应用程序的 PID 下

但是一个大项目到底哪里使用了这些方法,靠自己 review 代码和看日志是不现实的,谷歌官方提供了官方检查器 veridex 来检测一个 apk 中哪里使用了非 SDK 接口,veridex下载

下载完毕后解压,解压后目录结构如下:

可以看到它里面包含了各名单的 txt 文件,其中 appcompat.sh 就是运行脚本

把需要扫描的 apk 放到同一目录下,使用终端 cd 到该目录,运行以下命令:(test.apk 为目标 apk,imprecise:不精确)

1
2
3
4
5
./appcompat.sh --dex-file=test.apk

./appcompat.sh --dex-file=test.apk >> happy.txt

./appcompat.sh --dex-file=test.apk --imprecise >> happy2.txt

对项目中的条目分类了一下,得到如下列表,观察详细内容,发现 veridex 把 Android 自己调用的也检测出来了

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
Rxjava:

#2: Linking greylist Lsun/misc/Unsafe;->arrayBaseOffset(Ljava/lang/Class;)I use(s): (io.reactivex:rxjava:1.2.1@jar)
Lcom/google/common/hash/LittleEndianByteArray$UnsafeByteArray;-><clinit>()V
Lcom/google/common/primitives/UnsignedBytes$LexicographicalComparatorHolder$UnsafeComparator;-><clinit>()V
Lrx/internal/util/unsafe/ConcurrentCircularArrayQueue;-><clinit>()V
Lrx/internal/util/unsafe/ConcurrentSequencedCircularArrayQueue;-><clinit>()V
Lrx/internal/util/unsafe/SpscUnboundedArrayQueue;-><clinit>()V

#3: Linking greylist Lsun/misc/Unsafe;->arrayIndexScale(Ljava/lang/Class;)I use(s): (io.reactivex:rxjava:1.2.1@jar)
Lcom/google/common/hash/LittleEndianByteArray$UnsafeByteArray;-><clinit>()V
Lcom/google/common/primitives/UnsignedBytes$LexicographicalComparatorHolder$UnsafeComparator;-><clinit>()V
Lrx/internal/util/unsafe/ConcurrentCircularArrayQueue;-><clinit>()V
Lrx/internal/util/unsafe/ConcurrentSequencedCircularArrayQueue;-><clinit>()V
Lrx/internal/util/unsafe/SpscUnboundedArrayQueue;-><clinit>()V

#5: Linking greylist Lsun/misc/Unsafe;->compareAndSwapLong(Ljava/lang/Object;JJJ)Z use(s):
Lcom/google/common/cache/Striped64$Cell;->a(JJ)Z
Lcom/google/common/cache/Striped64;->b(JJ)Z
Lrx/internal/util/unsafe/MpmcArrayQueueConsumerField;->b(JJ)Z
Lrx/internal/util/unsafe/MpmcArrayQueueProducerField;->c(JJ)Z
Lrx/internal/util/unsafe/SpmcArrayQueueConsumerField;->b(JJ)Z

#6: Linking greylist Lsun/misc/Unsafe;->compareAndSwapObject(Ljava/lang/Object;JLjava/lang/Object;Ljava/lang/Object;)Z use(s):
Lcom/google/common/util/concurrent/AbstractFuture$UnsafeAtomicHelper;->a(Lcom/google/common/util/concurrent/AbstractFuture;Lcom/google/common/util/concurrent/AbstractFuture$Listener;Lcom/google/common/util/concurrent/AbstractFuture$Listener;)Z
Lcom/google/common/util/concurrent/AbstractFuture$UnsafeAtomicHelper;->a(Lcom/google/common/util/concurrent/AbstractFuture;Lcom/google/common/util/concurrent/AbstractFuture$Waiter;Lcom/google/common/util/concurrent/AbstractFuture$Waiter;)Z
Lcom/google/common/util/concurrent/AbstractFuture$UnsafeAtomicHelper;->a(Lcom/google/common/util/concurrent/AbstractFuture;Ljava/lang/Object;Ljava/lang/Object;)Z
Lrx/internal/util/unsafe/MpscLinkedQueue;->c(Lrx/internal/util/atomic/LinkedQueueNode;)Lrx/internal/util/atomic/LinkedQueueNode;

#8: Linking greylist Lsun/misc/Unsafe;->getLongVolatile(Ljava/lang/Object;J)J use(s):
Lrx/internal/util/unsafe/ConcurrentSequencedCircularArrayQueue;->a([JJ)J
Lrx/internal/util/unsafe/SpscArrayQueue;->a()J
Lrx/internal/util/unsafe/SpscArrayQueue;->b()J
Lrx/internal/util/unsafe/SpscUnboundedArrayQueue;->a()J
Lrx/internal/util/unsafe/SpscUnboundedArrayQueue;->b()J

#9: Linking greylist Lsun/misc/Unsafe;->getObject(Ljava/lang/Object;J)Ljava/lang/Object; use(s):
Lrx/internal/util/unsafe/ConcurrentCircularArrayQueue;->a([Ljava/lang/Object;J)Ljava/lang/Object;

#10: Linking greylist Lsun/misc/Unsafe;->getObjectVolatile(Ljava/lang/Object;J)Ljava/lang/Object; use(s):
Lrx/internal/util/unsafe/BaseLinkedQueueConsumerNodeRef;->a()Lrx/internal/util/atomic/LinkedQueueNode;
Lrx/internal/util/unsafe/BaseLinkedQueueProducerNodeRef;->c()Lrx/internal/util/atomic/LinkedQueueNode;
Lrx/internal/util/unsafe/ConcurrentCircularArrayQueue;->b([Ljava/lang/Object;J)Ljava/lang/Object;
Lrx/internal/util/unsafe/SpscUnboundedArrayQueue;->a([Ljava/lang/Object;J)Ljava/lang/Object;

#12: Linking greylist Lsun/misc/Unsafe;->objectFieldOffset(Ljava/lang/reflect/Field;)J use(s):
Lcom/google/common/cache/Striped64$Cell;-><clinit>()V
Lcom/google/common/cache/Striped64;-><clinit>()V (2 occurrences)
Lcom/google/common/hash/Striped64$Cell;-><clinit>()V
Lcom/google/common/hash/Striped64;-><clinit>()V (2 occurrences)
Lcom/google/common/util/concurrent/AbstractFuture$UnsafeAtomicHelper;-><clinit>()V (5 occurrences)
Lrx/internal/util/unsafe/SpscUnboundedArrayQueue;-><clinit>()V (2 occurrences)
Lrx/internal/util/unsafe/UnsafeAccess;->a(Ljava/lang/Class;Ljava/lang/String;)J

#13: Linking greylist Lsun/misc/Unsafe;->putObject(Ljava/lang/Object;JLjava/lang/Object;)V use(s):
Lcom/google/common/util/concurrent/AbstractFuture$UnsafeAtomicHelper;->a(Lcom/google/common/util/concurrent/AbstractFuture$Waiter;Lcom/google/common/util/concurrent/AbstractFuture$Waiter;)V
Lcom/google/common/util/concurrent/AbstractFuture$UnsafeAtomicHelper;->a(Lcom/google/common/util/concurrent/AbstractFuture$Waiter;Ljava/lang/Thread;)V
Lrx/internal/util/unsafe/ConcurrentCircularArrayQueue;->a([Ljava/lang/Object;JLjava/lang/Object;)V

#14: Linking greylist Lsun/misc/Unsafe;->putOrderedLong(Ljava/lang/Object;JJ)V use(s):
Lrx/internal/util/unsafe/ConcurrentSequencedCircularArrayQueue;->a([JJJ)V
Lrx/internal/util/unsafe/SpmcArrayQueueProducerField;->d(J)V
Lrx/internal/util/unsafe/SpscArrayQueue;->d(J)V
Lrx/internal/util/unsafe/SpscArrayQueue;->e(J)V
Lrx/internal/util/unsafe/SpscUnboundedArrayQueue;->a(J)V
Lrx/internal/util/unsafe/SpscUnboundedArrayQueue;->b(J)V

#15: Linking greylist Lsun/misc/Unsafe;->putOrderedObject(Ljava/lang/Object;JLjava/lang/Object;)V use(s):
Lrx/internal/util/unsafe/ConcurrentCircularArrayQueue;->b([Ljava/lang/Object;JLjava/lang/Object;)V
Lrx/internal/util/unsafe/SpscUnboundedArrayQueue;->a([Ljava/lang/Object;JLjava/lang/Object;)V

Google:

#4: Linking greylist Lsun/misc/Unsafe;->compareAndSwapInt(Ljava/lang/Object;JII)Z use(s):
Lcom/google/common/cache/Striped64;->c()Z

#7: Linking greylist Lsun/misc/Unsafe;->getLong(Ljava/lang/Object;J)J use(s):
Lcom/google/common/hash/LittleEndianByteArray$UnsafeByteArray$1;->a([BI)J
Lcom/google/common/hash/LittleEndianByteArray$UnsafeByteArray$2;->a([BI)J
Lcom/google/common/primitives/UnsignedBytes$LexicographicalComparatorHolder$UnsafeComparator;->a([B[B)I (2 occurrences)

#11: Linking greylist Lsun/misc/Unsafe;->getUnsafe()Lsun/misc/Unsafe; use(s):
Lcom/google/common/cache/Striped64;->a()Lsun/misc/Unsafe;
Lcom/google/common/hash/LittleEndianByteArray$UnsafeByteArray;->c()Lsun/misc/Unsafe;
Lcom/google/common/hash/Striped64;->a()Lsun/misc/Unsafe;
Lcom/google/common/primitives/UnsignedBytes$LexicographicalComparatorHolder$UnsafeComparator;->a()Lsun/misc/Unsafe;
Lcom/google/common/util/concurrent/AbstractFuture$UnsafeAtomicHelper;-><clinit>()V

AndroidX

#16: Reflection greylist-max-p Landroid/animation/LayoutTransition;->cancel use(s):
Landroidx/transition/ViewGroupUtilsApi14;->a(Landroid/animation/LayoutTransition;)V

#21: Reflection greylist Landroid/app/Dialog;->mOnKeyListener use(s):
Landroidx/core/view/KeyEventDispatcher;->a(Landroid/app/Dialog;)Landroid/content/DialogInterface$OnKeyListener;

#29: Reflection greylist Landroid/content/res/Resources;->mResourcesImpl use(s):
Landroidx/appcompat/app/ResourcesFlusher;->d(Landroid/content/res/Resources;)V

#30: Reflection greylist Landroid/graphics/Typeface;->createFromFamiliesWithDefault use(s):
Landroidx/core/graphics/TypefaceCompatApi24Impl;-><clinit>()V
Landroidx/core/graphics/TypefaceCompatApi26Impl;->f(Ljava/lang/Class;)Ljava/lang/reflect/Method;
Landroidx/core/graphics/TypefaceCompatApi28Impl;->f(Ljava/lang/Class;)Ljava/lang/reflect/Method;

#31: Reflection greylist Landroid/media/session/MediaSession;->getCallingPackage use(s):
Landroid/support/v4/media/session/MediaSessionCompatApi24;->a(Ljava/lang/Object;)Ljava/lang/String;

#33: Reflection greylist Landroid/os/Bundle;->getIBinder use(s):
Landroidx/core/app/BundleCompat$BundleCompatBaseImpl;->a(Landroid/os/Bundle;Ljava/lang/String;)Landroid/os/IBinder;

#34: Reflection greylist Landroid/os/Bundle;->putIBinder use(s):
Landroidx/core/app/BundleCompat$BundleCompatBaseImpl;->a(Landroid/os/Bundle;Ljava/lang/String;Landroid/os/IBinder;)V

#35: Reflection greylist Landroid/service/media/MediaBrowserService$Result;->mFlags use(s):
Landroidx/media/MediaBrowserServiceCompatApi26;-><clinit>()V

#45: Reflection greylist Landroid/view/LayoutInflater;->mFactory2 use(s):
Landroidx/core/view/LayoutInflaterCompat;->b(Landroid/view/LayoutInflater;Landroid/view/LayoutInflater$Factory2;)V

#46: Reflection greylist Landroid/view/View;->computeFitSystemWindows use(s):
Landroidx/appcompat/widget/ViewUtils;-><clinit>()V

#47: Reflection greylist Landroid/view/View;->mAccessibilityDelegate use(s):
Landroidx/core/view/ViewCompat;->b(Landroid/view/View;)Z

#49: Reflection greylist-max-p Landroid/view/View;->mViewFlags use(s):
Landroidx/transition/ViewUtils;->a()V

#50: Reflection greylist-max-p Landroid/view/animation/Animation;->mListener use(s):
Landroidx/fragment/app/FragmentManagerImpl;->a(Landroid/view/animation/Animation;)Landroid/view/animation/Animation$AnimationListener;

#51: Reflection greylist Landroid/widget/AbsListView;->mIsChildViewEnabled use(s):
Landroidx/appcompat/widget/DropDownListView;-><init>(Landroid/content/Context;Z)V

#52: Reflection greylist-max-p Landroid/widget/AutoCompleteTextView;->doAfterTextChanged use(s):
Landroidx/appcompat/widget/SearchView$AutoCompleteTextViewReflector;-><init>()V

#53: Reflection greylist-max-p Landroid/widget/AutoCompleteTextView;->doBeforeTextChanged use(s):
Landroidx/appcompat/widget/SearchView$AutoCompleteTextViewReflector;-><init>()V

#54: Reflection greylist-max-p Landroid/widget/AutoCompleteTextView;->ensureImeVisible use(s):
Landroidx/appcompat/widget/SearchView$AutoCompleteTextViewReflector;-><init>()V

#65: Reflection greylist Ljava/lang/Thread;->inheritableThreadLocals use(s):
Lcom/google/common/base/internal/Finalizer;->b()Ljava/lang/reflect/Field;

#66: Reflection greylist Llibcore/icu/ICU;->addLikelySubtags use(s):
Landroidx/core/text/ICUCompat;-><clinit>()V

#67: Reflection greylist Lsun/misc/Unsafe;->allocateInstance use(s):
Lcom/google/gson/internal/UnsafeAllocator;->create()Lcom/google/gson/internal/UnsafeAllocator;

#68: Reflection greylist Lsun/misc/Unsafe;->theUnsafe use(s):
Lcom/google/gson/internal/UnsafeAllocator;->create()Lcom/google/gson/internal/UnsafeAllocator;
Lrx/internal/util/unsafe/UnsafeAccess;-><clinit>()V

68 hidden API(s) used: 15 linked against, 53 through reflection
60 in greylist
0 in blacklist
0 in greylist-max-o
8 in greylist-max-p
To run an analysis that can give more reflection accesses,
but could include false positives, pass the --imprecise flag.
  • greylist: 灰名单,即当前版本仍能使用的非 SDK 接口,但在下一版本中可能变成被限制的非 SDK 接口
  • blacklist:黑名单,使用了就会报错。也是我们项目中必须解决的非 SDK 接口
  • greylist-max-o: 在 targetSDK<=O 中能使用,但是在 targetSDK>=P 中被限制的非 SDK 接口
  • greylist-max-p: 在 targetSDK<=P 中能使用,但是在 targetSDK>=Q 中被限制的非 SDK 接口

使用非官方 API 的后果

在 Developer Preview 的后续版本中,访问非 SDK 接口的各种方法都会产生错误或其他不良结果,下面列出了几种示例:

Android Q 暗黑模式适配

1
2
3
4
5
6
7
/**
* 检查当前系统是否已开启暗黑模式
*/
fun getDarkModeStatus(context: Context): Boolean {
val mode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return mode == Configuration.UI_MODE_NIGHT_YES
}

在需要跟随夜间模式的 Activity 中添加 android:configChanges="uiMode" 选项,并重写 onConfigurationChanged() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<activity
android:name=".TestActivity"
android:configChanges="uiMode" />

override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> {
// 暗黑模式开启
}
Configuration.UI_MODE_NIGHT_NO -> {
// 暗黑模已关闭
}
}
}

关于 configChanges

Android Q 全面屏及手势适配

左图 18.5:9 设备上最大纵横比设置为 16:9 的应用

右图 18.5:9 设备上最大纵横比设置为 18.5:9 的应用

1
2
3
4
<!-- 适配全面屏 -->
<meta-data
android:name="android.max_aspect"
android:value="2.22" />

建议将应用设计为支持 2.1 或更高的纵横比,如果未设置任何值,并且 android:resizeableActivity 不为 true,则最大纵横比默认为 1.86(大约相当于 16:9),应用将无法充分利用额外的屏幕空间

打开全面屏:

这张图片基本上指的是过去这么长时间,一般应用可以利用的在屏幕上的一个所谓的“安全范围”,在 Android Q 上,我们首先建议大家去考虑使用最下面的导航栏区域

在没有适配全面屏的应用中,会看到如下效果:

官方文档

手势操作再添新机制,Android Q Beta 5 更新详解 | 剧透

Android Q Labs | Android Q 手势导航


Android 版本适配
https://enderhoshi.github.io/2019/08/12/Android 版本适配/
作者
HoshIlIlI
发布于
2019年8月12日
许可协议