今天讲的是target升级为27后高频出现的crash,Only fullscreen opaque activities can request orientation的crash,当targetsdk>=27的时候,且手机Android系统是26,27的时候会crash(华为和小米8.1自己做了优化所以不会出现这个问题,其他品牌手机没有测试)。因为开发的是SDK,所以在适配上很是无奈,SDK提供给使用方,不能限制使用方的targetSdk。如果是APP开发,targetsdk随意设置的话,以下这个仅供参考建议,下面我们就开始分析和解决之路。
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");
}
}
从26的源码中可看出,当targetsdk>26,设置了屏幕方向,并且是透明的会报上述错误。证明在Android8.0手机上应用如果是targetsdk>26,设置了透明和屏幕方向则会挂掉
if (getApplicationInfo().targetSdkVersion >= O_MR1 && 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");
}
}
从27的源码中可看出,当targetsdk>=27,设置了屏幕方向,并且是透明的会报上述错误。证明在Android8.1手机上应用如果是targetsdk>=27,设置了透明和屏幕方向则会挂掉
targetsdk为28 Activity的onCreate方法源码中去掉了上述代码,不做限制。
具体的什么是透明呢,看源码isTranslucentOrFloating,符合以下条件:
/**
* Determines whether the {@link Activity} is considered translucent or floating.
* @hide
*/
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;
}
什么是固定方向呢?
/**
* Returns true if the activity's orientation is fixed.
* @hide
*/
public boolean isFixedOrientation() {
return isFixedOrientationLandscape() || isFixedOrientationPortrait()
|| screenOrientation == SCREEN_ORIENTATION_LOCKED;
}
满足以下条件,就是满足固定方向
orientation == SCREEN_ORIENTATION_PORTRAIT (1)
|| orientation == SCREEN_ORIENTATION_SENSOR_PORTRAIT (7)
|| orientation == SCREEN_ORIENTATION_REVERSE_PORTRAIT (9)
|| orientation == SCREEN_ORIENTATION_USER_PORTRAIT; (12)
orientation == SCREEN_ORIENTATION_LANDSCAPE (0)
|| orientation == SCREEN_ORIENTATION_SENSOR_LANDSCAPE (6)
|| orientation == SCREEN_ORIENTATION_REVERSE_LANDSCAPE (8)
|| orientation == SCREEN_ORIENTATION_USER_LANDSCAPE; (11)
screenOrientation == SCREEN_ORIENTATION_LOCKED (14)
crash分析表现
target/手机系统版本 | crash |
---|---|
26/(Android8.0,Android8.1,Android9.0) | 否 |
27/Android8.0 | 是 |
27/Android8.1 | 是 |
27/Android9.0 | 否 |
28/(Android8.0) | 是 |
28/(Android8.1) | 是 |
28/(Android9.0) | 否 |
根据测试结果,当targetsdk>=27的时候,且手机Android系统是26,27的时候会crash 从上面的step源码中可以总结出我们下面的切入点:透明条件和方向
1、windowIsTranslucent 是否透明
2、windowIsFloating 通常适用于Activity作为Dialog,或者Panel
3、windowSwipeToDismiss 目前常用的Theme中还没发现
目前Android中使用的所有方向有:
public static final int SCREEN_ORIENTATION_UNSET = -2;
public static final int SCREEN_ORIENTATION_UNSPECIFIED = -1;
public static final int SCREEN_ORIENTATION_LANDSCAPE = 0;
public static final int SCREEN_ORIENTATION_PORTRAIT = 1;
public static final int SCREEN_ORIENTATION_USER = 2;
public static final int SCREEN_ORIENTATION_BEHIND = 3;
public static final int SCREEN_ORIENTATION_SENSOR = 4;
public static final int SCREEN_ORIENTATION_NOSENSOR = 5;
public static final int SCREEN_ORIENTATION_SENSOR_LANDSCAPE = 6;
public static final int SCREEN_ORIENTATION_SENSOR_PORTRAIT = 7;
public static final int SCREEN_ORIENTATION_REVERSE_LANDSCAPE = 8;
public static final int SCREEN_ORIENTATION_REVERSE_PORTRAIT = 9;
public static final int SCREEN_ORIENTATION_FULL_SENSOR = 10;
public static final int SCREEN_ORIENTATION_USER_LANDSCAPE = 11;
public static final int SCREEN_ORIENTATION_USER_PORTRAIT = 12;
public static final int SCREEN_ORIENTATION_FULL_USER = 13;
public static final int SCREEN_ORIENTATION_LOCKED = 14;
只要不满足固定条件,下面剩下的都可以使用
public static final int SCREEN_ORIENTATION_UNSET = -2;
public static final int SCREEN_ORIENTATION_UNSPECIFIED = -1;
public static final int SCREEN_ORIENTATION_USER = 2;
public static final int SCREEN_ORIENTATION_BEHIND = 3;
public static final int SCREEN_ORIENTATION_SENSOR = 4;
public static final int SCREEN_ORIENTATION_NOSENSOR = 5;
public static final int SCREEN_ORIENTATION_FULL_SENSOR = 10;
public static final int SCREEN_ORIENTATION_FULL_USER = 13;
在hook前我们需要知道满足crash几种代码操作
1、配置文件中设置透明,代码动态设置固定方向;
2、配置文件AndroidManifest.xml中同时设置固定方向和透明theme;
3、配置文件中设置方向,代码中动态设置透明(使用setTheme);
目前上面3中都会出现crash,但是注意一种情况不会crash,不在配置文件中设置,只在代码中同时设置透明和固定方向。
根据第一种crash,我们需要hook的是setRequestedOrientation方法,具体参见代码
public class StartActivityInvocationHandler2 implements InvocationHandler {
private Object mBase;
private Context mContext;
public StartActivityInvocationHandler2(Context context, Object base){
this.mContext = context;
this.mBase = base;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("setRequestedOrientation".equals(method.getName())){
if(args!=null){
//被设置的方向参数 args[1]
int orient = (int) args[1];
//因为crash环境,所以加个判断过滤下
if (mContext.getApplicationInfo().targetSdkVersion >= 27 && (Build.VERSION.SDK_INT == 26 || Build.VERSION.SDK_INT == 27)){
args[1] = -1;//设置成默认
}
}
}
return method.invoke(mBase,args);
}
}
这样其实不是完美的,因为如果activity没有设置透明属性,这么写也会改变人家Activity的方向,不友好,因此我们这里再优化一下,增加当前activity是否透明的条件判断。从上面的代码中可以看出我们目前能使用的就是proxy(执行方法的代理对象)和method(方法本身)以及args(执行方法的参数),如果不清楚他们的内容,建议自己打印一下。这里我们需要proxy来获取调用方的一些内容。
1、看源码,查看源码里它所有的方法(包括方法返回值,参数等),变量等;
2、如果看不了源码,就使用反射打印它所有的内容
3、筛选自己需要的东西
那么我们需要什么呢,那需要看透明条件需要什么了?
/**
* Determines whether the {@link Activity} is considered translucent or floating.
* @hide
*/
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;
}
根据源码我尝试使用反射取得该方法的值,但是这个方法的取值跟传入的context有关系,就是你要获取哪个activity的透明情况,你就得传哪个,这个方法可以用来在activity中使用,不适合我们这里hook使用,因为我们没有当前的activity可以传。
public static boolean isTranslucentOrFloating3(Context context){
boolean isTranslucentOrFloating = false;
try {
int [] styleableRes = (int[]) Class.forName("com.android.internal.R$styleable").getField("Window").get(null);
Method obtainStyledAttributesM = Context.class.getDeclaredMethod("obtainStyledAttributes",int[].class);
obtainStyledAttributesM.setAccessible(true);
//activity是否透明,跟传入的对象有关
TypedArray typedArray = (TypedArray) obtainStyledAttributesM.invoke(context,styleableRes);
Method m = ActivityInfo.class.getMethod("isTranslucentOrFloating", TypedArray.class);
m.setAccessible(true);
isTranslucentOrFloating = (boolean)m.invoke(null, typedArray);
m.setAccessible(false);
} catch (Exception e) {
e.printStackTrace();
}
return isTranslucentOrFloating;
}
经过各种尝试和测试,最终我找到了一个不需要当前context的正确有效的方法,话不多说,上代码,该方法亲测管用哦~~~
public static boolean isTranslucentOrFloating2(Context context,int themeId){
boolean isTranslucent = isThemeFromTypedArray(context,android.R.attr.windowIsTranslucent,themeId);
boolean isSwipeToDismiss = !isThemeHasTypedArray(context,android.R.attr.windowIsTranslucent,themeId)
&& isThemeFromTypedArray(context,android.R.attr.windowSwipeToDismiss,themeId);
boolean isFloating = isThemeFromTypedArray(context,android.R.attr.windowIsFloating,themeId);
return isFloating || isTranslucent || isSwipeToDismiss;
}
private static boolean isThemeFromTypedArray(Context context,int attrId,int themeId){
int[] attr = new int[]{attrId};
TypedArray array = context.getTheme().obtainStyledAttributes(themeId, attr);
boolean value = array.getBoolean(0,false );
return value;
}
private static boolean isThemeHasTypedArray(Context context,int attrId,int themeId){
int[] attr = new int[]{attrId};
TypedArray array = context.getTheme().obtainStyledAttributes(themeId, attr);
boolean has = array.hasValue(0 );
return has;
}
确定了使用的方法,我们现在开始准备需要的参数context和themeId,context这个不指定,随意一点就行,themeId的话这里我没有快捷的方法,通过保存读取到的AndroidManifest.xml文件中的Activity和与之相对应的theme来获取当前的themeId,这里我们详细讲一下如何获取当前的themeId。
上面hook住setRequestedOrientation方法的时候,我们打印了proxy对象的所有方法包括参数和返回值,筛选后使用了getActivityClassForToken方法,它需要的参数interface android.os.IBinder,返回值是class android.content.ComponentName,ComponentName对象是我们需要的,我们可以通过这个对象获取到当前的class,这样就可以获取相对应的themeId
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("setRequestedOrientation".equals(method.getName())){
if(args!=null){
//arg[0]是获取activity的关键 token
IBinder token = (IBinder) args[0];
//被设置的方向参数 args[1]
int orient = (int) args[1];
//因为crash环境,所以加个判断过滤下
if (mContext.getApplicationInfo().targetSdkVersion >= 27 && (Build.VERSION.SDK_INT == 26 || Build.VERSION.SDK_INT == 27)){
Object iActivityManager = proxy;
try {
//获取当前对象
Method getActivityClassForTokenM = iActivityManager.getClass().getDeclaredMethod("getActivityClassForToken",IBinder.class);
getActivityClassForTokenM.setAccessible(true);
ComponentName cname = (ComponentName) getActivityClassForTokenM.invoke(iActivityManager,token);
//通过查找AndroidManifest.xml获取当前themeId
int themeId = GetActivity.getInstance().getTheme(mContext,Class.forName(cname.getClassName()));
//使用isTranslucentOrFloating2方法获取当前是否在配置文件中设置透明
boolean isTrans = ThemeUtils.isTranslucentOrFloating2(mContext,themeId);
if(isTrans){
//如果是透明,就把屏幕方向设置
args[1] = -1;//设置成默认
}
}catch (Exception e){
}
}
}
}
return method.invoke(mBase,args);
}
自此crash的第一种情况,完美解决,中间可能有一些需要优化的地方,如有不妥,请大家联系我~
在启动Activity前获取到这些属性,然后并修改,这里就涉及到了hookLauncherActivity,在LAUNCH_ACTIVITY的时候处理。主要的就是反射关键方法isFixedOrientation来获取当前的透明条件,具体的hook方法摸索就靠大家自己了,这里直接给出代码:
@Override
public boolean handleMessage(Message msg) {
//每发一个消息都会走一次这个方法
if(msg.what == 100){//LAUNCH_ACTIVITY
handleLaunchActivity(mContext,msg);
}
return false;
}
/**
* 开始启动创建Activity拦截
* @param msg
*/
private void handleLaunchActivity(Context context,Message msg) {
//这里获取到的是ActivityRecord
Object record = msg.obj;
try {
Field activityInfoField = record.getClass().getDeclaredField("activityInfo");
activityInfoField.setAccessible(true);
ActivityInfo orignInfo = (ActivityInfo) activityInfoField.get(record);
if (context.getApplicationInfo().targetSdkVersion >= 27 && (Build.VERSION.SDK_INT == 26 || Build.VERSION.SDK_INT == 27)){
//isFixedOrientation参数是否满足条件
Method fixM = ActivityInfo.class.getDeclaredMethod("isFixedOrientation");
boolean result = (boolean) fixM.invoke(orignInfo);
if(result){
//设置方向为默认或者非固定崩溃的方向
orignInfo.screenOrientation = -1;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
代码中动态设置透明,目前还没有发现有什么方法能hook住,Activity的setTheme方法在onCreate方法前调用,但是无意中发现,调用setTheme方法时会先去调用setTaskDescription方法,打印出的内容如下
TaskDescription Label: null Icon: null IconFilename: null colorPrimary: -1644826 colorBackground: -16777216 statusBarColor: -16777216 navigationBarColor: -1
这个方法中目前只能拿到上面一些参数,还没有找到setTheme解决的方向,不过我尝试了一下网上各种在代码中设置透明的方法,setTheme的透明效果好像不怎么能生效,其他的设置透明的方法都不会导致crash。
在解决上述3中crash的时候,因为没有找到拦截setTheme的方法,所以推荐大家使用前两种解决方法,拦截动态设置方向方法setRequestedOrientation和拦截LaunchActivity时修改方向。
这个crash是当targetsdk>=27的,且手机Android系统是26,27的时候会crash,虽然在28的源码上谷歌去掉了固定方向透明度限制问题,但是对于我们开发者来说适配也很重要,26,27的手机在市面上占比也不算小,希望这篇文章能帮到大家很好的解决问题,不想使用hook的可以直接从否定固定方向入手,比如在AndroidManifest.xml中将方向设置为android:screenOrientation=“behind”,总之条条大路通罗马,祝大家好运~~