插件化二(Android)

上一篇文章《插件化一(android)》里大概构思了下插件加载与校验的流程和一些大体设计,这次就具体展开,在《动态加载与插件化》里提到以apk形式开发带res资源的插件,这里也介绍下具体的实现方式。

插件信息规划

那我们就开始进入正题,在实现插件化的时候,我们都需要考虑,对于插件的描述信息(如插件名,插件版本等等),我们应该放在哪里。比如弄一个文件储存插件信息再和插件一起打个包,或如jar形式的可以直接在jar里添加插件信息,相信大家都能想到多种是实现方法。

在这里我则是将插件信息写在zip(即jar或apk)的末尾的comment里,即在不修改zip文件的结构的前提下,在zip结构中添加信息。为了后续插件开发方便我为写入插件信息的工作编写了个cmd 工具,也方便后续用ANT与Eclipse集成简化开发流程,以下是部分代码。

  1. int main(int argc, char* argv[])
  2. {
  3.    if (argc < 3)
  4.    {
  5.       printf("params count error");
  6.       return -1;
  7.    }
  8.    char* path = argv[1];
  9.    char* data = argv[2];
  10.  
  11.    FileInfo* file = open(path, "r+");//打开zip文件
  12.    if (file == NULL)
  13.    {
  14.       printf("open file error");
  15.       return -1;
  16.    }
  17.  
  18.    if (!isVaildZip(file))//校验是否是合法zip文件
  19.    {
  20.       printf("error zip file");
  21.       return -1;
  22.    }
  23.  
  24.    if (!writeComment(file, data, strlen(data)))//写入信息到zip文件中
  25.       addComment(file, data, strlen(data));
  26.  
  27.  
  28.    EndRecord* end = readZipEndRecord(file);
  29.  
  30.    if (end == NULL)
  31.    {
  32.       printf("read end Record error");
  33.       return -1;
  34.    }
  35.  
  36.    printf(" comment is %s", end->Comment);
  37.    return 0;
  38. }

插件的加载

讲完插件信息的规划,我们来看下插件化至关重要的一步插件的加载,在《动态加载与插件化》里介绍过Android动态加载的实现方式,对于纯代码的android插件加载,相对来说还是比较简单的一个DexClassLoader就搞定了,相信DexClassLoader网上很多介绍的文章,这里就不具体介绍了,直接上代码如下

  1. @Override
  2.    public void loadPluginPackage(Context context, PluginInfo info, PluginContextLoadCallBack callBack)
  3.    {
  4. //创建dexopt的目录DexClassLoader需要的
  5.        File file = new File(context.getFilesDir(), "dexopt");
  6.        if (!file.exists())
  7.       file.mkdirs();
  8.  
  9.        File pluginFile = new File(info.getPath());
  10.        File temp;
  11.        try
  12.        {
  13.       temp = StorageManager.create(context).createFile(info.getName()+".jar",StorageManager.Cache);
  14.       Logger.I("move plugin to run path");
  15.       IOManager.moveTo(pluginFile, temp);//把插件移到加载目录
  16.        }
  17.        catch (IOException e)
  18.        {
  19.       ExceptionUtils.handle(e);
  20.       if(callBack != null)
  21.           callBack.onError(ErrorInfo.Plugin_Load_Error, "move "+pluginFile.getPath()+" to temp path fail");
  22.       return;
  23.        }
  24.  
  25.        if (file != null)
  26.        {
  27. //加载插件,就这么一句别的你都可以忽略
  28.       _loader = new DexClassLoader(temp.getPath(), file.getPath(), null, context.getClassLoader());
  29.       _app = context;
  30.  
  31.       if (callBack != null)
  32.       {
  33.           callBack.onLoad(this);
  34.           return;
  35.       }
  36.        }
  37.  
  38.    }

过完纯代码的插件加载方式,那我们再来看下如何加载带res资源的apk形式的插件(PS:以下方式使用到多个android内部的api,兼容性未大规模测试过),在《动态加载与插件化》介绍到要使用Apk里的res那就必须要拿到这个APK对应的Resources对象,那里我介绍了两种获取Resources对象的方法,这里我着重讲下第二种(第一种以后再介绍^_^)。

那如何获得插件APK对应的Context呢,如果去研究Androidd的Activity的启动过程不难发现,Application(就是Context^_^)是由一个叫LoadedApk的对象创建的,LoadedApk有一个makeApplication方法有两个参数,boolean和Instrumentation,第一个参数是指定是否使用创建默认的Application,第二个参数是是一个环境对象,用于跟踪android个组件的创建,在android的测试框架中可能会接触到它。调用如下(某些版本里LoadedApk是个不公开的内部类,所以以反射方式调用,也建议以下所有访问内部api的都用反射方式调用,这样可以做多版本的兼容)

  1. // 创建apk的application
  2.  application = ReflectHelper.invoke(loadedApk, "makeApplication", new Class<?>[] { boolean.class, Instrumentation.class }, false, new Instrumentation());

