基于Android10.0适配应用界面–修改系统源码

原文:

基于Android10.0适配应用界面--修改系统源码 - 掘金

前言

原始的需求是这样的,客户会在系统中预装多个应用,但某些应用是没有经过适配的,客户要求的像素密度是160,但某些应用在该像素密度下显示会显得很小。客户不想改应用,要求在该160的像素密度下,也要能够正常显示应用。

思路

思路一 动态切换像素密度(糟糕的思路)

初期是通过adb shell指令进行切换测试的。经测试,这些在160像素密度下显示异常的应用,在320的像素密度下,则能显示正常。也就说只要保证在显示异常的应用时,系统像素密度切320,则能解决此问题。指令如下:


arduino

复制代码

wm density 320 //将像素密度切换为320

通过wm指令的源码,找到通过代码进行切换的方法:


ini

复制代码

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,该方法如下:


arduino

复制代码

//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系统用来描述屏幕显示指标的一个类,即描述屏幕显示的各个参数,主要参数如下:


arduino

复制代码

//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方法,更改配置。如下:


ini

复制代码

@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对象的创建:


less

复制代码

//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中,它定义如下:


swift

复制代码

//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调用:


scss

复制代码

//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方法实现如下:


java

复制代码

//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对象:


less

复制代码

//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的创建上,直接看源码:


less

复制代码

//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方法进行更新。我们看下该方法:


arduino

复制代码

//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上:


less

复制代码

//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对象。

作者:迷你球
链接:https://juejin.cn/post/6930949098090528775
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。