问题:
做国际化的时候,改变系统语言,主工程改变语言,插件只有关闭进程后才会改变。
问题分析:
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);
}