Android动态加载学习总结(二):资源访问

本文总结了Android动态加载中资源访问的问题,探讨了如何在不复制资源到宿主程序的情况下,通过PathClassLoader加载已安装apk的资源。文章介绍了插件的设计,包括MainActivity、UIUtil和AndroidManifest.xml的配置,并详细阐述了宿主程序的BaseActivity和MainActivity如何实现资源加载。实验结果显示成功访问了插件的资源,验证了解决方案的有效性。
摘要由CSDN通过智能技术生成

参考资料:
《Android开发艺术探索 –任玉刚》
Android中插件开发篇之—-应用换肤原理解析

博文涉及Demo:
宿主github地址:
https://github.com/codergz/ResourceLoading
插件github地址:
https://github.com/codergz/ResourceLoaderApk

前言:动态加载要解决的三个问题,分别是资源访问,Activity生命周期管理,类加载器的管理。前面一片文章总结了类加载器的学习,里面介绍了一些东西,对于管理类加载器还没有涉及。这篇是资源访问相关的学习,里面有用到类加载器文章中的PathClassLoader,如果对此没有什么了解的话,可以先去看看介绍Android动态加载学习总结(一):类加载器,本篇博文的Demo来自于博文
Android中插件开发篇之—-应用换肤原理解析,对于初步接触,为了更好的学习,我简略了一些内容。并且由于DexClassLoader的一些问题,我将博主的类加载方式更改成了PathClassLoader,既然是PathClassLoader,我们知道,这个类加载器只能加载dex文件,和已安装的apk文件,所以本篇博文的demo是访问已安装apk中的资源。

一、资源访问的问题

动态加载一个插件,如何访问它的资源?在我们宿主程序中,我们通过R文件访问资源,但是去访问插件的资源,明显是行不通的,我们在宿主程序中并没有插件的资源。

如果只是去解决资源访问的问题的话,我们的确有方法,比如提前在宿主程序中预置一份,那么我们就需要在一个插件发布的时候将资源复制到宿主程序。能解决资源访问吗?肯定可以,但是我们为什么要有插件化技术(动态加载)?为了减小宿主程序apk大小,为了降低宿主程序的更新频率,那么去复制到宿主程序明显违背了这项技术最初的目的。

那么我们的解决方案如下:

Context中有两个与资源有关的抽象方法:

public abstract AssetManager getAssets();
public abstract Resources getResources();

我们需要实现这两个方法,实现方式如下:

protected void loadResources(String dexPath) {
        //关于/assets目录下的文件,该目录下的文件不生成ID,如果我们要使用插件中该目录下的文件,我们需要指定文件的路径和文件名
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            //我们通过调用AssetManager中的addAssetPath方法,可以将一个apk中资源加载到Resources对象中,而addAssetPath是隐藏API,所以通过反射调用
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            //我们将APK的路径传给这个方法,资源便加载到AssetManager中
            addAssetPath.invoke(assetManager, dexPath);
            mAssetManager = assetManager;
        } catch (Exception e) {
            e.printStackTrace();
        }

        Resources superRes = super.getResources();
        superRes.getDisplayMetrics();
        superRes.getConfiguration();
        //通过AssetManger创建一个新的Resources对象,通过这个对象去访问插件资源
        mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
        mTheme = mResources.newTheme();
        mTheme.setTo(super.getTheme());
    }

    @Override
    public AssetManager getAssets() {
        return mAssetManager == null ? super.getAssets() : mAssetManager;
    }

    @Override
    public Resources getResources() {
        return mResources == null ? super.getResources() : mResources;
    }

    @Override
    public Resources.Theme getTheme() {
        return mTheme == null ? super.getTheme() : mTheme;
    }

}

二、插件的设计

我们已经知道了解决方案,那么开始插件设计,新建一个工程,命名为ResourcesLoaderApk1。

  • MainActivity
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

}
  • 主活动布局activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
   tools:context="com.example.gao.resourceloaderapk1.MainActivity"
    android:background="#998877" >


