framework插件化技术-资源加载(免安装)

在上一篇《framework插件化技术-类加载》说到了一种framework特性插件化技术中的类加载方式解决lib的动态加载问题,但是我们都知道在Android里面,除了代码意外,还有一块非常重要的领域就是资源。
plugin里的代码是不能按照普通的方式直接加载资源的,因为plugin里拿到的context都是从app传过来的,如果按照普通的方式加载资源,加载的都是app的资源,无法加载到plugin apk里的资源。所以我们需要在plugin的feature和res中间加入一个中间层:PluginResLoader,来专门负责加载资源,结构如下:

构造Resources

我们都知道在Android中Resources类是专门负责加载资源的,所以PluginResLoader的首要任务就是构建Resources。我们平时在Activity中通过getResources()的接口获取Resources对象,但是这里获取到的Resources是与应用Context绑定的Resources,我们需要构造plugin的Resources,这里我们可以看下如何构造Resources:

我们可以看到构造函数里需要传入三个参数,AssetManager,DisplayMetrics,Configuration。后面两个参数我们可以通过app传过来的context获得,所以现在问题可以转换为构造AssetManager。我们发现AssetManager的构造函数并不公开,所以这里只能通过反射的方式构造。同时AssetManager还有一个接口可以直接传入资源的路径:

public int addAssetPath(String path)
复制代码

所以,综上所述,我们可以得到构造Resources的方法:
PluginResLoader:

public Resources getResources(Context context) {
    AssetManager assetManager = null;

    try {
        mPluginPath = context.getDataDir().getPath() + "/plugin.apk";
        assetManager = AssetManager.class.newInstance();
        if(assetManager != null) {
            try {
                AssetManager.class.getDeclaredMethod("addAssetPath", String.class)
                        .invoke(assetManager, mPluginPath);
            } catch (InvocationTargetException e) {
                assetManager = null;
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                assetManager = null;
                e.printStackTrace();
            }
        }

    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    Resources resources;
    if(assetManager != null) {
        resources = new Resources(assetManager,
                context.getResources().getDisplayMetrics(),
                context.getResources().getConfiguration());
    } else {
        resources = context.getResources();
    }
    return resources;
}
复制代码

这样,我们就可以通过构造出来的Resources访问plugin上的资源。 但是在实际的操作中,我们发现获取了Resources还不能解决所有的资源访问问题。style和layout依然无法直接获取。

获取id

由于plugin不是已安装的apk,所以我们不能使用Resources的getIdentifier接口来直接获取资源id。但是我们知道,apk的资源在编译阶段都会生成R文件,所以我们可以通过反射plugin apk中的R文件的方式来获取id:

    /**
     * get resource id through reflect the R.java in plugin apk.
     * @param context the base context from app.
     * @param type res type
     * @param name res name
     * @return res id.
     */
    public int getIdentifier(Context context, String type, String name) {
        if(context == null) {
            Log.w(TAG, "getIdentifier: the context is null");
            return -1;
        }

        if(RES_TYPE_STYLEABLE.equals(type)) {
            return reflectIdForInt(context, type, name);
        }

        return getResources(context).getIdentifier(name, type, PLUGIN_RES_PACKAGE_NAME);
    }

    /**
     * get resource id array through reflect the R.java in plugin apk.
     * eg: get {@link R.styleable}
     * @param context he base context from app.
     * @param type res type
     * @param name res name
     * @return res id
     */
    public int[] getIdentifierArray(Context context, String type, String name) {
        if(context == null) {
            Log.w(TAG, "getIdentifierArray: the context is null");
            return null;
        }

        Object ids = reflectId(context, type, name);
        return  ids instanceof int[] ? (int[])ids : null;
    }

    private Object reflectId(Context context, String type, String name) {
        ClassLoader classLoader = context.getClassLoader();
        try {
            String clazzName = PLUGIN_RES_PACKAGE_NAME + ".R$" + type;
            Class<?> clazz = classLoader.loadClass(clazzName);
            if(clazz != null) {
                Field field = clazz.getField(name);
                field.setAccessible(true);
                return field.get(clazz);
            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return -1;
    }

    private int reflectIdForInt(Context context, String type, String name) {
        Object id = reflectId(context, type, name);
        return id instanceof Integer ? (int)id : -1;
    }
复制代码

构造LayoutInflater

我们都知道,获取layout资源无法直接通过Resources拿到,而需要通过LayoutInflater来获取。一般的方法:

LayoutInflater inflater = LayoutInflater.from(context); 
复制代码

而这里穿进去的context是应用传进来的context,很明显通过应用的context我们是无法获取到plugin里的资源的。那么我们如何获取plugin里的layout资源呢?是否需要构造一个自己的LayoutInflater和context?我们进一步去看一下源码:

public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
}
复制代码

我们可以看到是通过context的getsystemService方法来获取的LayoutInflater。进入到Context的源码我们可以看到Context是一个抽象类,并没有getSystemService的实现。那这个实现到底在那里呢?这里就不得不去研究一下Context的整个结构。

Context

我们可以看到我们平时用到的组件,基本都是出自于ContextWrapper,说明ContextWrapper是Context的一个基本实现。 我们看一下ContextWrapper获取SystemSerivce的方法:

@Override
    public Object getSystemService(String name) {
        return mBase.getSystemService(name);
}
复制代码

最终会通过mBase来获取。同样的我们还可以看到ContextWrapper里很多其他的方法也是代理给mBase去实现的。很明显这是一个装饰模式。ContextWrapper只是一个Decorator,真正的实现在mBase。那么我们看下mBase在哪里创建:

public ContextWrapper(Context base) {
        mBase = base;
    }
    
    /**
     * Set the base context for this ContextWrapper.  All calls will then be
     * delegated to the base context.  Throws
     * IllegalStateException if a base context has already been set.
     * 
     * @param base The new base context for this wrapper.
     */
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
}
复制代码

这里我们可以看到有两处可以传进来,一处是构造函数里,还有一处是通过attachBaseContext方法传进来。由于我们这里的lib主要针对Activity实现,所以我们需要看一下在Activity创建伊始,mBase是如何构建的。
我们都知道,Activity的创建是在ActivityThread的performLaunchActivity方法中:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...
    //创建base context
    ContextImpl appContext = createBaseContextForActivity(r);
    
    Activity activity = null;
    try {
        ...
        //创建activity
        activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
        ...
    } catch (Exception e) {
        ...
    }
    ...
    if (activity != null) {
        ...
        //绑定base context到activity
        activity.attach(appContext, this, getInstrumentation(), r.token,
                    r.ident, app, r.intent, r.activityInfo, title, r.parent,
                    r.embeddedID, r.lastNonConfigurationInstances, config,
                    r.referrer, r.voiceInteractor, window, r.configCallback);
        ...
    }
    ...
}
复制代码