现在我们知道Application是由LoadedApk创建,那LoadedApk对象我们又从哪里获得呢,查看Android的源码顺藤摸瓜,最终找到了ActivityThread. getPackageInfoNoCheck这个方法,这个方法在4.0以上的系统和4.0以下的系统,参数是不一样的.

4.0以上有两个参数,第一个是ApplicationInfo 就是对应APK的Application信息这个大家应该熟悉的,我们可以通过PackageManager.getPackageArchiveInfo这个方法传入apk路径和Flag参数获得PackageInfo,从PackageInfo里就能获得APK的Applicationinfo,然后用应用的Applicationinfo的参数替换一下如uid,datadir等.第二个是CompatibilityInfo, CompatibilityInfo是4.0以上才有的(4.0以下没有这个参数),包含和屏幕分辩率有关的信息,我们可以直接通过Resources对象获得Resources.getCompatibilityInfo()这样我们就凑齐参数了,可以放大了^_^,调用代码

  1. private void RelpacePluginInfo(PackageInfo info, Context context, String path)
  2.    {
  3.   info.applicationInfo.dataDir = context.getApplicationInfo().dataDir;
  4.   info.applicationInfo.publicSourceDir = path;
  5.   info.applicationInfo.sourceDir = path;
  6.   info.applicationInfo.uid = context.getApplicationInfo().uid;
  7.   info.applicationInfo.metaData = context.getApplicationInfo().metaData;
  8.   info.applicationInfo.nativeLibraryDir = context.getApplicationInfo().nativeLibraryDir;
  9.    }
  1. private void getPackageInfoNoCheck(final Context context, final ApplicationInfo info, final ResultCallBack<Object> callBack)
  2.    {
  3. //注意ActivityThread. getPackageInfoNoCheck必须在主线程调用
  4.   AsyncManager.postUI(new Runnable()
  5.   {
  6.       @Override
  7.       public void run()
  8.       {
  9.      final Object value = getPackageInfoNoCheck(context, info);
  10.      if (callBack != null)
  11.      {
  12.          if (value != null)
  13.         callBack.onCompleted(value);
  14.          else callBack.onError(CallBack.Error, "get loadedapk fail");
  15.      }
  16.       }
  17.   });
  18.    }
  19.  
  20.    private Object getPackageInfoNoCheck(Context context, ApplicationInfo info)
  21.    {
  22.   ActivityThread thread = ActivityThread.currentActivityThread();
  23.   Object value;
  24.   try
  25.   {
  26.       value = ReflectHelper.invoke(ActivityThread.class, "getPackageInfoNoCheck", new Class<?>[] { ApplicationInfo.class }, thread, info);
  27.       return value;
  28.   }
  29.   catch (Exception e)
  30.   {
  31.       ExceptionUtils.handle(e);
  32.       return thread.getPackageInfoNoCheck(info, context.getResources().getCompatibilityInfo());//如果调用一个参数的getPackageInfoNoCheck失败,就尝试访问两个参数的
  33.   }
  34.    }

现在万事OK不,但是当我们执行makeApplication时,Logcat无情的抛了个异常给我们,查看异常我们很容易发现原因,是某个目录没有访问的权限,是由于getPackageInfoNoCheck中创建LoadedApk时需要用DexClassLoader去加载Apk的代码,指定的路径无法访问。难道是死胡同一条,那接着去查看makeApplication的逻辑,可以发现如果LoadedApk里的mClassLoader这个字段如果不为null,LoadedApk就不会去重新创建,这样就给了我们机会,我们可以自己用DexClassLoader去加载Apk再通过反射设置给LoadedApk就骗过它了。

  1. ClassLoader loader = getApkClassLoader(currentApp, path);
  2.      if (!ReflectHelper.setValue(loadedApk, ClassLoader, loader))
  3.      {
  4.     Logger.E("set LoadedApk ClassLoader fail");
  5.     return null;
  6.      }

这样之后我们就能顺利调用LoadedApk的makeApplication方法创建Apk对应的Application,获得Application后我们是不是就可以随便访问Apk里的资源了呢,实际上不是这么容易在的,在《动态加载与插件化》里提到,如果直接用Application的LayoutInflater去创建View资源我们是可以顺利拿到View也可以创建,但是会有潜在问题,由于这个Context所对应的Apk没有安装,如果View里使用到系统服务(如剪切板),系统服务如果去按报名检索这个apk时,是无法找到的,那时候就直接GameOver,所以我们还要对LayoutInflater做一定的处理(其实就是保证具体的Context是安装了的那个),这个有两种方法,一种是替换LayoutInflater的反射mContext字段,还有种是通过LayoutInflater(LayoutInflater,Context)这个构造创建个新的LayoutInflater。在这之前我们要先构造个特殊的Context,一个包含宿主APK信息和插件资源信息的Context