</RelativeLayout>
  • 类UIUtil
public class UIUtil {

    public static String getTextString(Context ctx){
        //在宿主程序中通过反射调用该方法得到本插件程序中的strings.xml中定义的app_name资源
        return ctx.getResources().getString(R.string.app_name);
    }

    public static Drawable getImageDrawable(Context ctx){
        //在宿主程序中通过反射调用该方法得到本插件程序中的icon图片资源
        return ctx.getResources().getDrawable(R.drawable.icon);
    }
}
  • AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.gao.resourceloaderapk1">

    <application android:allowBackup="true" android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher" android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>
    </application>

</manifest>

OK,插件设计完成,在模拟器中运行,也就相当于安装到了模拟器中,我们在Android动态加载学习总结(一):类加载器中已经知道,PathClassLoader可以加载dex文件和已安装的apk文件(其实是因为已安装的在cache中有缓存的dex文件),我们进入adb shell ,看看生成的apk的名字是什么(如果多次运行的话,这个名字会变的,建议修改的话,去看看名字变没有,我们要根据这个名字在宿主程序中进行加载)

  • 进入adb shell,并进入data/app目录

这里写图片描述

  • ls查看这个目录下的所有apk,并找到我们插件程序的apk名字
    这里写图片描述

    也就是说我们宿主程序中PathClassLoader要加载的apk的路径是”/data/app/com.example.gao.resourceloaderapk1-1.apk”这个在下面的宿主程序MainActivity的类加载部分中会用到,注意一下。

三、宿主程序的设计

  • BaseActivity(含有资源访问的解决方案,和上面的代码一样)
public class BaseActivity extends Activity {

    protected AssetManager mAssetManager;//资源管理器
    protected Resources mResources;//资源
    protected Resources.Theme mTheme;//主题


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    protected void loadResources(String dexPath) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, dexPath);
            mAssetManager = assetManager;
        } catch (Exception e) {
            e.printStackTrace();
        }
        Resources superRes = super.getResources();
        superRes.getDisplayMetrics();
        superRes.getConfiguration();
        mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
        mTheme = mResources.newTheme();
        mTheme.setTo(super.getTheme());
    }

    @Override
    public AssetManager getAssets() {
        return mAssetManager == null ? super.getAssets() : mAssetManager;
    }

    @Override
    public Resources getResources() {
        return mResources == null ? super.getResources() : mResources;
    }

    @Override
    public Resources.Theme getTheme() {
        return mTheme == null ? super.getTheme() : mTheme;
    }

}
  • MainActivity(其中PathClassLoader加载的代码和第一篇博客差不多,更改了需要加载的apk路径还有intent,注意AndroidManifext.xml中的action需要跟intent一致)
public class MainActivity extends BaseActivity {

    /**
     * 需要替换主题的控件
     * TextView,ImageView
     */
    private TextView textV;
    private ImageView imgV;

    /**
     * 更换控件的按钮
     */
    private Button btnChange;
    /**
     * 类加载器
     */
    protected PathClassLoader pc1 = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textV = (TextView)findViewById(R.id.text);
        imgV = (ImageView)findViewById(R.id.imageview);