在上述代码中,创建了一个ContextImpl对象,以及一个Activity对象,并且将ContextImpl作为base context通过activity的attach方法传给了Activity:

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback) {
    attachBaseContext(context);
    ...
}
复制代码

还记得我们在前面提到的ContextWrapper中构建mBase的两个地方,一个是构造函数,还有一个便是attachBaseContext方法。所以至此我们可以解答前面疑惑的mBase对象是什么了,原来就是在ActivityThread创建Activity的同时创建的ContextImpl对象。也就是说,其实contexImpl是context的真正实现。
回到我们前面讨论的getSystemService问题,我们到/frameworks/base/core/java/android/app/ContextImpl.java中看:

    @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }
复制代码

再转到/frameworks/base/core/java/android/app/SystemServiceRegistry.java,我们找到注册LayoutInflater服务的地方:

registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
        new CachedServiceFetcher<LayoutInflater>() {
        @Override
        public LayoutInflater createService(ContextImpl ctx) {
        return new PhoneLayoutInflater(ctx.getOuterContext());
}});
复制代码

这里我们可以看到系统的LayoutInflater实现其实是PhoneLayoutInflater。这样好办了。我们可以仿造PhoneLayoutInflater构造一个PluginLayoutInflater就好了。但是在前面的讨论中我们知道LayoutInflater是无法直接创建的,而是通过Context间接创建的,所以这里我们还需要构造一个PluginContext,仿造原有的Context的方式,在getSystemService中返回我们的PluginLayoutInflater。具体实现如下:
PluginContextWrapper:

public class PluginContextWrapper extends ContextWrapper {
    private Resources mResource;
    private Resources.Theme mTheme;

    public PluginContextWrapper(Context base) {
        super(base);
        mResource = PluginResLoader.getsInstance().getResources(base);
        mTheme = PLuginResLoader.getsInstance().getTheme(base);
    }

    @Override
    public Resources getResources() {
        return mResource;
    }

    @Override
    public Resources.Theme getTheme() {
        return mTheme;
    }

    @Override
    public Object getSystemService(String name) {
        if(Context.LAYOUT_INFLATER_SERVICE.equals(name)) {
            return new PluginLayoutInflater(this);
        }
        return super.getSystemService(name);
    }
}
复制代码

