原因
错误是由全屏透明 Activity 引起的
Caused by: java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation
手机是 华为荣耀8 Android 8.0
当时,为了解决不显示引导页,直接进入应用时,会闪屏的问题,引入了透明 Activity 样式
<style name="GuideActivityStyle" parent="ImmersionStatusBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
这是 Google 出于安全的考虑,对 Android8.0 版本做的处理
当一个 Activity 固定方向并且是透明的,在8.0以后的版本中就会抛出异常。源码如下
Entry ent = AttributeCache.instance().get(packageName,realTheme, com.android.internal.R.styleable.Window, userId);
final boolean translucent = ent != null && (ent.array.getBoolean(com.android.internal.R.styleable.Window_windowIsTranslucent, false)|| (!ent.array.hasValue(
com.android.internal.R.styleable.Window_windowIsTranslucent) && ent.array.getBoolean(com.android.internal.R.styleable.Window_windowSwipeToDismiss,false)));
fullscreen = ent != null && !ent.array.getBoolean(com.android.internal.R.styleable.Window_windowIsFloating, false) && !translucent;
fullscreen = ent != null && !ActivityInfo.isTranslucentOrFloating(ent.array);
noDisplay = ent != null && ent.array.getBoolean(com.android.internal.R.styleable.Window_windowNoDisplay, false);
if (ActivityInfo.isFixedOrientation(requestedOrientation) && !fullscreen && appInfo.targetSdkVersion > O) {
throw new IllegalStateException("Only fullscreen activities can request orientation");
}
可以看出当 三个条件同时满足的时候,系统会抛出"Only fullscreen activities can request orientation"异常。
- ActivityInfo.isFixedOrientation(requestedOrientation)
表示判断当前的 Activity是否固定了方向,true为固定了方向。 - fullscreen 表示Activity是否是透明的或者是否悬浮在Activity上,是透明的或者悬浮在Activity上fullscreen就等于false。以下三种情况认为不是“fullscreen“:
- “windowIsTranslucent” 为 true;
- “windowIsTranslucent” 为 false,但“windowSwipeToDismiss” 为 true;
- “windowIsFloating“ 为 true;
- appInfo.targetSdkVersion > O 表示编译版本号大于26
所幸的是在 Android 9.0 的时候,已经删掉了这个限制了
解决方案
- windowSwipeToDismiss 禁止预览
当启动一个activity或者应用的时候,系统会先加载一个window preview的UI来增加过渡效果,但是有时候这种效果体验并不好,比如用户自定义的白色界面,然后预览的黑色界面,这样就会出现所谓的闪屏
<style name="GuideActivityStyle" parent="ImmersionStatusBar">
<item name="android:windowIsTranslucent">false</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:windowBackground">@null</item>
</style>
- 使用占位图片
<style name="SplashTheme" parent="AppTheme">
<item name="android:windowBackground">@drawable/shape_launch</item>
<item name="android:windowFullscreen">true</item>
</style>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque">
<item android:drawable="@android:color/white"/>
<item>
<bitmap
android:src="@mipmap/main_splash_bg"
android:gravity="fill" />
</item>
</layer-list>
2020-05-06 补充
问题
用了上面的方案,能解决部分手机兼容性问题。但上线之后,依旧有客户反馈,使用微信登录会导致闪退
2020-05-06 03:25:18 A/UncaughtException: **************************************************
Manufacturer: vivo, Model: vivo X9s L, API 27
PackageName: com.xxx.yyy.zzz
VersionName: 3.4.3, VersionCode: 91
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at d.l.a.h.i(FragmentManagerImpl.java:3)
at d.l.a.h.c(FragmentManagerImpl.java:36)
at d.l.a.h.b(FragmentManagerImpl.java:38)
at d.l.a.a.c(BackStackRecord.java:6)
at com.xxx.yyy.zzz.activity.MainActivity.a(MainActivity.java:7)
at g.l.a.a.a.b.run(Unknown Source:4)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:192)
at android.app.ActivityThread.main(ActivityThread.java:6866)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:549)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:817)
**************************************************
原因
谷歌在安卓8.0版本时为了支持全面屏,增加了一个限制:如果是透明的Activity,则不能固定它的方向,因为它的方向其实是依赖其父Activity的(因为透明)。
这个bug只有在8.0中有,8.1中已经修复。具体crash有两种:
- Activity的风格为透明,在manifest文件中指定了一个方向,则在onCreate中crash
- Activity的风格为透明,如果调用setRequestedOrientation方法固定方向,则crash
解决方案
如果进onCreate的时候,如果判断是透明窗口风格,直接把屏幕朝向改为未指定类型即SCREEN_ORIENTATION_UNSPECIFIED就可以了,因为Activity是透明的,所以其方向依赖于父Activity,所以这个改动对结果不会产生任何影响。
至于很多透明Activity的代码中调用setRequestedOrientation,更好处理,项目不是有BaseActivity吗,我们直接判断如果是androidO版本,我们不调用它即可,结果也是等效的。
- 获取 com.android.internal.R$styleable.Window 这个stylable,stylable 是 R 的内部类,因此要获取到这个数组,就只能用反射调用。然后再通过反射 ActivityInfo#isTranslucentOrFloating() 这个方法,来判断 Activity 是否是透明或者浮动的。
- 利用反射,修改mActivityInfo中的变量screenOrientation,设置成 SCREEN_ORIENTATION_UNSPECIFIED
- override setRequestedOrientation 方法,直接return
代码如下
public class BaseActivity extends FragmentActivity {
private static final String TAG = "BaseActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
if(needCheckOrientation()) {
boolean fixOrientation = fixOrientation();
Log.i(TAG, "onCreate fixOrientation when Oreo, result = " + fixOrientation);
}
super.onCreate(savedInstanceState);
}
private boolean needCheckOrientation() {
return Build.VERSION.SDK_INT == Build.VERSION_CODES.O && isTranslucentOrFloating();
}
@Override
public void setRequestedOrientation(int requestedOrientation) {
if(needCheckOrientation()) {
Log.i(TAG, "setRequestedOrientation avoid calling setRequestedOrientation when Oreo");
return;
}
super.setRequestedOrientation(requestedOrientation);
}
private boolean isTranslucentOrFloating(){
boolean isTranslucentOrFloating = false;
try {
int [] styleableRes = (int[]) Class.forName("com.android.internal.R$styleable").getField("Window").get(null);
final TypedArray ta = 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;
}
private boolean fixOrientation(){
try {
Field field = Activity.class.getDeclaredField("mActivityInfo");
field.setAccessible(true);
ActivityInfo o = (ActivityInfo)field.get(this);
o.screenOrientation = -1;
field.setAccessible(false);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
然后微信的 WXEntryActivity 再集成 BaseActivity,最终修复 BUG