【Android】Android插件开发 —— 打开插件的Activity(预注册方式)

来源:互联网 发布:怎么修改淘宝上的地址 编辑:程序博客网 时间:2024/06/06 04:40

Android插件开发 —— 打开插件的Activity(预注册方式)


1. 前言

据上一篇博客《Android插件开发 —— 基础入门篇》中所讲的,我们可以用DexClassLoader加载插件中的类。但如果就这样打开插件中的Activity是无法打开的。这一篇博客主要讲如何打开插件中的Activity。开发工具为Android Studio。

2. 尝试打开插件中的Activity

1. 新建一个插件接口Module,名为PluginSDK

Module类型为Android Library。
包名为:zhp.android.plugin.sdk
定义一个接口,IPlugin.java:

package zhp.android.plugin.sdk;import android.app.Activity;/** * 供宿主程序和插件使用的接口 */public interface IPlugin {    /**     * 供宿主回调的方法     */    void execute(Activity activity);}

2. 新建一个宿主Module,名为PluginHost

Module类型为Phone&Tablet module。
包名为:zhp.android.plugin.host
添加对PluginSDK的依赖。(如果不会用Android Studio添加依赖Module,参见上一篇博客)。
新建一个Activity名为MainActivity:

package zhp.android.plugin.host;import android.os.Environment;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import java.io.File;import dalvik.system.DexClassLoader;import dalvik.system.PathClassLoader;import zhp.android.plugin.sdk.IPlugin;/** * 宿主程序的MainActivity *  @author 郑海鹏 *  @since 2015/11/17 19:13 */public class MainActivity extends AppCompatActivity {    DexClassLoader classLoader;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        initClassLoader();    }    /**     * 初始化classLoader     */    private void initClassLoader() {        // 插件放在sd卡的根目录下        String apkPath = Environment.getExternalStorageDirectory() + File.separator + "plugin.apk";        // dex文件的释放目录        File releasePath = getDir("dexs", 0);        // 类加载器        classLoader = new DexClassLoader(apkPath, releasePath.getAbsolutePath(), null, getClassLoader());    }    /**     * 点击按钮以后打开插件     */    public void onClick(View view){        openPlugin();    }    /**     * 打开插件     */    private void openPlugin(){        try{            // 加载插件的入口类,并实例化出一个对象,回调execute()方法。            Class<?> pluginClass = classLoader.loadClass("zhp.android.plugin.first.Entrace");            IPlugin pluginObj = (IPlugin) pluginClass.newInstance();            pluginObj.execute(this);        }catch(Exception e){            e.printStackTrace();        }    }}

Activity的布局文件:

<?xml version="1.0" encoding="utf-8"?><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"    android:paddingLeft="@dimen/activity_horizontal_margin"    android:paddingRight="@dimen/activity_horizontal_margin"    android:paddingTop="@dimen/activity_vertical_margin"    android:paddingBottom="@dimen/activity_vertical_margin"    tools:context=".MainActivity">    <Button        android:text="打开插件"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:onClick="onClick"        android:layout_centerHorizontal="true"        android:layout_centerVertical="true"/></RelativeLayout>

在宿主Moduel中预注册Activity

在宿主Module的AndroidManifests.xml文件中注册一个Activity:

<activity android:name="zhp.android.plugin.activities.Activity1" />

文件读取的权限

因为我们要从sd卡加载插件,所以要在宿主Module的AndroidManifests.xml文件中添加文件读取的权限:

<!-- 往sdcard中写入数据的权限 --><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/><!-- 在sdcard中创建/删除文件的权限 --><uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>

3. 新建一个插件Module:名为Plugin_First

Module类型为Phone&Tablet module。
包名为:zhp.android.plugin.first
添加对PluginSDK的依赖。

1. 新建一个插件的Activity

新建一个package:zhp.android.plugin.activities ( 注意要和之前在宿主Module中预注册的一样 ↑ )
在zhp.android.plugin.activities包内新建一个Activity,名为Activity1(注意要和上面在宿主Module中预注册的一样):

package zhp.android.plugin.activities;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.widget.TextView;public class Activity1 extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        TextView tv = new TextView(this);        tv.setText("这里是插件的Activity1!");        setContentView(tv);    }}