PluginLayoutInflater:

public class PluginLayoutInflater extends LayoutInflater {

    private static final String[] sClassPrefixList = {
            "android.widget",
            "android.webkit",
            "android.app"
    };

    protected PluginLayoutInflater(Context context) {
        super(context);
    }

    protected PluginLayoutInflater(LayoutInflater original, Context newContext) {
        super(original, newContext);
    }

    @Override
    public LayoutInflater cloneInContext(Context newContext) {
        return new PluginLayoutInflater(this, newContext);
    }

    @Override
    protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
        for (String prefix : sClassPrefixList) {
            View view = createView(name, prefix, attrs);
            if(view != null) {
                return view;
            }
        }
        return super.onCreateView(name, attrs);

    }
}
复制代码

PluginResLoader:

public Context getContext(Context base) {
    return new PluginContextWrapper(base);
}
复制代码

这样在plugin中如果需要加载布局只需要以这样的方式即可加载:

LayoutInflater inflater = LayoutInflater.from(
                        PluginResLoader.getsInstance().getContext(context));
复制代码

其中应用的传过来的context可以作为pluginContext的base,这样我们可以获取很多应用的信息。

构造plugin主题

我们都知道主题是一系列style的集合。而一般我们设置主题的范围是app或者是Activity,但是如果我们只纯粹希望Theme单纯应用于我们的lib,而我们的lib并不是一个app或者是一个Activity,我们希望我们的lib能够有一个统一的风格。那怎么样构建主题并且应用与plugin呢? 首先我们需要看下在Google的原声控件里是如何读取主题定义的属性的,比如在 /frameworks/base/core/java/android/widget/Toolbar.java 控件里是这样读取的:

public Toolbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Toolbar,
                defStyleAttr, defStyleRes);
        ...
}
复制代码

这里的attrs在两参构造函数里传进来:

public Toolbar(Context context, AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.toolbarStyle);
}
复制代码

这里我们读到两个关键信息,Toolbar的风格属性是toolbarStyle,而控件通过属性解析资源的方式是通过context.obtainStyledAttributes拿到TypedArray来获取资源。
那Google又是在哪里定义了toolbarStyle的呢?查看Goolge的资源代码,我们找到在/frameworks/base/core/res/res/values/themes_material.xml 中:

<style name="Theme.Material">
    ...
    <item name="toolbarStyle">@style/Widget.Material.Toolbar</item>
    ...
</style>
复制代码

在Theme.Materail主题下定义了toolbarStyle的风格。这里顺便提一下,/frameworks/base/core/res/res/values/themes_material.xml 是Google专门为material风格设立的主题文件,当然values下的所有文件都可以合为一个,但是很明显这样分开存储会在代码结构上清晰许多。同样的在/frameworks/base/core/res/res/values/themes.xml 文件下的Theme主题下也定义了toolbarStyle,这是Android的默认主题,也是所有主题的祖先。关于toolbarStyle的各个主题下的定义,这里就不一一列举了,感兴趣的童鞋可以直接到源码里看。 到这里framework把控件接口做好,应用只需要在AndroidManifest.xml文件里配置Activity或者Application的主题:

<activity android:name=".MainActivity"
            android:theme="@android:style/Theme.Material">
复制代码

就可以将Activity界面应用于Material主题,从而Toolbar控件就会选取Material主题配置的资源来适配。这样,就可以达到资源和代码的完全解耦,不需要改动代码,只需要配置多套资源(比如设置Holo主题,Material主题等等),就可以让界面显示成完全不同的样式。
现在我们回头看context.obtainStyleAtrributes方法,我们去具体看下这个方法的实现:

public final TypedArray obtainStyledAttributes(
        AttributeSet set, @StyleableRes int[] attrs, @AttrRes int defStyleAttr,
        @StyleRes int defStyleRes) {
    return getTheme().obtainStyledAttributes(
        set, attrs, defStyleAttr, defStyleRes);
}
复制代码

这里可以看到最终是通过getTheme来实现的,也就是最终解析属性是交给Theme来做的。这里就可以看到和主题的关联了。那么我们继续往下看下获取到的主题是什么,Activity的getTheme实现在/frameworks/base/core/java/android/view/ContextThemeWrapper.java:

    @Override
    public Resources.Theme getTheme() {
        ...
        initializeTheme();
        return mTheme;
    }
复制代码

