安卓动态加载技术

dynamic technology for android

Posted by fa1con on November 13, 2019

动态加载技术

动态加载apk

java中用继承自ClassLoader的类加载器,然后通过defineClass从二进制流中加载Class

android中派生出两个类DexClassLoader和PathClassLoader,本质上重载了findClass方法,Dalvik虚拟机识别的是dex文件

DexFile在加载类时,具体是调用成员方法loadClass或者loadClassBinaryName,loadClassBinaryName需要将包含名的类名中的“.”转换为“/”

1
2
3
4
public Class loadClass(String name, ClassLoader loader) {
        String slashName = name.replace('.', '/');
        return loadClassBinaryName(slashName, loader);
}

这两者的区别在于DexClassLoader需要提供一个可写的outpath路径,用来释放.apk包或者.jar包中的dex文件。换个说法来说,就是PathClassLoader不能主动从zip包中释放出dex,因此只支持直接操作dex格式文件,或者已经安装的apk(因为已经安装的apk在cache中存在缓存的dex文件)。而DexClassLoader可以支持.apk、.jar和.dex文件,并且会在指定的outpath路径释放出dex文件。

然后看一下实例: 首先要写一个要被加载的apk 定义一个接口 IShowToast.java

1
2
3
4
5
package com.dynamic;
import android.content.Context;
public interface IShowToast {
    int showToast(Context context);
}

还有一个具体实现的类 ShowToastImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 动态加载测试实现
 */

package com.dynamic;
import android.content.Context;
import android.widget.Toast;
import com.dynamic.IShowToast;
public class ShowToastImpl implements IShowToast {
    @Override
    public int showToast(Context context) {
        Toast.makeText(context, "我来自另一个dex文件", Toast.LENGTH_LONG).show();
        return 100;
    }
}

然后在android studio下编译成apk,这里名字为app-debug.apk

然后要写一个宿主apk,用它去加载前面的apk,前面加载的apk的包名最好和宿主apk包名保持一致(不知道会不会出问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.dynamic;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.nfc.Tag;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.dynamic.IShowToast;
import java.io.File;
import java.io.IOException;
import java.util.List;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private IShowToast lib;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button showBannerBtn = (Button) findViewById(R.id.button);
        showBannerBtn.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                loadDexClass();
            }
        });
    }
    /**
     * 加载dex文件中的class,并调用其中的showToast方法
     */
    private void loadDexClass() {
        String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "app-debug.apk";
        Log.i(TAG,dexPath);
        File dexOutputDir = getDir("dex", 0);
        DexClassLoader cl = new DexClassLoader(dexPath,dexOutputDir.getAbsolutePath(),null,getClassLoader());
        try {
            //该name就是internalPath路径下的dex文件里面的ShowToastImpl这个类的包名+类名
            Class<?> clz = cl.loadClass("com.dynamic.ShowToastImpl");
            IShowToast impl= (IShowToast) clz.newInstance();//通过该方法得到IShowToast类
            if (impl!=null)
                impl.showToast(this);//调用打开弹窗
        } catch (Exception e) {
            e.printStackTrace();
            Log.i(TAG,"加载app-debug.apk有问题");
        }
    }
}

最后是权限问题,非常重要: AndroidManifest.xml里添加

1
2
3
4
<!--往SDCard写入数据权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--从SDCard读取数据权限-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

然后就是app-debug.apk需要adb push到模拟器的/storage/emulated/0/demo链接

https://blog.csdn.net/jiangwei0910410003/article/details/17679823

Android动态加载Dex过程 类加载器用法与不同

动态加载资源(图片之类的)

需要解决的问题:将插件apk中的资源添加到宿主apk中,需要采用反射机制 为什么需要采用反射机制? 调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources中,由于addAssetPath是隐藏api无法直接调用,所以需要采用反射,所以将apk路径传递给assetManager,资源就加载到AssetManager中,然后再通过AssetManager创建一个新的Resources对象,这个对象就是我们可以使用的apk中的资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    public static Resources getSkinResource(Context context, String skinFilePath) {
        if (mSkinResource != null) {
            return mSkinResource;
        }
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.setAccessible(true);
            addAssetPath.invoke(assetManager, skinFilePath);
            Resources superRes = context.getResources();
            mSkinResource = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
            return mSkinResource;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

然后获取apk包名,这里自己手动写包名也可以

1
2
3
4
5
6
7
    public static String getPackageName(Context context, String apkFilePath) {
        PackageManager pm = context.getPackageManager();
        PackageInfo info = pm.getPackageArchiveInfo(apkFilePath,
                PackageManager.GET_ACTIVITIES);
        return info.packageName;
    }

然后是获取资源id,context.getResources()是当前apk的资源对象,通过getResourceEntryName(orginalResourceId)的方法拿到对应资源的id(注意:加载资源的名称和宿主app的名称保持一致,不然会无法编译app,名字不一样的情况暂时不知道怎么处理),type要和加载资源类型保持一致,比如drawable目录下的type是SkinManager.DRAWABLE,mipmap目录下的type是SkinManager.MIPMAP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    /**
     * 根据原项目中的图片id获取皮肤文件中的图片id
     *
     * @param context
     * @param skinResource
     * @param orginalResourceId
     * @param skinPackageName   皮肤包名
     * @param type              资源类型(mipmap,drawable,string,color)
     * @return
     */
    public static int getSkinResorceId(Context context, Resources skinResource, int orginalResourceId, String skinPackageName, String type) {
        //根据原资源文件id获取资源文件对应的名称
        String resourceEntryName = context.getResources().getResourceEntryName(orginalResourceId);
        Log.e("TEST", "entryName " + resourceEntryName);
        return skinResource.getIdentifier(resourceEntryName, type, skinPackageName);
    }

切记,授予app读写sd卡权限,不然获取包名的时候会返回空指针 AndroidManifest.xml里添加

1
2
3
4
<!--往SDCard写入数据权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--从SDCard读取数据权限-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

demo链接

Android动态加载插件资源 Android中插件开发篇之—-应用换肤原理解析