2. 实现入口类

在包zhp.android.plugin.first下新建一个类Entrance:

package zhp.android.plugin.first;import android.app.Activity;import android.content.Intent;import android.util.Log;import dalvik.system.DexClassLoader;import zhp.android.plugin.activities.Activity1;import zhp.android.plugin.sdk.IPlugin;/*** 插件的入口类*/public class Entrace implements IPlugin{    @Override    public void execute(Activity activity) {        Log.i("郑海鹏", "Entrace#execute(): " + "插件已执行,正要打开Activity!");        activity.startActivity(new Intent(activity, Activity1.class));    }}

4. 尝试运行

  1. 导出Plugin_First的apk,重命名为“plugin.apk”放到sd卡的根目录。
  2. 运行PluginHost,在出现的界面中点击“打开插件按钮”。
  3. 如果报错了,正常。我们来看一下报的是什么错:

    java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{zhp.android.plugin.host/zhp.android.plugin.activities.Activity1}: java.lang.ClassNotFoundException: Didn’t find class “zhp.android.plugin.activities.Activity1” on path: DexPathList[[zip file “/data/app/zhp.android.plugin.host-1/base.apk”],nativeLibraryDirectories=[/vendor/lib, /system/lib]]

更奇怪的是,如果我们不实例化插件的Entrace类,直接加载Activity1的类用于startActivity。下面这段代码:

Class activityClass = classLoader.loadClass("zhp.android.plugin.activities.Activity1");Intent intent = new Intent(this, activityClass);startActivity(intent);

也会报上面的异常。