这里theme的创建在initializeTheme方法里:

    private void initializeTheme() {
        final boolean first = mTheme == null;
        if (first) {
            
            //创建主题
            mTheme = getResources().newTheme();
            ...
        }
        
        //适配特定主题style
        onApplyThemeResource(mTheme, mThemeResource, first);
    }
    
    protected void onApplyThemeResource(Resources.Theme theme, int resId, boolean first) {
        theme.applyStyle(resId, true);
    }
复制代码

这里我们就可以看到theme的创建是通过Resources的newTheme()方法来创建的,并且通过theme.applyStyle方法将对应的theme资源设置到theme对象中。
至此,我们已经知道如何构建一个theme了,那么怎么获取themeId呢?
我们知道framework是通过读取Activity或Application设置的theme白噢钱来设置theme对象的,那我们的plugin是否也可以在AndroidManifest.xml文件里读取这样类似的标签呢?答案是肯定的。
在AndroidManifest.xml里,还有个metadata元素可以配置。metadata是一个非常好的资源代码解耦方式,在metadata里配置的都是字符串,不管是否存在plugin,都不会影响app的编译及运行,因为metadata的解析都在plugin端。

<meta-data
        android:name="plugin-theme"
        android:value="pluginDefaulTheme"/>
复制代码

然后,我们我们就可以得出构建plugin主题的方案了:

即,

  1. 解析应用在AndroidManifest文件中配置的metadata数据。
  2. 获取到对应应用设置的Theme。
  3. 拿到Theme的styleId
  4. 构造Theme。

这样我们就可以构造统一风格的plugin了。 具体的实现代码如下:
PluginResLoader:

    public Resources.Theme getTheme(Context context) {
        if(context == null) {
            return null;
        }

        Resources.Theme theme = context.getTheme();
        String themeFromMetaData = null;
        if(context instanceof Activity) {
            Activity activity = (Activity)context;
            try {
                ActivityInfo info = activity.getPackageManager().getActivityInfo(activity.getComponentName(),
                        PackageManager.GET_META_DATA);
                if(info != null && info.metaData != null) {
                    themeFromMetaData = info.metaData.getString(THEME_METADATA_NAME);
                }
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
        } else {
           //the context is not Activity, 
           //get metadata from Application.
            try {
                ApplicationInfo appInfo = context.getPackageManager()
                        .getApplicationInfo(context.getPackageName(),
                                PackageManager.GET_META_DATA);
                if(appInfo != null && appInfo.metaData != null) {
                    themeFromMetaData = appInfo.metaData.getString(THEME_METADATA_NAME);
                }
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
        }

        if(themeFromMetaData == null) {
            //get theme from metadata fail, return the theme from baseContext.
            return theme;
        }

        int themeId = -1;
        if(WIDGET_THEME_NAME.equals(themeFromMetaData)) {
            themeId = getIdentifier(context, "style", WIDGET_THEME_NAME);
        } else {
            Log.w(TAG, "getTheme: the theme from metadata is wrong");
        }

        if(themeId >= 0) {
            theme = getResources(context).newTheme();
            theme.applyStyle(themeId, true);
        }
        return theme;
    }
复制代码
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
.NET Framework 4.5-x86-x64是一种由微软公司开发的应用程序框架。它提供了一种创建和运行各种类型应用程序的环境,包括桌面应用程序、Web应用程序和移动应用程序。 使用.NET Framework 4.5-x86-x64,开发人员可以利用其强大的功能来创建功能丰富、高性能的应用程序。它提供了一系列的类库和工具,可以轻松地处理各种任务,例如图形处理、数据访问和网络通信。它还支持多种编程语言,包括C#、Visual Basic和F#等,使开发人员可以按照自己的喜好选择最适合自己的编程语言。 对于应用程序的部署,.NET Framework 4.5-x86-x64提供了高度灵活性和可移植性。开发人员可以将应用程序打包成自包含的可执行文件,方便用户在不同计算机上安装和运行。此外,.NET Framework 4.5-x86-x64还支持与现有系统和应用程序的集成,使应用程序能够与其他软件进行无缝协作。 作为一个跨平台的框架,.NET Framework 4.5-x86-x64可以在多个操作系统上运行,包括Windows、Linux和macOS等。这为开发人员提供了更大的灵活性和选择性,使他们可以根据不同的需求和目标平台选择最适合的环境。 总之,.NET Framework 4.5-x86-x64是一个功能强大、灵活性高的应用程序框架,它为开发人员提供了丰富的工具和资源,使他们能够轻松地创建出各种类型的应用程序,并在不同平台上进行部署和运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值