        btnChange = (Button)findViewById(R.id.btn1);
        /**
         * 通过点击按钮,更新TextView和ImageView两个控件
         */
        btnChange.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View arg0) {

        /**使用PathClassLoader方法加载类*/

        //创建一个意图,用来找到指定的apk:这里的"com.example.gao.resourceloaderapk1"是指定apk中在AndroidMainfest.xml文件中定义的<action name="com.example.gao.resourceloaderapk1"/>
        Intent intent = new Intent("com.example.gao.resourceloaderapk1", null);
        //获得包管理器
        PackageManager pm = getPackageManager();
        List<ResolveInfo> resolveinfoes =  pm.queryIntentActivities(intent, 0);
        //获得指定的activity的信息
        ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;

        //获得apk的目录,这个目录在第二部分插件程序的设计末尾已得到
        final String apkPath = "/data/app/com.example.gao.resourceloaderapk1-1.apk";

        //native代码的目录
        String libPath = actInfo.applicationInfo.nativeLibraryDir;
        //创建类加载器,把dex加载到虚拟机中
        //第一个参数:是指定apk安装的路径,这个路径要注意只能是通过actInfo.applicationInfo.sourceDir来获取
        //第二个参数:是C/C++依赖的本地库文件目录,可以为null
        //第三个参数:是上一级的类加载器
        pc1 = new PathClassLoader(apkPath,libPath, MainActivity.this.getClassLoader());
         //调用父类的loadResources()方法
        loadResources(apkPath);
        setContent();
        }
     });
}

private void setContent(){
        try{
            Class clazz = pc1.loadClass("com.example.gao.resourceloaderapk1.UIUtil");
            /**
             * 通过反射调用插件中UIUtil类中的getTextString方法
             */
            Method method = clazz.getMethod("getTextString", Context.class);
            String str = (String)method.invoke(null, this);
            //更改宿主程序的TextView控件内容
            textV.setText(str);
            /**通过反射调用插件中UIUtil类中的getImageDrawable方法
            */
            method = clazz.getMethod("getImageDrawable", Context.class);
            Drawable drawable = (Drawable)method.invoke(null, this);
            //更改宿主程序的ImageView内容
            imgV.setImageDrawable(drawable);

        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
  • activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
  tools:context="com.example.resourceloader.MainActivity" >
     <LinearLayout 
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

           //更改TextView和ImageView的按钮
            <Button 
                android:id="@+id/btn1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginRight="10dp"
                android:text="主题1"/>
            //要更改的TextView,原显示“demo”
            <TextView 
                android:id="@+id/text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="demo"/>
            //要更改的ImageView,原显示"ic_launcher图片"
            <ImageView 
                android:id="@+id/imageview"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="20dp"
                android:src="@drawable/ic_launcher"/>

        </LinearLayout>
</RelativeLayout>
  • AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.gao.resourceloading">

    <application android:allowBackup="true" android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher" android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                //注意这个action,MainActivity中Intent那行代码与这个action保持一致
                <action android:name="com.example.gao.resourceloaderapk1"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

运行宿主程序,运行结果如下:
这里写图片描述

我们点击一下主题一的按钮,结果如下:

这里写图片描述

结果显示我们成功访问了插件程序的资源,TextView的内容由demo更改为了插件程序的app_name,ImageView的图片由宿主程序的ic_launcher图片改为了插件程序的icon图片。

在这之中,loadResources(String dexPath)方法起到了访问插件程序资源的作用,如果我们把MainActivity中按钮点击的代码中的loadResources(apkPath)注释掉呢?相当于我们只是通过动态加载和反射调用了插件程序的UIUtil类的以下两个方法

public static String getTextString(Context ctx){

        return ctx.getResources().getString(R.string.app_name);
    }

    public static Drawable getImageDrawable(Context ctx){
        return ctx.getResources().getDrawable(R.drawable.icon);
    }

我们并没有访问到插件程序的资源。那肯定用宿主程序的资源了,那么app_name,我们宿主程序也有,但是icon这个图片我们宿主程序并没有,所以把loadResources(apkPath)方法注释掉后,我们点击按钮,只会更改TextView这个内容,改成我们宿主程序的app_name这个字符串的内容,即ResourcesLoading,而图片并不会变。

结果如下:
这里写图片描述

四、总结:

在本文中通过PathClassLoader去动态加载已安装的apk,有一点限制是需要安装插件后才可以加载这个apk,当然,我们也可以通过DexClassLoader去加载这个apk,这样我们可以在插件未安装的情况下就去访问它的资源,那么根据这个,我们做一些更换主题,皮肤等等就有了解决方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值