插件框架篇一之解决系统语言切换插件语言不变的问题

本文分析了Android系统语言切换时,插件应用语言未更新的问题原因,涉及AssetManager、Resources和配置更新流程。解决方案是通过监听ACTION_CONFIGURATION_CHANGED和ACTION_LOCALE_CHANGED广播,在ProxyActivity中注册广播并在接收到时同步主工程的配置到插件Resources。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题:
做国际化的时候,改变系统语言,主工程改变语言,插件只有关闭进程后才会改变。

问题分析:
PluginLoader工程中->JarResources类->getResourceByCl方法
截取部分代码如下:

AssetManager am = (AssetManager) AssetManager.class.newInstance();
am.getClass().getMethod("addAssetPath", String.class)
        .invoke(am, path);
if(JarApplication.getInstance() == null){
    JarApplication.setInstance(context.getApplicationContext());
}
Resources superRes = JarApplication.getInstance().getResources();
Resources res = new Resources(am, superRes.getDisplayMetrics(),
        superRes.getConfiguration());

new了一个AssetManager实例并绑定到新的Resources中。

PluginLoader工程中->JarMainBaseActivity类->setOverrideResources方法

@Override
public void setOverrideResources(JarResources myres) {
    if (myres == null) {
        this.jarResources = null;
        this.resources = null;
        this.assetManager = null;
        this.theme = null;
    } else {
        this.jarResources = myres;
        this.resources = myres.getResources();
        this.assetManager = myres.getAssets();
        Theme t = myres.getResources().newTheme();
        t.setTo(getTheme());
        this.theme = t;
    }
}

将JarResources中新new的resources替换掉activity的resources。也就是每个插件包的resources都是新建的。

我们再继续看切换系统语言,Android源码怎么走
packages/apps/Settings/com/android/settings/LocalePicker.java->updateLocale函数

public void updateConfiguration(Configuration values) {  
    enforceCallingPermission(android.Manifest.permission.CHANGE_CONFIGURATION,  
            "updateConfiguration()");  

    synchronized(this) {  
        if (values == null && mWindowManager != null) {  
            // sentinel: fetch the current configuration from the window manager  
            values = mWindowManager.computeNewConfiguration();  
        }  

        if (mWindowManager != null) {  
            mProcessList.applyDisplaySize(mWindowManager);  
        }  

        final long origId = Binder.clearCallingIdentity();  
        if (values != null) {  
            Settings.System.clearConfiguration(values);  
        }  
        updateConfigurationLocked(values, null, false, false);  
        Binder.restoreCallingIdentity(origId);  
    }  
}  

权限校验后,调用updateConfigurationLocked方法

/** 
 * Do either or both things: (1) change the current configuration, and (2) 
 * make sure the given activity is running with the (now) current 
 * configuration.  Returns true if the activity has been left running, or 
 * false if <var>starting</var> is being destroyed to match the new 
 * configuration. 
 * @param persistent
 */  
