背景
在开发某次需求中, 需要预加载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);
if (getApplicationInfo().targetSdkVersion > O && mActivityInfo.isFixedOrientation()) { final TypedArray ta = obtainStyledAttributes(com.android.internal.R.styleable.Window); final boolean isTranslucentOrFloating = ActivityInfo.isTranslucentOrFloating(ta); ta.recycle(); if (isTranslucentOrFloating) { throw new IllegalStateException( "Only fullscreen opaque activities can request orientation"); } }
... }
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, 满足以下条件就会触发必现的崩溃.
- App的targetSdkVersion > 26.
- Activity使用了透明主题.
- Activity显式设置了Activity的方向, 包括:
- 在 AndroidManifest.xml 文件中为Activity设置 android:screenOrientation 属性, 会在onCreate函数中触发.
- 代码中调用 setRequestedOrientation 函数.
解决方法
很显然, 我们满足了这些条件, 那么怎么修复呢?
- 首先降级到26及以下肯定是不可取的.
- 其次取消透明主题无法实现需求.
那只能在代码层面绕过这个判断了, 抛出异常的是 onCreate 和 setRequestedOrientation 两个函数, 针对这两个函数覆写进行处理.
- 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;
public final class FixAndroidOTranTheme { private static final String TAG = "FixOTransparentTheme";
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; }
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; }
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; }
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) { 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 {
@Override protected void onCreate(Bundle bundle) { FixAndroidOTranTheme.fixOnCreate(this, isTransparentTheme()); super.onCreate(bundle); }
protected boolean isTransparentTheme() { return false; }
@Override public void setRequestedOrientation(int requestedOrientation) { if (FixAndroidOTranTheme.canRequestedOrientation(this, isTransparentTheme())) { super.setRequestedOrientation(requestedOrientation); } } }
|
结尾
好了, 以上就是本文章的全部内容了, 希望能对你有所帮助.
以上代码在生产环境中运行有一段时间了, 目前未发现异常.