阅读 269

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

前言

原始的需求是这样的,客户会在系统中预装多个应用,但某些应用是没有经过适配的,客户要求的像素密度是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,祝大家新年快乐!

微信公众号

我在微信公众号也有写文章,更新比较及时,有兴趣者可以微信搜索【Android系统实战开发】,关注有惊喜哦!

文章分类
Android
文章标签