public boolean updateConfigurationLocked(Configuration values,  
        ActivityRecord starting, boolean persistent, boolean initLocale) {  
    int changes = 0;  

    boolean kept = true;  

    if (values != null) {  
        Configuration newConfig = new Configuration(mConfiguration);  
        changes = newConfig.updateFrom(values);  
        if (changes != 0) {  
            if (DEBUG_SWITCH || DEBUG_CONFIGURATION) {  
                Slog.i(TAG, "Updating configuration to: " + values);  
            }  

            EventLog.writeEvent(EventLogTags.CONFIGURATION_CHANGED, changes);  

            if (values.locale != null && !initLocale) {  
                saveLocaleLocked(values.locale,   
                                 !values.locale.equals(mConfiguration.locale),  
                                 values.userSetLocale, values.simSetLocale);  
            }  


            mConfigurationSeq++;  
            if (mConfigurationSeq <= 0) {  
                mConfigurationSeq = 1;  
            }  
            newConfig.seq = mConfigurationSeq;  
            mConfiguration = newConfig;  
            Slog.i(TAG, "Config changed: " + newConfig);  

            final Configuration configCopy = new Configuration(mConfiguration);  

            AttributeCache ac = AttributeCache.instance();  
            if (ac != null) {  
                ac.updateConfiguration(configCopy);  
            }  

            // Make sure all resources in our process are updated  
            // right now, so that anyone who is going to retrieve  
            // resource values after we return will be sure to get  
            // the new ones.  This is especially important during  
            // boot, where the first config change needs to guarantee  
            // all resources have that config before following boot  
            // code is executed.  
            mSystemThread.applyConfigurationToResources(configCopy);  

            if (persistent && Settings.System.hasInterestingConfigurationChanges(changes)) {  
                Message msg = mHandler.obtainMessage(UPDATE_CONFIGURATION_MSG);  
                msg.obj = new Configuration(configCopy);  
                mHandler.sendMessage(msg);  
            }  

            for (int i=mLruProcesses.size()-1; i>=0; i--) {  
                ProcessRecord app = mLruProcesses.get(i);  
                try {  
                    if (app.thread != null) {  
                        if (DEBUG_CONFIGURATION) Slog.v(TAG, "Sending to proc "  
                                + app.processName + " new config " + mConfiguration);  
                        app.thread.scheduleConfigurationChanged(configCopy);  
                    }  
                } catch (Exception e) {  
                }  
            }  
            Intent intent = new Intent(Intent.ACTION_CONFIGURATION_CHANGED);  
            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY  
                    | Intent.FLAG_RECEIVER_REPLACE_PENDING);  
            broadcastIntentLocked(null, null, intent, null, null, 0, null, null,  
                    null, false, false, MY_PID, Process.SYSTEM_UID);  
            if ((changes&ActivityInfo.CONFIG_LOCALE) != 0) {  
                broadcastIntentLocked(null, null,  
                        new Intent(Intent.ACTION_LOCALE_CHANGED),  
                        null, null, 0, null, null,  
                        null, false, false, MY_PID, Process.SYSTEM_UID);  
            }  

        }  
    }  

    if (changes != 0 && starting == null) {  
        // If the configuration changed, and the caller is not already  
        // in the process of starting an activity, then find the top  
        // activity to check if its configuration needs to change.  
        starting = mMainStack.topRunningActivityLocked(null);  
    }  

    if (starting != null) {  
        kept = mMainStack.ensureActivityConfigurationLocked(starting, changes);  
        // And we need to make sure at this point that all other activities  
        // are made visible with the correct configuration.  
        mMainStack.ensureActivitiesVisibleLocked(starting, changes);  
    }  

    if (values != null && mWindowManager != null) {  
        mWindowManager.setNewConfiguration(mConfiguration);  
    }  

    return kept;  
}

做了两件事:1、改变当前的configuration 2、确保所有正在运行的activity都能更新改变后的configuration 3、
我们继续跟踪updateFrom函数

/** 
 * Copy the fields from delta into this Configuration object, keeping 
 * track of which ones have changed.  Any undefined fields in 
 * <var>delta</var> are ignored and not copied in to the current 
 * Configuration. 
 * @return Returns a bit mask of the changed fields, as per 
 * {@link #diff}. 
 */  
public int updateFrom(Configuration delta) {  
    int changed = 0;  
    ...  
    if (delta.locale != null  
            && (locale == null || !locale.equals(delta.locale))) {  
        changed |= ActivityInfo.CONFIG_LOCALE;  
        locale = delta.locale != null  
                ? (Locale) delta.locale.clone() : null;  
        textLayoutDirection = LocaleUtil.getLayoutDirectionFromLocale(locale);  
    }  
    if (delta.userSetLocale && (!userSetLocale || ((changed & ActivityInfo.CONFIG_LOCALE) != 0)))  
    {  
        userSetLocale = true;  
        changed |= ActivityInfo.CONFIG_LOCALE;  
    }  
    ...  
    return changed;  
}

继续跟踪updateConfigurationLocked函数
其中mLurProcesses 是ArrayList类型.LRU是Least Recently Used的缩写,用户保存所有运行过的进程. ProcessRecord进程类, 一个apk文件运行时会对应一个进程.跟踪调用的scheduleConfigurationChanged()函数

