【第22期】观点:IT 行业加班,到底有没有价值?

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

原创 2015年11月21日 18:09:56

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

版权声明:本文为博主原创文章,未经博主允许不得转载。 举报

相关文章推荐

【Android】Android插件开发 —— 基础入门篇

Android插件开发 —— 基础入门篇1. 插件开发的三个角色 宿主App(PluginHost) 用户已经安装在手机上的应用,通过宿主可以加载插件,实现动态加载。 插件(Plugin) 用户尚...
  • H28496
  • H28496
  • 2015-11-17 22:36
  • 1060

【Android】Android插件开发 —— 打开插件的Activity(代理方式)

用代理的方式打开插件Activity的整体思想 1. 插件中的Activity由于没有在宿主的AndroidManifest.xml中注册,因此不能直接由宿主程序打开。但是,我们仍然可以通过DexCl...
  • H28496
  • H28496
  • 2015-12-28 00:18
  • 1441

Android插件开发初探——基础篇

Android插件开发初探对于Android的插件化其实已经讨论已久了,但是市面上还没有非常靠谱成熟的插件框架供我们使用。这里我们就尝试性的对比一下Java中,我们使用插件化该是一个怎么样的流程,且我...
  • yzzst
  • yzzst
  • 2015-05-08 16:49
  • 6093

用产品思维设计API(五)—— 安全,就只能用HTTPS?

用产品思维设计API(五)—— 安全,就只能用HTTPS?前言 最近公司内部在重构项目代码,包括API方向的重构,期间遇到了很多的问题,不由得让我重新思考了下。 - 一个优雅的API该如何设...
  • yzzst
  • yzzst
  • 2017-02-05 17:56
  • 1090

Android中插件开发篇之----类加载器

前言关于插件,已经在各大平台上出现过很多,eclipse插件、chrome插件、3dmax插件,所有这些插件大概都为了在一个主程序中实现比较通用的功能,把业务相关或者让可以让用户自定义扩展的功能不附加...

Android 使用动态加载框架DL进行插件化开发

概述: 随着应用的不断迭代,应用的体积不断增大,项目越来越臃肿,冗余增加.项目新功能的添加,无法确定与用户匹配性,发生严重异常往往牵一发而动全身,只能紧急发布补丁版本,强制用户进行更...

在AndroidManifest文件中注册Activity

所有的活动都要在AndroidManifest.xml中进行注册才能生效,那么我们现在就打开AndroidManifest.xml来给FirstActivity注册吧,代码如下所示:     pac...

注册Activity相关方法

我们知道,只要是新建的Activity都需要注册如果只是简单注册,这样就行了   记得一定要加那个“.” 如果你要让你新注册的Activity是运行后第一个出现的页面,就需要这样

一张图看懂Android注册登录+服务端

整个环境是运行在Android虚拟机+tomcat服务器+MySQL上面的。。。需要对服务端以及Android都要比较熟悉。才能够比较完整的配置下来。。。 Android端相关代码: 1.注册登录...

Android开发--创建Activity 要点、注册

1.一个Activity 是一个类。 且从Activity类继承。 2.要重写父类的方法 onCreate()方法。    载入布局。 3. 每一个Activity都要在Androi...
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)