Fix Bug: Android 8.0.0 透明主题Activity设置屏幕方向崩溃

背景

在开发某次需求中, 需要预加载Web内容, 在需要展示时快速展现, 目的是优化用户体验.

于是采用了透明Activity配置了WebVieww提前进行加载, 展示前将Window内容移出屏幕之外, 展示时再将Window内容移回来.

由于没有Android8.0的测试机, 测试期间未能发现问题, 上线后才发现Bugly平台新增了不少Bug.

预加载代码不展示了, 不是本文章要讲的内容.

分析

崩溃日志

1
2
3
4
5
6
1 java.lang.RuntimeException:Unable to start activity ComponentInfo{com.pxwx.assistant/com.pxwx.main.ui.MainActivity}: java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation
2 android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2957)
3 ......
4 Caused by:
5 java.lang.IllegalStateException:Only fullscreen opaque activities can request orientation
6 android.app.Activity.onCreate(Activity.java:1038)

源码

查看Activity类源码, 报错位置如下:

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
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState);

// 1. 条件: targetSdkVersion > 26
if (getApplicationInfo().targetSdkVersion > O && mActivityInfo.isFixedOrientation()) {
final TypedArray ta = obtainStyledAttributes(com.android.internal.R.styleable.Window);
final boolean isTranslucentOrFloating = ActivityInfo.isTranslucentOrFloating(ta);
ta.recycle();
// 2. 条件: Activity透明或悬浮
if (isTranslucentOrFloating) {
throw new IllegalStateException(
"Only fullscreen opaque activities can request orientation");
}
}

...
}

// 判断Activity是否透明或者悬浮
public static boolean isTranslucentOrFloating(TypedArray attributes) {
final boolean isTranslucent =
attributes.getBoolean(com.android.internal.R.styleable.Window_windowIsTranslucent,
false);
final boolean isSwipeToDismiss = !attributes.hasValue(
com.android.internal.R.styleable.Window_windowIsTranslucent)
&& attributes.getBoolean(
com.android.internal.R.styleable.Window_windowSwipeToDismiss, false);
final boolean isFloating =
attributes.getBoolean(com.android.internal.R.styleable.Window_windowIsFloating,
false);

return isFloating || isTranslucent || isSwipeToDismiss;
}

结果

分析源码后发现, 该问题是Android 8.0系统上的一个Bug, 满足以下条件就会触发必现的崩溃.

  1. App的targetSdkVersion > 26.
  2. Activity使用了透明主题.
  3. Activity显式设置了Activity的方向, 包括:
    • AndroidManifest.xml 文件中为Activity设置 android:screenOrientation 属性, 会在onCreate函数中触发.
    • 代码中调用 setRequestedOrientation 函数.

解决方法

很显然, 我们满足了这些条件, 那么怎么修复呢?

  • 首先降级到26及以下肯定是不可取的.
  • 其次取消透明主题无法实现需求.

那只能在代码层面绕过这个判断了, 抛出异常的是 onCreatesetRequestedOrientation 两个函数, 针对这两个函数覆写进行处理.

  • onCreate: 在 super.onCreate 执行之前设置屏幕方向为 ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, 这样 isTranslucentOrFloating 函数返回false, 就不再抛出异常.
  • setRequestedOrientation: 若满足上面的条件, 不去执行 super.setRequestedOrientation 函数.

这样就避免了崩溃的产生.

实现类

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
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.content.res.TypedArray;
import android.os.Build;
import android.util.Log;

