本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
本人小楠——一位励志的Android开发者。

前言

在手机APP开发的时候,一般默认会适配竖屏,游戏开发除外。但是在Android平板电脑开发中,屏幕旋转的问题比较突出,可以这样说,平板电脑的最初用意就是横屏使用的,比较方便,用户会经常旋转我们设备的屏幕。

在我的《屏幕旋转的适配问题以及遇到的一些坑》这篇文章中提到了一些坑,包括View的测量不准确,onConfigurationChanged的回调不确定,今天主要分析一下onConfigurationChanged调用的不确定性因素。

关于这个问题,笔者在网上搜索了一下关于为什么onConfigurationChanged的方法不会被调用,基本都是说清单文件里面没有正确配置,因为在Android2.3以后需要增加screenSize这个配置,完整的配置如下:

                        

但是完全搜索不到关于我的问题的搜索结果,毕竟做Android平板的并不多,因此写下来记录自己的学习过程。

关于官方文档

我们知道,在Activity、View(ViewGroup)、Fragment、Service、Content Provider等等在设备的配置发生变化的时候,会回调onConfigurationChanged的方法。实质上主要是Activity中收到AMS的通知,回调,然后把事件分发到Window、Fragment、ActionBar等。

下面我们可以通过Activity的onConfigurationChanged方法源码可以看到:

