前言
原始的需求是这样的,客户会在系统中预装多个应用,但某些应用是没有经过适配的,客户要求的像素密度是160,但某些应用在该像素密度下显示会显得很小。客户不想改应用,要求在该160的像素密度下,也要能够正常显示应用。
思路
思路一 动态切换像素密度(糟糕的思路)
初期是通过adb shell指令进行切换测试的。经测试,这些在160像素密度下显示异常的应用,在320的像素密度下,则能显示正常。也就说只要保证在显示异常的应用时,系统像素密度切320,则能解决此问题。指令如下:
wm density 320 //将像素密度切换为320
通过wm指令的源码,找到通过代码进行切换的方法:
import android.view.IWindowManager;
import android.view.WindowManagerGlobal;
try {
final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
int displayId = Display.DEFAULT_DISPLAY;
wm.setForcedDisplayDensityForUser(displayId, setDensity, UserHandle.USER_CURRENT);
} catch (RemoteException e) {
//do something....
}
只需要在适合的地方,通过判断包名,需要切换320的应用就执行如上代码,像素密度就被切换过来了。
但是,效果并不好。在点击应用,进行跳转时,会有1到2秒的卡顿,后面通过查源码分析,是因为Configuaration更改了,系统进行了界面冻结,等配置完成更新,然后才会继续跑显示应用的流程。
而客户却接受了这个效果。
但这不是自己想要的效果。于是有了思路二。
思路二 更改应用的Resources
这个思路的产生是想到,应用显示随着像素密度的更改而改变,那使用的布局则只能是相对布局,单位通常是dip(dp),该单位最终是要通过某个方法转化为px,该方法如下:
//frameworks/base/core/java/android/util/TypedValue.java
public static float applyDimension(int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
那么只要保证显示异常的应用在调用该方法进行转化的时候,metrics.density的值由我们进行控制即可。
metrics是一个DisplayMetrics对象,而DisplayMetrics类是android系统用来描述屏幕显示指标的一个类,即描述屏幕显示的各个参数,主要参数如下:
//frameworks/base/core/java/android/util/DisplayMetrics.java
...
public float density; //逻辑像素密度,计算方法density = densityDpi * (1.0f / 160);如果densityDpi为320,则该值为2.0f
public int densityDpi; //具体的像素密度大小,如160dpi,320dpi...
public float scaledDensity;//用于字体大小的显示,scaledDensity = density * fontScale。其中fontScale代表用户设定的Android设备字体缩放比例,默认为1。也就是说,当用户没有改变Android设备的字体缩放比例时,sp、dp与px的换算是相同的。
public float xdpi;
public float ydpi;
...
稍微介绍了DisplayMetrics类,每个应用在被打开之后,都会分配有一个DisplayMetrics对象,正常来说,每个屏幕配置都一样,都是从系统拿来。除非应用本身重写getResources方法,更改配置。如下:
@Override
public Resources getResources() {
Resources resources = super.getResources();
Configuration configuration = resources.getConfiguration();
configuration.densityDpi = 320;
resources.updateConfiguration(configuration, resources.getDisplayMetrics());
return resources;
}
但上面也说了,客户不愿意更改应用,所以不存在重写getResources方法的情况。那只能改源码了(不想看下面啰嗦流程介绍的,可以跳过直接看解决方法)。
getResources流程的介绍
追踪getResources方法,发现ContextImpl类直接返回了一个mResources成员,mResources是在应用打开的时候被赋值的。个中细节不多说,最终是在ResourceManager类中实现Resources对象的创建:
//frameworks/base/core/java/android/app/ResourcesManager.java
public @Nullable Resources getResources(@Nullable IBinder activityToken,
@Nullable String resDir,
@Nullable String[] splitResDirs,
@Nullable String[] overlayDirs,
@Nullable String[] libDirs,
int displayId,
@Nullable Configuration overrideConfig,
@NonNull CompatibilityInfo compatInfo,
@Nullable ClassLoader classLoader) {
try {
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
final ResourcesKey key = new ResourcesKey(
resDir,
splitResDirs,
overlayDirs,
libDirs,
displayId,
overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
compatInfo);
classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
return getOrCreateResources(activityToken, key, classLoader);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
先创建一个ResourcesKey对象,主要作为key值保存ResourcesImpl对象。所有应用的ResourcesImpl对象都保存在mResourceImpls中,它定义如下:
//frameworks/base/core/java/android/app/ResourcesManager.java
private final ArrayMap<ResourcesKey, WeakReference<ResourcesImpl>> mResourceImpls =
new ArrayMap<>();
ResourcesImpl对象是Resources中的一个成员,Resources的方法,最终会调用其成员mResourcesImpl的方法。这里不展开,下面会说到。
系统每创建一个ResourcesImpl对象,就会调用mResourceImpls的put方法将该对象保存起来,保存的key就是如上所创建的ResourcesKey。
我们继续看getResources方法中的getOrCreateResources调用:
//frameworks/base/core/java/android/app/ResourcesManager.java
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
synchronized (this) {
if (DEBUG) {
Throwable here = new Throwable();
here.fillInStackTrace();
Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here);
}
if (activityToken != null) {
final ActivityResources activityResources =
getOrCreateActivityResourcesStructLocked(activityToken);
// Clean up any dead references so they don't pile up.
ArrayUtils.unstableRemoveIf(activityResources.activityResources,
sEmptyReferencePredicate);
// Rebase the key's override config on top of the Activity's base override.
if (key.hasOverrideConfiguration()
&& !activityResources.overrideConfig.equals(Configuration.EMPTY)) {
final Configuration temp = new Configuration(activityResources.overrideConfig);
temp.updateFrom(key.mOverrideConfiguration);
key.mOverrideConfiguration.setTo(temp);
}
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
}
// We will create the ResourcesImpl object outside of holding this lock.
} else {
// Clean up any dead references so they don't pile up.
ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);
// Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
//通过key来查找是否有相应的ResourcesImpl对象存在
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
// We will create the ResourcesImpl object outside of holding this lock.
}
// If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
//如果如上找不到相应的ResourcesImpl,则创建一个
ResourcesImpl resourcesImpl = createResourcesImpl(key);
if (resourcesImpl == null) {
return null;
}
// Add this ResourcesImpl to the cache.
//将创建出来的ResourcesImpl对象添加到mResourceImpls中
mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
final Resources resources;
if (activityToken != null) {
resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
} else {
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
return resources;
}
}
getOrCreateResources():是最终获取或则创建Resources的方法,来详细看下系统是如何创建的;我们主要看第三种情况:
1.activityToken不为空,则通过key获取ResourcesImpl对象,然后通过getOrCreateResourcesForActivityLocked()方法获取或者创建一个Resources对象;
2.activityToken为空,则通过key获取ResourcesImpl对象,然后getOrCreateResourcesLocked()获取或者创建一个Resources对象;
3.如果不存在key对应的ResourcesImpl对象,则通过createResourcesImpl()创建ResourcesImpl对象,再根据activityToken是否为null,调用对应的方法,创建Resources对象;
createResourcesImpl方法实现如下:
//frameworks/base/core/java/android/app/ResourcesManager.java
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
//assets用于应用资源文件的管理,通过传入的key参数进行创建,key中的mResDir成员为资源文件的路径
final AssetManager assets = createAssetManager(key);
if (assets == null) {
return null;
}
//这里根据id(一般为0)和daj生成一个DisplayMetrics对象
final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
//根据ResourcesKey和DisplayMetrics成员生成Configuration对象。
final Configuration config = generateConfig(key, dm);
//assets,dm,config,daj将作为ResourcesImpl创建的参数,后续resources的操作将依赖这几个参数
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
if (DEBUG) {
Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
}
return impl;
}
这里先跳出来,等会再看ResourcesImpl对象创建的过程。
createResourcesImpl方法是在getOrCreateResources方法中调用的,接下来要做的才是真正创建了Resources对象:
//frameworks/base/core/java/android/app/ResourcesManager.java
private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
@NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) {
// Find an existing Resources that has this ResourcesImpl set.
final int refCount = mResourceReferences.size();
for (int i = 0; i < refCount; i++) {
WeakReference<Resources> weakResourceRef = mResourceReferences.get(i);
Resources resources = weakResourceRef.get();
if (resources != null &&
Objects.equals(resources.getClassLoader(), classLoader) &&
resources.getImpl() == impl) {
if (DEBUG) {
Slog.d(TAG, "- using existing ref=" + resources);
}
return resources;
}
}
// Create a new Resources reference and use the existing ResourcesImpl object.
Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
: new Resources(classLoader);
//创建了Resources对象后,调用setImpl方法将ResourcesImpl对象 赋值到自身成员mResourcesImpl中
resources.setImpl(impl);
//所有的应用的Resouces对象也是通过mResourceReferences来进行管理的,就是一个list
mResourceReferences.add(new WeakReference<>(resources));
if (DEBUG) {
Slog.d(TAG, "- creating new ref=" + resources);
Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
}
return resources;
}
这个方法先找是否有存在的可用的Resources,如果没有,则进行创建,并将创建好的Resources对象加入到mResourceReferences list中,方便管理。
回到ResourcesImpl的创建上,直接看源码:
//frameworks/base/core/java/android/content/res/ResourcesImpl.java
public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
@Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
mAssets = assets;
mMetrics.setToDefaults();
mDisplayAdjustments = displayAdjustments;
mConfiguration.setToDefaults();
updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
}
mAssets用于应用资源文件的管理,后续所有涉及到资源相关的,都会调用mAssets的成员方法。mMetrics和mConfiguration先给了一个默认值,然后再通过updateConfiguration方法进行更新。我们看下该方法:
//frameworks/base/core/java/android/content/res/ResourcesImpl.java
public void updateConfiguration(Configuration config, DisplayMetrics metrics,
CompatibilityInfo compat) {
...
//这里将metrics赋值mMetrics
if (metrics != null) {
mMetrics.setTo(metrics);
}
...
//更新config
final @Config int configChanges = calcConfigChanges(config);
...
//将config中的densityDpi和density更新给mMetrics
if (mConfiguration.densityDpi != Configuration.DENSITY_DPI_UNDEFINED) {
mMetrics.densityDpi = mConfiguration.densityDpi;
mMetrics.density =
mConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
}
...
}
DisplayMetrics对象的densityDpi和density最终还是会被Configuration所刷新。所以修改的时候需要修改Confiuration对象的densityDpi值。
修改方法
说完过程,下面说说在哪修改合适。
还记得上面所说的,创建ResourcesImpl对象时,传入了四个参数,其中一个是Configuration对象。我们的修改就在它的生成方法generateConfig上:
//frameworks/base/core/java/android/app/ResourcesManager.java
private Configuration generateConfig(@NonNull ResourcesKey key, @NonNull DisplayMetrics dm) {
Configuration config;
final boolean isDefaultDisplay = (key.mDisplayId == Display.DEFAULT_DISPLAY);
final boolean hasOverrideConfig = key.hasOverrideConfiguration();
if (!isDefaultDisplay || hasOverrideConfig) {
config = new Configuration(getConfiguration());
if (!isDefaultDisplay) {
applyNonDefaultDisplayMetricsToConfiguration(dm, config);
}
if (hasOverrideConfig) {
config.updateFrom(key.mOverrideConfiguration);
if (DEBUG) Slog.v(TAG, "Applied overrideConfig=" + key.mOverrideConfiguration);
}
} else {
config = getConfiguration();
}
//add {
String apkName = key.mResDir;
int setDensity = Resources.getSystem().getInteger(R.integer.config_desity_switch_value);
String needChangedensityApk = Resources.getSystem().getString(R.string.density_change_pacagename);
//Slog.d(TAG, "generateConfig---->" + apkName + "--setDensity-->" + setDensity + "--needChangedensityApk---->" + needChangedensityApk);
if (apkName != null && needChangedensityApk != null && needChangedensityApk.contains(apkName)) {
config.densityDpi = setDensity;
}
//add }
return config;
}
这里根据资源包的路径来判断是否要进行densityDpi的更改,如果是显示异常的应用,则进行修改。可将需要修改的异常应用的资源包路径放到density_change_pacagename中进行配置。
经过测试发现,这个体验就很顺畅。
关于Configuration类和DisplayMetrics类的思考
在做这个功能的过程中,比较疑惑的是,Configuration类和DisplayMetrics类看着功能比较类似,都是屏幕参数配置相关,为什么需要搞出两个呢?
而最终为什么又是通过DisplayMetrics类中的参数来进行资源的选择?
看了下Configuration类的实现,发现它居然继承Parcelable接口。说明它支持跨进程传输。而DisplayMetrics类则就是一个正常的类,没继承任何接口。
这么看来,猜测可能DisplayMetrics类用于应用内部,而Configuration类则用于外部。当Configuration对象更改时,也刷新了内部DisplayMetrics对象。
结语
上一篇文章更新是在2020年的9月16号,真是惭愧。之所以停更,一个原因是小孩即将出生,忙前忙后的。另一个原因是每年的这个时候都是工作比较忙的时候,尤其是2020年,简直忙疯了。
接下来的时间也并不富裕,要帮忙带娃,哄睡~~~呜呜呜
好怀念以前可以有一整块学习和写东西的时间。
但接下来还是会抽时间分享自己做过的一些东西和学习成果。初衷还是倒逼自己去成长和纠错。
Anyway,祝大家新年快乐!