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

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

构造Resources

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

咱们能够看到构造函数里须要传入三个参数,AssetManager,DisplayMetrics,Configuration。后面两个参数咱们能够经过app传过来的context得到,因此如今问题能够转换为构造AssetManager。咱们发现AssetManager的构造函数并不公开,因此这里只能经过反射的方式构造。同时AssetManager还有一个接口能够直接传入资源的路径:

public int addAssetPath(String path)
复制代码

因此,综上所述,咱们能够获得构造Resources的方法:
PluginResLoader:web

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依然没法直接获取。bash

获取id

因为plugin不是已安装的apk,因此咱们不能使用Resources的getIdentifier接口来直接获取资源id。可是咱们知道,apk的资源在编译阶段都会生成R文件,因此咱们能够经过反射plugin apk中的R文件的方式来获取id:app

/**
     * 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来获取。通常的方法:ide

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在哪里建立:post

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方法中:ui

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;
    }
复制代码
相关文章
相关标签/搜索