public void onConfigurationChanged(Configuration newConfig) {    mCalled = true;    //分发到Activity中的所有Fragment    mFragments.dispatchConfigurationChanged(newConfig);    //分发到Activity的Window对象    if (mWindow != null) {        // Pass the configuration changed event to the window        mWindow.onConfigurationChanged(newConfig);    }    //分发到Activity的ActionBar    if (mActionBar != null) {        mActionBar.onConfigurationChanged(newConfig);    }}

这里我们讨论的是为什么当我们的界面在设备配置发生变化的时候(屏幕旋转),有时候并不会回调onConfigurationChanged呢?

关于Activity的官方文档有下面一句话:

Activity官方文档.png

也就是说,在设备配置发生变化的时候,会回调onConfigurationChanged,但是前提条件是当你的Activity(组件)还在运行的时候。

这就很明显了,说明一旦你的界面暂停以后就不会回调这个方法了。但是这样会导致一个问题,就是你的界面跳转到其他界面的时候(当前界面暂停),然后发生了一次屏幕旋转,再返回的时候,你的界面虽然旋转了,但是并没有回调onConfigurationChanged方法,并没有执行你的UI适配代码。

源码分析

想到四大组件,我们第一时间应该会想到AMS(ActivityManagerService),没错,今天我们的始发站就是AMS。

在AMS里面搜索了一下关键字Configuration,发现了updateConfigurationLocked这个方法(没有说明的情况下,都是只给出省略版):
相信眼尖的朋友一定会看出来,在这里由AMS创建了Configuration对象,然后通过进程间通信,通知我们的app进程。

private boolean updateConfigurationLocked(Configuration values, ActivityRecord starting,        boolean initLocale, boolean persistent, int userId, boolean deferResume) {    int changes = 0;    if (mWindowManager != null) {        mWindowManager.deferSurfaceLayout();    }    if (values != null) {        //创建Configuration对象        Configuration newConfig = new Configuration(mConfiguration);        changes = newConfig.updateFrom(values);        if (changes != 0) {            for (int i=mLruProcesses.size()-1; i>=0; i--) {                ProcessRecord app = mLruProcesses.get(i);                try {                    if (app.thread != null) {                        //通过进程间通信,通知我们的app进程                        app.thread.scheduleConfigurationChanged(configCopy);                    }                } catch (Exception e) {                }            }        }    }}

thread是一个IApplicationThread对象,继承了IInterface接口,也就是说是一个AIDL对象,实际上这个接口的实现类是ActivityThread里面的内部类ApplicationThread。

public interface IApplicationThread extends IInterface {}

那么就是说这时候AMS通过IApplicationThread进行了进程间通信,实际上调用了我们APP所在的进程的ActivityThread里面的内部类ApplicationThread的scheduleConfigurationChanged方法:

public void scheduleConfigurationChanged(Configuration config) {    updatePendingConfiguration(config);    sendMessage(H.CONFIGURATION_CHANGED, config);}

这个方法很简单,就是发送消息给我们应用程序的系统Handler,然后由它来处理消息,下面继续分析处理消息的过程:

case CONFIGURATION_CHANGED:    mCurDefaultDisplayDpi = ((Configuration)msg.obj).densityDpi;    mUpdatingSystemConfig = true;    handleConfigurationChanged((Configuration)msg.obj, null);    mUpdatingSystemConfig = false;    break;

这里继续调用了handleConfigurationChanged方法:

final void handleConfigurationChanged(Configuration config, CompatibilityInfo compat) {    //收集需要回调onConfigurationChanged的组件信息    ArrayList callbacks = collectComponentCallbacks(false, config);    if (callbacks != null) {        final int N = callbacks.size();        for (int i=0; i

这个方法首先收集需要回调onConfigurationChanged的组件信息,如果当前循环的组件是Activity,那么通过调用performConfigurationChangedForActivity方法回调Activity的onConfigurationChanged。
如果当前循环不是Activity,比如说是Service等,也需要performConfigurationChanged进行相应回调。

下面我们先看performConfigurationChangedForActivity这个方法:

private void performConfigurationChangedForActivity(ActivityClientRecord r,                                                    Configuration newBaseConfig,                                                    boolean reportToActivity) {    r.tmpConfig.setTo(newBaseConfig);    if (r.overrideConfig != null) {        r.tmpConfig.updateFrom(r.overrideConfig);    }    performConfigurationChanged(r.activity, r.token, r.tmpConfig, r.overrideConfig,            reportToActivity);    freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig));}

实际上也会调用performConfigurationChanged方法,这里最终会回调Activity的onConfigurationChanged方法:

private void performConfigurationChanged(ComponentCallbacks2 cb,                                         IBinder activityToken,                                         Configuration newConfig,                                         Configuration amOverrideConfig,                                         boolean reportToActivity) {    Activity activity = (cb instanceof Activity) ? (Activity) cb : null;    if (shouldChangeConfig) {        if (reportToActivity) {            final Configuration configToReport = createNewConfigAndUpdateIfNotNull(                    newConfig, contextThemeWrapperOverrideConfig);            //回调Activity的onConfigurationChanged方法            cb.onConfigurationChanged(configToReport);        }        //这里有个注意点,就是我们需要先调用super的onConfigurationChanged方法,父类的方法中会把mCalled置为true。        //因为上文提到,父类的方法需要进行一次分发。否则就会抛出SuperNotCalledException。        if (activity != null) {            if (reportToActivity && !activity.mCalled) {                throw new SuperNotCalledException(                        "Activity " + activity.getLocalClassName() +                        " did not call through to super.onConfigurationChanged()");            }            activity.mConfigChangeFlags = 0;            activity.mCurrentConfig = new Configuration(newConfig);        }    }}

这里有个注意点,就是我们需要先调用super的onConfigurationChanged方法,父类的方法中会把mCalled置为true。
因为上文提到,父类的方法需要进行一次分发。否则就会抛出SuperNotCalledException。

我们的问题还没有解决,就是为什么我们的组件在暂停以后并不会回调呢?问题的核心代码就出在收集组件信息的时候,我们回到ActivityThread的系统Handler的handleConfigurationChanged方法中:

//收集需要回调onConfigurationChanged的组件信息ArrayList callbacks = collectComponentCallbacks(false, config);

这里收集了组件的信息,下面我们点进去collectComponentCallbacks这个方法缪一眼:

ArrayList collectComponentCallbacks(        boolean allActivities, Configuration newConfig) {    //初始化一个ArrayList用于存储需要回调的组件信息    ArrayList callbacks            = new ArrayList();    synchronized (mResourcesManager) {        //拿到所有Application对象        final int NAPP = mAllApplications.size();        for (int i=0; i

这个方法初始化一个ArrayList用于存储需要回调的组件信息,然后收集了当前应用的所有Application对象(多进程的时候可能就会有多个),Activity,Service,Content Provider信息,然后进行下一步回调。

关键是在收集Activity的时候,进行了一次判断:

if (!ar.activity.mFinished && (allActivities || !ar.paused)) {    // If the activity is currently resumed, its configuration    // needs to change right now.    //如果当前的Activity是resumed状态的时候,需要马上回调    callbacks.add(a);

经过源码的分析,已经可以得出这个结论就是:
当Activity已经Finish掉或者已经暂停的时候,并不会把这个Activity添加进来,这样做是为了保证系统的效率,只去处理那些活跃(resume)的Activity,其他的不处理。

解决办法

办法一:

我们可以《屏幕旋转的适配问题以及遇到的一些坑》这篇文章中提到的,通过自定义广播的方式去接收android.intent.action.CONFIGURATION_CHANGED这个广播。
注意这个广播只能够在Java代码中注册才会有效果。

办法二:

重写Activity的onRestart代替onConfigurationChanged方法,只不过需要判断一下当前的屏幕方向。

@Overrideprotected void onRestart() {    super.onRestart();    if (isLandOrientation()) {        //横屏    } else {        //竖屏    }}public boolean isLandOrientation() {    if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {        return true;    } else {        return false;    }}

自己手动判断一下横竖屏即可。

如果觉得我的文字对你有所帮助的话,欢迎关注我的公众号:

公众号:Android开发进阶

我的群欢迎大家进来探讨各种技术与非技术的话题,有兴趣的朋友们加我私人微信huannan88,我拉你进群交(♂)流(♀)

更多相关文章

  1. android中对apk文件反编译的方法(详细)
  2. 好的android程序该这样编写
  3. AsyncTask源码深入分析和巧记线程池
  4. Android(java方法)上实现mp4的分割和拼接 (一)
  5. AsyncTask使用以及源码分析
  6. 这种方式教你简单的在Flutter中分离View与Model的方法
  7. Android之完美退出方法
  8. Android梳理 Activity
  9. Android(安卓)Dialog使用

随机推荐

  1. Android Activity生命周期 (图文)
  2. 可以显示行号的Android Log 工具
  3. Android(安卓)8.1开启Bluetooth A2DP sin
  4. Android adb LOGCAT显示中文
  5. 视频Android studio1.3.1从菜鸟到高手--
  6. android 动画使用方式
  7. Android中SQLite版本升级
  8. android sd卡读写 附源码
  9. android permission list
  10. Android从一个APP跳转到另一个APP的主界