public final void scheduleConfigurationChanged(Configuration config)  
            throws RemoteException {  
    Parcel data = Parcel.obtain();  
    data.writeInterfaceToken(IApplicationThread.descriptor);  
    config.writeToParcel(data, 0);  
    mRemote.transact(SCHEDULE_CONFIGURATION_CHANGED_TRANSACTION, data, null,  
            IBinder.FLAG_ONEWAY);  
    data.recycle();  
} 

通过binder进行进程间通信,远程调用的是ActivityThread.java中的私有内部类ApplicationThread。

private class ApplicationThread extends ApplicationThreadNative {  
    private static final String HEAP_COLUMN = "%13s %8s %8s %8s %8s %8s %8s";  
    private static final String ONE_COUNT_COLUMN = "%21s %8d";  
    private static final String TWO_COUNT_COLUMNS = "%21s %8d %21s %8d";  
    private static final String TWO_COUNT_COLUMNS_DB = "%21s %8d %21s %8d";  
    private static final String DB_INFO_FORMAT = "  %8s %8s %14s %14s  %s";  


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

scheduleConfigurationChanged方法中通过queueOrSendMessage方法发送handler消息CONFIGURATION_CHANGED,调用handleConfigurationChanged()

final void handleConfigurationChanged(Configuration config, CompatibilityInfo compat) {  
   ArrayList<ComponentCallbacks2> callbacks = null;  
    ...         ...  
   applyConfigurationToResourcesLocked(config, compat);  
   ...   
   callbacks = collectComponentCallbacksLocked(false, config);  
   ...      
   if (callbacks != null) {  
       final int N = callbacks.size();  
       for (int i=0; i<N; i++) {  
           performConfigurationChanged(callbacks.get(i), config);  
       }  

applyConfigurationToResourcesLocked方法的作用是将config更新到Resources中,来跟踪该方法

final boolean applyConfigurationToResourcesLocked(Configuration config,  
        CompatibilityInfo compat) {  

    int changes = mResConfiguration.updateFrom(config);  
    DisplayMetrics dm = getDisplayMetricsLocked(null, true);  


    if (compat != null && (mResCompatibilityInfo == null ||  
            !mResCompatibilityInfo.equals(compat))) {  
        mResCompatibilityInfo = compat;  
        changes |= ActivityInfo.CONFIG_SCREEN_LAYOUT  
                | ActivityInfo.CONFIG_SCREEN_SIZE  
                | ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;  
    }  

    ...  

    Resources.updateSystemConfiguration(config, dm, compat);  

    ...  

    Iterator<WeakReference<Resources>> it =  
        mActiveResources.values().iterator();  
    while (it.hasNext()) {  
        WeakReference<Resources> v = it.next();  
        Resources r = v.get();  
        if (r != null) {  
            if (DEBUG_CONFIGURATION) Slog.v(TAG, "Changing resources "  
                    + r + " config to: " + config);  
            r.updateConfiguration(config, dm, compat);  
            //Slog.i(TAG, "Updated app resources " + v.getKey()  
            //        + " " + r + ": " + r.getConfiguration());  
        } else {  
            //Slog.i(TAG, "Removing old resources " + v.getKey());  
            it.remove();  
        }  
    }  

    return changes != 0;  
}

Resources.updateSystemConfiguration()清除一部分系统资源, 并且将config更新到Resources, 而Resources包含了一个AssetManager对象, 该对象的核心实现是在AssetManager.cpp中完成的. 然后循环清空mActivityResources资源. 再回到handleConfigurationChanged()函数, 执行完updateSystemConfiguration后, 会循环该进程的所有activity。
再回到handleConfigurationChanged方法,其中

if (callbacks != null) {
    final int N = callbacks.size();
    for (int i=0; i<N; i++) {
        performConfigurationChanged(callbacks.get(i), config);
    }
}

继续跟踪performConfigurationChanged方法

private final void performConfigurationChanged(  
        ComponentCallbacks2 cb, Configuration config) {  
    // Only for Activity objects, check that they actually call up to their  
    // superclass implementation.  ComponentCallbacks2 is an interface, so  
    // we check the runtime type and act accordingly.  
    Activity activity = (cb instanceof Activity) ? (Activity) cb : null;  
    if (activity != null) {  
        activity.mCalled = false;  
    }  

    boolean shouldChangeConfig = false;  
    if ((activity == null) || (activity.mCurrentConfig == null)) {  
        shouldChangeConfig = true;  
    } else {  

        // If the new config is the same as the config this Activity  
        // is already running with then don't bother calling  
        // onConfigurationChanged  
        int diff = activity.mCurrentConfig.diff(config);  
        if (diff != 0) {  
            // If this activity doesn't handle any of the config changes  
            // then don't bother calling onConfigurationChanged as we're  
            // going to destroy it.  
            if ((~activity.mActivityInfo.getRealConfigChanged() & diff) == 0) {  
                shouldChangeConfig = true;  
            }  
        }  
    }  

    if (DEBUG_CONFIGURATION) Slog.v(TAG, "Config callback " + cb  
            + ": shouldChangeConfig=" + shouldChangeConfig);  
    if (shouldChangeConfig) {  
        cb.onConfigurationChanged(config);  

        if (activity != null) {  
            if (!activity.mCalled) {  
                throw new SuperNotCalledException(  
                        "Activity " + activity.getLocalClassName() +  
                    " did not call through to super.onConfigurationChanged()");  
            }  
            activity.mConfigChangeFlags = 0;  
            activity.mCurrentConfig = new Configuration(config);  
        }  
    }  
}

先判断configuration是否改变,然后调用activity.onConfigurationChanged方法

/** 
* Called by the system when the device configuration changes while your 
* activity is running.  Note that this will <em>only</em> be called if 
* you have selected configurations you would like to handle with the 
* {@link android.R.attr#configChanges} attribute in your manifest.  If 
* any configuration change occurs that is not selected to be reported 
* by that attribute, then instead of reporting it the system will stop 
* and restart the activity (to have it launched with the new 
* configuration). 
*  
* <p>At the time that this function has been called, your Resources 
* object will have been updated to return resource values matching the 
* new configuration. 
*  
* @param newConfig The new device configuration. 
*/  
public void onConfigurationChanged(Configuration newConfig) {  
   mCalled = true;  

   mFragments.dispatchConfigurationChanged(newConfig);  

   if (mWindow != null) {  
       // Pass the configuration changed event to the window  
       mWindow.onConfigurationChanged(newConfig);  
   }  

   if (mActionBar != null) {  
       // Do this last; the action bar will need to access  
       // view changes from above.  
       mActionBar.onConfigurationChanged(newConfig);  
   }  
} 

将config改变的事件分发下去。

解决方案:
从以上分析可以看到,改变本地语言后会发出ACTION_CONFIGURATION_CHANGED和ACTION_LOCALE_CHANGED广播。所以我们可以监听广播来更新插件的config。
1、在ProxyActivity中注册广播。一个activity中可以注册多个BroadcastReceiver,所以在ProxyActivity注册广播中并不影响插件中继续注册广播。

/**
 * 注册广播
 * 可以同时注册多个BroadcastReceiver,所以并不影响插件内继续注册广播
 */
private void registerReceiver(){
    //应用内配置语言
    IntentFilter filter = new IntentFilter();
    filter.addAction(Intent.ACTION_LOCALE_CHANGED);
    registerReceiver(mReceiver, filter);
}
/**
 * 监听系统语言切换广播
 */
private BroadcastReceiver mReceiver = new BroadcastReceiver() {

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        JLog.i("clf", "config...action="+action);
        if(action.equals(Intent.ACTION_LOCALE_CHANGED)){
            //切换系统语言
            updateLocalConfig();
        }
    }
};

2、插件的Resources和AssetManager是新new的,所以进程重启之前config都是没有更新的。所以需要在收到ACTION_LOCALE_CHANGED广播时,将主工程的config同步到插件Resources.

/**
 * 由于插件的Resources是新new的,所以这里需要做config同步操作,与主工程同步
 */
private void updateLocalConfig(){
    if(JarApplication.getInstance() == null){
        return;
    }
    Resources resources = getResources();
    Configuration config = resources.getConfiguration();
    DisplayMetrics metrics = resources.getDisplayMetrics();
    config.locale = JarApplication.getInstance().getResources().getConfiguration().locale;
    resources.updateConfiguration(config, metrics);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值