import androidx.annotation.NonNull;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
* 作者:wenhui.w
* 日期:2024-09-23 15:04
* 描述:
* activity设置透明主题,在8.0.0版本设置横竖屏会抛异常(包括xml设置和代码设置)
* 谷歌在8.0.1之后修复
*/
public final class FixAndroidOTranTheme {
private static final String TAG = "FixOTransparentTheme";
/**
* 在Activity调用onCreate函数之前修改屏幕方向,防止在清单文件中设置方向、透明主题,导致崩溃。
* <p>
* {@link ActivityInfo#screenOrientation}
* {@link ActivityInfo#SCREEN_ORIENTATION_UNSPECIFIED}
*
* @param activity
* @param isTransparentTheme 当前Activity是否是透明主题
* @return true:成功修复;false:不需要修复或修复失败。
*/
public static boolean fixOnCreate(@NonNull Activity activity, boolean isTransparentTheme) {
if (isTargetVersion() && (isTransparentTheme || isTranslucentOrFloating(activity))) {
return fixScreenOrientation(activity);
}
return false;
}

/**
* 是否是需要修复的目标版本
*/
public static boolean isTargetVersion() {
return Build.VERSION.SDK_INT == Build.VERSION_CODES.O;
}

/**
* 判断Activity是否是透明主题,通过反射调用系统函数获取
* {@link ActivityInfo#isTranslucentOrFloating}
*/
public static boolean isTranslucentOrFloating(@NonNull Activity activity) {
boolean isTranslucentOrFloating = false;
try {
int[] styleableRes = (int[]) Class.forName("com.android.internal.R$styleable").getField("Window").get(null);
final TypedArray ta = activity.obtainStyledAttributes(styleableRes);
Method m = ActivityInfo.class.getMethod("isTranslucentOrFloating", TypedArray.class);
m.setAccessible(true);
isTranslucentOrFloating = (boolean) m.invoke(null, ta);
m.setAccessible(false);
} catch (Exception e) {
e.printStackTrace();
}
return isTranslucentOrFloating;
}

/**
* 修复屏幕方向,通过反射调用系统函数设置
*
* @param activity
*/
public static boolean fixScreenOrientation(@NonNull Activity activity) {
try {
Field field = Activity.class.getDeclaredField("mActivityInfo");
field.setAccessible(true);
ActivityInfo info = (ActivityInfo) field.get(activity);
info.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
field.setAccessible(false);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}

/**
* 判断是否能设置屏幕方向。在{@link Activity#setRequestedOrientation(int)}前调用。
*
* @param activity
* @param isTransparentTheme 当前Activity是否是透明主题
* return true:能设置
*/
public static boolean canRequestedOrientation(@NonNull Activity activity, boolean isTransparentTheme) {
if (isTargetVersion()) {
if (isTransparentTheme) {
Log.d(TAG, "透明主题Activity在Android8.0版本不能调用 setRequestedOrientation 函数。");
return false;
}
if (isTranslucentOrFloating(activity)) {
String message = "透明主题Activity在Android8.0版本不能调用 setRequestedOrientation 函数,请复写 isTransparentTheme 函数,并返回true。";
if (BuildConfig.DEBUG) {
// debug调试期间及时发现未复写isTransparentTheme函数问题
throw new UnsupportedOperationException(message);
} else {
Log.e(TAG, message);
}
return false;
}
}
return true;
}
}

如何使用?

BaseActivity 中增加了 isTransparentTheme 函数进行优化, 透明Activity需要覆写该函数返回true.

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
public abstract class BaseActivity extends AppCompatActivity {

/**
* 必须在调用super之前调用{@link FixAndroidOTranTheme#fixOnCreate}
*/
@Override
protected void onCreate(Bundle bundle) {
FixAndroidOTranTheme.fixOnCreate(this, isTransparentTheme());
super.onCreate(bundle);
}

/**
* Activity是否设置了透明主题, 设置透明主题的Activity可以覆写该函数返回true.
* PS:
* 1. 防止{@link FixAndroidOTranTheme#isTranslucentOrFloating}获取失败
* 2. 减少反射实现带来的性能开销
*/
protected boolean isTransparentTheme() { return false; }

/**
* Android8.0、透明主题的Activity不执行该函数
* PS:
* 1. 目的是防止其他页面或新增页面时未复写{@link #isTransparentTheme()}函数出现崩溃
*/
@Override
public void setRequestedOrientation(int requestedOrientation) {
if (FixAndroidOTranTheme.canRequestedOrientation(this, isTransparentTheme())) {
super.setRequestedOrientation(requestedOrientation);
}
}
}

结尾

好了, 以上就是本文章的全部内容了, 希望能对你有所帮助.

以上代码在生产环境中运行有一段时间了, 目前未发现异常.

作者

王文辉

发布于

2024-11-17

更新于

2024-11-18

许可协议