【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. 尝试运行
- 导出Plugin_First的apk,重命名为“plugin.apk”放到sd卡的根目录。
- 运行PluginHost,在出现的界面中点击“打开插件按钮”。
如果报错了,正常。我们来看一下报的是什么错:
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. 总结
- 使用预注册的方式打开插件的Activity,或者Service等。可以在宿主程序中预先注册一个或多个组件供插件使用。
- 但这种方式很不灵活,如果一个插件需要10个Activity,而宿主中只预先注册了5个怎么办?当然可以使用Fragment来实现,不过会嵌套很多层Fragment。代码维护起来比较麻烦。
- 还有另外一个严重的不足:如果有两个插件,都需要用到Activity1。当第一个插件的类注入到PathCLassLoader后,打开第二个插件的Activity1时出现的是第一个插件的Activity1!这个问题的解决方式可以在注入之前,保留原始的数据。当加载第二个插件时,重新和原始数据合并后再注入。不过这样会很卡顿,尤其是插件比较大的时候。
- 另外一点,细心的读者可能发现插件的Activity没有使用.xml布局。这是因为宿主程序无法加载插件的res文件。关于这个问题将在后续的博客中介绍。
5. 源代码
http://download.csdn.net/detail/h28496/9288957
- 【Android】Android插件开发 —— 打开插件的Activity(预注册方式)
- Android插件开发 —— 通过预注册方式打开activity(记录我踩过的坑)
- 【Android】Android插件开发 —— 打开插件的Activity(代理方式)
- 【Android】Android插件开发 —— 打开插件的Activity(Hook系统方法)
- android插件开发-就是你了!启动吧!插件的activity(一)
- android插件开发-就是你了!启动吧!插件的activity(二)
- android插件开发——加载插件
- android 通过代理activity的方式实现插件化
- android 通过代理activity的方式实现插件化
- Android Small插件化框架解读——Activity注册和生命周期[阿里工程师分享]
- Android移动APP开发笔记——Cordova(PhoneGap)通过CordovaPlugin插件调用 Activity 实例
- android插件化开发activity篇
- Android应用程序插件化研究之Activity注册
- Android插件化开发 第四篇 [加载插件Activity]
- Android插件化开发 第四篇 [加载插件Activity]
- phoneGap+android 单插件和多插件的注册
- Android开发环境建立的难题,ADT插件方式。
- Android插件化开发—RePlugin插件化框架
- Linux下的proc目录详解
- 基于 platform 总线的设备驱动编写模式:
- ls- 查看linux 文件的大小
- 驱动模型
- C语言扫雷
- 【Android】Android插件开发 —— 打开插件的Activity(预注册方式)
- 利用linux 内核所提供的input子系统编写字符设备驱动的步骤
- linux 命令 多一个窗口
- 并发和竞态
- [笔记-架构探险]框架优化与功能扩展3.2.安全框架shiro、提供安全控制特性2-jsp页面标签和框架aop启用权限控制
- UVA - 253 Cube painting(骰子涂色)
- 此诊断出现在编译器生成的函数“CList<TYPE,ARG_TYPE> &CList<TYPE,ARG_TYPE>::operator =(const CList<TYPE,ARG_TYPE> &)”
- 内核定时器:
- Unable to start activity ComponentInfo 解决方法