如下

  1. public Context getPluginContext()
  2.     {
  3.    return new ContextWrapper(getAppContext())
  4.    {
  5.        @Override
  6.        public Resources getResources()
  7.        {
  8.       return getPluginAppContext().getResources();
  9.        }
  10.  
  11.        @Override
  12.        public Theme getTheme()
  13.        {
  14.       return getPluginAppContext().getTheme();
  15.        }
  16.  
  17.        @Override
  18.        public ClassLoader getClassLoader()
  19.        {
  20.            return getPluginClassLoader();
  21.        }
  22.  
  23.        @Override
  24.        public AssetManager getAssets()
  25.        {
  26.            return getPluginAppContext().getResources().getAssets();
  27.        }
  28.    };
  29.     }

接下来处理LayoutInflater

第一种方式

  1. LayoutInflater inflater = LayoutInflater.from(getPluginAppContext());
  2. if (ReflectHelper.setValueAll(inflater, "mContext",getPluginContext()))
  3.    Logger.I("set mContext suc");

第二种方式,略长。。。

  1.   public LayoutInflater getLayoutInflater()
  2.     {
  3.    if (getAppContext().equals(getPluginAppContext()))
  4.    {
  5.        return LayoutInflater.from(getPluginAppContext());
  6.    }
  7.  
  8.  
  9.    LayoutInflater inflater = buildLayoutInflater();
  10.    inflater.setFactory(new Factory()
  11.    {
  12.        @Override
  13.        public View onCreateView(String name, Context context, AttributeSet set)
  14.        {
  15.       View view = null;
  16.       Class<?> cls;
  17.       try
  18.       {
  19.               cls = getViewClass(name);
  20.               Constructor<?> constructor = cls.getConstructor(Context.class,AttributeSet.class);
  21.               view = (View)constructor.newInstance(context,set);
  22.       }
  23.       catch(ClassNotFoundException e)
  24.       {
  25.           e.printStackTrace();
  26.       }
  27.       catch (SecurityException e)
  28.       {
  29.           e.printStackTrace();
  30.       }
  31.       catch (NoSuchMethodException e)
  32.       {
  33.           e.printStackTrace();
  34.       }
  35.       catch (IllegalArgumentException e)
  36.       {
  37.           e.printStackTrace();
  38.       }
  39.       catch (InstantiationException e)
  40.       {
  41.           e.printStackTrace();
  42.       }
  43.       catch (IllegalAccessException e)
  44.       {
  45.           e.printStackTrace();
  46.       }
  47.       catch (InvocationTargetException e)
  48.       {
  49.           e.printStackTrace();
  50.       }
  51.  
  52.       return view;
  53.        }
  54.    });
  55.  
  56.    return inflater;
  57.     }
  58.  
  59.     Class<?> getViewClass(String name) throws ClassNotFoundException
  60.     {
  61.    if(-1 == name.indexOf("."))
  62.        return View.class.getClassLoader().loadClass("android.widget."+name);
  63.    else
  64.        return getPluginClassLoader().loadClass(name);
  65.     }
  66.  
  67.     LayoutInflater buildLayoutInflater()
  68.     {
  69.    return new LayoutInflater( LayoutInflater.from(getPluginAppContext()),getPluginContext())
  70.    {
  71.        @Override
  72.        public LayoutInflater cloneInContext(Context context)
  73.        {
  74.       return this;
  75.        }
  76.    };
  77.     }

