【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插件开发---通过预注册方式打开插件中activity

  • 2017年08月11日 18:20
  • 6.76MB
  • 下载

【Android多模块构建】如何启动另外一个module中的Activity

一、多模块构建 比如下图是我们现在工程的结构 project ├─── setting.gradle ├─── build.gradle ├─── app │ └──...
  • colinandroid
  • colinandroid
  • 2017年05月27日 15:01
  • 2068

Android动态加载黑科技 动态创建Activity模式

基本信息 Author:kaedea GitHub:android-dynamical-loading 代理Activity模式的限制 还记得我们在代理Activity...
  • axi295309066
  • axi295309066
  • 2016年08月17日 17:00
  • 764

Android巧妙关闭SDK中的Activity

如何关闭SDK中Activity因为博主最近一直在做商业化相关的工作,需要写用于公司内部所有项目的广告SDK。 而广告SDK内部需要集成几家广告平台的SDK,这就避免不了和很多第三方SDK打交道。但...
  • MyLero
  • MyLero
  • 2017年03月24日 14:42
  • 740

Android插件化基础(4),动态启动插件中的Activity

Android插件化基础(4),动态启动插件中的ActivityAuthor:郑海波-莫川简介如何动态启动插件中的Activity呢?我们首先分析,启动插件中的Activity需要做那些准备? 1.插...
  • NUPTboyZHB
  • NUPTboyZHB
  • 2016年01月17日 14:09
  • 2681

Android中Activity触摸事件传递源码学习

Activity中的触摸事件传递对应两个方法:dispatchTouchEvent和onTouchEvent。
  • flyyyyyyyy_
  • flyyyyyyyy_
  • 2017年10月30日 20:07
  • 97

Android Studio 手动创建活动(Activity) 第一行代码 第二章

活动概念:是一种可以包含用户界面的组件,主要用于和用户进行交互 手动创建活动:新建一个Android项目,项目名为ActivityTest,包名使用默认值com.example.activi...
  • u012005313
  • u012005313
  • 2015年07月20日 16:41
  • 6524

android启动Activity的两种方法

有两种方法可以启动: 1. 先说在setClass启动一个Activity的方法吧:(显式调用---直接调用Activity的Class类) Intent intent = new Intent();...
  • lx627776548
  • lx627776548
  • 2016年08月13日 17:51
  • 12188

Android Activity页面加载时间性能分析,以及改进要点

在开发Android应用程序的过程中,经常会遇到App首页的加载时间慢,需要需要进行性能优化的问题。那么如何知道首页Activity的加载时间呢 ? 1. 当我们打开一个Activity的时候,log...
  • wangbaochu
  • wangbaochu
  • 2015年12月24日 21:29
  • 8117

Eclipse新建Activity并启用(二):启用新建Activity

启用新建的Activity需要下述三个步骤: 在本例中,通过点击主Activity下的一个按钮,启动新Activity界面 先在主Activity界面中添加一个按钮,并设置其监听器...
  • phenixyf
  • phenixyf
  • 2016年04月18日 18:15
  • 1100
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:【Android】Android插件开发 —— 打开插件的Activity(预注册方式)
举报原因:
原因补充:

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