【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插件开发 —— 打开插件的Activity(代理方式)

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

【Android】Android插件开发 —— 打开插件的Activity(Hook系统方法)

Android打开插件中Activity的实现原理摘要Android打开插件Activity的方式有很多种,类名固定的可以使用预注册的方式。代理也是一种很好的方式,同时代理的方式也可以用于打开插件中的...
  • H28496
  • H28496
  • 2016-09-14 03:04
  • 1762

插件开发之360 DroidPlugin源码分析(四)Activity预注册占坑

请尊重分享成果,转载请注明出处: http://blog.csdn.net/hejjunlin/article/details/52258434 在了解系统的activity,service,bro...

插件开发之360 DroidPlugin源码分析(四)Activity预注册占坑

在了解系统的activity,service,broadcastReceiver的启动过程后,今天将分析下360 DroidPlugin是如何预注册占坑的?本篇文章主要分析Activity预注册占坑,...

android插件开发-就是你了!启动吧!插件的activity(一)

通过之前的例子例子,我们学习了如何寻找hook点,并且做一些非常无聊的事情。比如是的粘贴板放一句无聊的句子,或者让系统在启动一个activity时打印一句话。这些看似无聊的事情其实都是为了本节做铺垫。...

android插件开发-就是你了!启动吧!插件的activity(二)

这篇博客是上篇的延续,在阅读之前先阅读第一部分:第一部分 我们在启动插件的activity时,通过替换component成功欺骗AMS获得了启动一个activity所必须的一些资源。不过,我们还没有...

Android Small插件化框架解读——Activity注册和生命周期[阿里工程师分享]

阿里工程师分享Small插件化框架解读——Activity注册和生命周期。

android插件开发——加载插件

在阅读本博文的时候,我假设你已经阅读了我之前写的几篇。猛击此处通过前面的几篇博客,我们解决了如何启动一个并没有在ActivityManifest.xml中声明的activity。但是有很多细心的读者私...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

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