主要信息是:在DexPathList[[zip file “/data/app/zhp.android.plugin.host-1/base.apk”]这个路径下找不到我们的Activity1。
回顾一下,当我们使用DexClassLoader时,释放的dex放在哪儿的?上一篇博客的文末特意展示了dex文件的路径:data/data/包名/app_dexs文件夹下(这个路径是由DexClassLoader构造方法的参数决定的,详见上面 ↑)。
但是系统默认的加载路径里面并没有其他dex文件的路径。

(如果你的报错信息不是上面这个,可能你的项目有其他错误,请先解决它们(^_^))

知道问题所在之后,我们有两种方式解决这个问题:
1. 将DexClassLoader的DexPathList合并到系统默认的加载器PathCLassLoader的DexPathList中;
2. 使用MultiDexApplication。
第二种方式放在下下一篇博客中介绍(下一篇博客分享用代理的方式使用插件的Activity),本篇博客介绍第一种方式:变量注入的方式。

3. 将其它DexPathList注入到PathClassLoader中

这一节介绍如何将DexClassLoader的DexPathList注入到PathClassLoader中去。用的是反射的方式。
来看一下代码:

/** *  将 DexClassLoader 的 PathList 注入到 PathCLassLoader 中去。 */public void inject(PathClassLoader pathLoader, DexClassLoader dexLoader) {    try {        // 1. 获得PathList        Object pathLoaderPathList = getPathList(pathLoader);        Object dexLoaderPathList = getPathList(dexLoader);        // 2. 获得DexElements        Object pathDexElements = getDexElements(pathLoaderPathList);        Object dexDexElements = getDexElements(dexLoaderPathList);        // 3. 合并为新的DexElements        Object dexElements = combineArray(pathDexElements, dexDexElements);        // 4. 注入回pathLoader的PathList中去        setField(pathLoaderPathList, pathLoaderPathList.getClass(), "dexElements", dexElements);    } catch (Exception e) {        e.printStackTrace();    }}

这个方法包含了注入的整个流程。
完整的代码如下:

package zhp.android.plugin.host;import java.lang.reflect.Array;import java.lang.reflect.Field;import dalvik.system.DexClassLoader;import dalvik.system.PathClassLoader;/** * @author 郑海鹏 * @since 2015/11/20 17:03 */public class ClassInject {    /**     *  将DexClassLoader的PathList注入到PathCLassLoader中去。     */    public void inject(PathClassLoader pathLoader, DexClassLoader dexLoader) {        try {            // 1. 获得PathList            Object pathLoaderPathList = getPathList(pathLoader);            Object dexLoaderPathList = getPathList(dexLoader);            // 2. 获得DexElements            Object pathDexElements = getDexElements(pathLoaderPathList);            Object dexDexElements = getDexElements(dexLoaderPathList);            // 3. 合并为新的DexElements            Object dexElements = combineArray(pathDexElements, dexDexElements);            // 4. 注入回pathLoader的PathList中去            setField(pathLoaderPathList, pathLoaderPathList.getClass(), "dexElements", dexElements);        } catch (Exception e) {            e.printStackTrace();        }    }    /**     * 获得PathList     */    private Object getPathList(Object classLoader) throws IllegalArgumentException,            NoSuchFieldException, IllegalAccessException, ClassNotFoundException {        return getField(classLoader, Class.forName("dalvik.system.BaseDexClassLoader"),                "pathList");    }    /**     * 获得DexElements     */    private Object getDexElements(Object paramObject) throws IllegalArgumentException,            NoSuchFieldException, IllegalAccessException {        return getField(paramObject, paramObject.getClass(), "dexElements");    }    /**     * 获得obj对象的名为fieldName的变量的值。     * @param obj 要获取变量值的对象     * @param classObject   参数obj的类型。     *                      因为我们要找的那个变量可能是在obj.getClass()的父类中声明的。     *                      如果直接使用obj.getClass().getDeclaredField(fieldName)会     *                      抛出NoSuchFieldException异常。     * @param fieldName 变量的名字     */    private Object getField(Object obj, Class<?> classObject, String fieldName)            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {        Field localField = classObject.getDeclaredField(fieldName);        localField.setAccessible(true);        return localField.get(obj);    }    /**     * 将obj对象的field变量的值设置为value     */    private void setField(Object obj, Class<?> classObject, String field, Object value)            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {        Field localField = classObject.getDeclaredField(field);        localField.setAccessible(true);        localField.set(obj, value);    }    /**     * 合并两个数组     */    private Object combineArray(Object array1, Object array2) {        Class<?> localClass = array1.getClass().getComponentType();        int length1 = Array.getLength(array1);        int lengthSum = length1 + Array.getLength(array2);        // 新建一个数组,类型为localClass, 长度为两者之和。        Object result = Array.newInstance(localClass, lengthSum);        for (int k = 0; k < lengthSum; ++k) {            if (k < length1) {                Array.set(result, k, Array.get(array1, k));            } else {                Array.set(result, k, Array.get(array2, k - length1));            }        }        return result;    }}

4. 再次尝试打开插件中的Activity

现在我们修改Host的MainActivity中初始化classLoader的方法,加一条注入的语句,如下:

/** * 初始化classLoader */private void initClassLoader() {    // 插件放在sd卡的根目录下    String apkPath = Environment.getExternalStorageDirectory() + File.separator + "plugin.apk";    // dex文件的释放目录    File releasePath = getDir("dexs", 0);    // 类加载器    classLoader = new DexClassLoader(apkPath, releasePath.getAbsolutePath(), null, getClassLoader());    // 注入到原生的ClassLoader中    ClassInject inject = new ClassInject();    inject.inject((PathClassLoader)getClassLoader(), classLoader);}

再次执行,应该就没有问题了:
插件已被执行

5. 总结

  1. 使用预注册的方式打开插件的Activity,或者Service等。可以在宿主程序中预先注册一个或多个组件供插件使用。
  2. 但这种方式很不灵活,如果一个插件需要10个Activity,而宿主中只预先注册了5个怎么办?当然可以使用Fragment来实现,不过会嵌套很多层Fragment。代码维护起来比较麻烦。
  3. 还有另外一个严重的不足:如果有两个插件,都需要用到Activity1。当第一个插件的类注入到PathCLassLoader后,打开第二个插件的Activity1时出现的是第一个插件的Activity1!这个问题的解决方式可以在注入之前,保留原始的数据。当加载第二个插件时,重新和原始数据合并后再注入。不过这样会很卡顿,尤其是插件比较大的时候。
  4. 另外一点,细心的读者可能发现插件的Activity没有使用.xml布局。这是因为宿主程序无法加载插件的res文件。关于这个问题将在后续的博客中介绍。

5. 源代码

http://download.csdn.net/detail/h28496/9288957

0 0
原创粉丝点击