到此我能就可以加载带资源的插件APK了,具体使用

  1. import com.joyreach.plugin.IActivityHost;
  2. import com.joyreach.plugin.IActivityLifeCycle;
  3. import com.joyreach.plugin.IPlugin;
  4. import com.joyreach.plugin.PluginContext;
  5. import android.app.Activity;
  6. import android.app.Dialog;
  7. import android.content.Context;
  8. import android.graphics.Color;
  9. import android.graphics.drawable.ColorDrawable;
  10. import android.view.Window;
  11. import android.widget.Toast;
  12. //插件类
  13. public class TestPlugin implements IPlugin,IActivityHost,IActivityLifeCycle
  14. {
  15.    PluginContext _context;
  16.  
  17.    @Override
  18.    public void onLoaded(PluginContext context)
  19.    {
  20.       _context = context;
  21.    }
  22.  
  23.    @Override
  24.    public void onUnloaded(PluginContext context)
  25.    {
  26.    }
  27.  
  28.    @Override
  29.    public void attach(Activity activity)
  30.    {
  31.       new TestDialog(activity, _context).show();
  32.    }
  33.  
  34.    @Override
  35.    public void dattach()
  36.    {
  37.    }
  38.  
  39.    @Override
  40.    public void onCreate(Activity activity)
  41.    {
  42.        Toast.makeText(activity, "onCreate", Toast.LENGTH_SHORT).show();
  43.    }
  44.  
  45.    @Override
  46.    public void onResume(Activity activity)
  47.    {
  48.        Toast.makeText(activity, "onResume", Toast.LENGTH_SHORT).show();
  49.    }
  50.  
  51.    @Override
  52.    public void onPause(Activity activity)
  53.    {
  54.        Toast.makeText(activity, "onPause", Toast.LENGTH_SHORT).show();
  55.    }
  56.  
  57.    @Override
  58.    public void onDestroy(Activity activity)
  59.    {
  60.        Toast.makeText(activity, "onDestroy", Toast.LENGTH_SHORT).show();
  61.    }
  62.  
  63.    class TestDialog extends Dialog
  64.    {
  65.        public TestDialog(Context context,PluginContext pluginContext)
  66.        {
  67.       super(context);
  68.  
  69.       getWindow().requestFeature(Window.FEATURE_NO_TITLE);
  70.       setContentView(pluginContext.getLayoutInflater().inflate(anye.plugin.R.layout.testdialog, null));
  71.       getWindow().setBackgroundDrawable(new ColorDrawable(Color.argb(0, 0, 0, 0)));
  72.        }
  73.  
  74.    }
  75. }
  1. Model.getInstance().getPluginSystem().loadPlugin("core", new PluginContext.PluginContextLoadCallBack()
  2.    {
  3.        @Override
  4.        public void onLoad(PluginContext context)
  5.        {
  6.       Toast.makeText(PluginTestActivity.this,
  7.          PluginManager.current().getInstallPlugins() + ":current load " + context.getInfo().getName() + "_" + context.getInfo().getVersonCode(),
  8.          Toast.LENGTH_LONG).show();
  9.  
  10.       PluginContext.PluginContainer container = context.load("plugin.test.main.TestPlugin");
  11.       container.load();
  12.  
  13.       IActivityHost host = container.asType();
  14.       host.attach(PluginTestActivity.this);
  15.  
  16.       Toast.makeText(PluginTestActivity.this,getResources().getString(R.string.title_activity_data_event_test),
  17.          Toast.LENGTH_LONG).show();
  18.        }
  19.  
  20.        @Override
  21.        public void onError(int code, String msg)
  22.        {
  23.       Toast.makeText(PluginTestActivity.this, "load fail: " + msg, Toast.LENGTH_LONG).show();
  24.        }
  25.    });

anye.plugin.R.layout.testdialog布局文件

运行效果

先到这里^_^,下次介绍插件整个加载流程,项目构成,可以洗洗睡了!!!!!!!!!!!

最新文章

  1. Android-简单的sdcard文件浏览
  2. 【SQLite】使用事务处理带参数的插入
  3. 基于visual Studio2013解决算法导论之007优先队列(堆实现)
  4. 【原】无脑操作:eclipse + maven搭建SSM框架
  5. Akka(26): Stream:异常处理-Exception handling
  6. Ubuntu系统安装Pyenv
  7. redhat 6 红帽6 Linux 网络配置
  8. HTML---引入css,js | 常用标签示例
  9. 初窥Java之四
  10. 依赖、耦合、解耦、控制反转(IOC)、依赖注入(DI)
  11. Visio 保存卡死解决办法
  12. OGG文件获取创建日期
  13. 4-windows 用cmd 如何输入命令 进入文件夹
  14. script 页面在指定位置加载
  15. Linux-数据库4
  16. BZOJ.2738.矩阵乘法(整体二分 二维树状数组)
  17. PL/SQL集合(一):记录类型(TYPE 类型名称 IS RECORD)
  18. 第138天:Web前端面试题总结(编程)
  19. perl6 单线程破解phpmyadmin脚本
  20. 0062 Spring MVC的文件上传与下载--MultipartFile--ResponseEntity

热门文章

  1. SQL基础问题整理
  2. 一个自己犯的react错误
  3. 修改MessageBox的标题的做法
  4. 字符串、对象、数组操作方法、json方法
  5. protobuf反射详解
  6. 科学的解决Http Token拦截器TokenInterceptor实现
  7. WPF 曲线图表控件(自制)(一)
  8. Uniform synchronization between multiple kernels running on single computer systems
  9. WPF图形/文字特别效果之一:交叉效果探讨(续)
  10. Bjarne Stroustrup语录2(一些C++使用注意点)