动态加载与插件化

  插件化备忘

1、  概述

  当一个软件项目开发结束并交互使用后,须要添加一些新的功能,咱们一般但愿在不修改原有的应用程序状况下,将新添加的功能植入到系统中,这就是所谓的插件化,新增长的功能模块就叫插件。插件化能大大的下降模块间的耦合性,有利于各模块的独立维护,加快项目的维护更新。这里记录了下,主流的集中语言,实现插件化的方法。java

2、  Java、Net和c/c++ 动态加载方式

1. Java

  Java的动态加载主要是靠classloader实现的.顾名思义,ClassLoader就是用来Load Class的,当一个Class被加载的时候,这个Class所引用到的全部Class也会被加载,并且这种加载是递归的,也就是说,若是A引用到B,B 引用到C,那么当A被加载的时候,B也会被加载,而B被加载的时候,C也会加载。如此递归直到全部须要的Class都加载好。特别注意ClassLoader双亲委派机制:ClassLoader老是先请求其父ClassLoader查找Class在自身查找Class。具体参考:http://yiminghe.iteye.com/blog/267959android

因此能够实现ClassLoader来实现Class的动态加载,不过java已经提供了现成的ClassLoader实现类:URLClassLoader能够加载URL所指定的jar。c++

 

示例:windows

有一个plugin.jar里面有一个实现IPlugin接口的类com.test.TestPluginapi

ClassLoader loader = new URLClassLoader(new URL[]{new URL("file://jar路劲\plugin.jar")});
Class<?> pluginCls = loader.loadClass("com.test.TestPlugin");
IPlugin plugin = (IPlugin)pluginCls. newInstance();
plugin.load();

 

PS:这只是范例代码还有不少注意点,好比jar中不能包含IPlugin接口,确保URLClassLoader的parent中包含IPlugin。如何导出jar,就不叙述了。上诉就是java的动态加载,在这基础上添加插件管理,版本控制(如在zip文件结构中打入插件信息,在android那块会具体说明)。app

2. Net

  Net的代码是以exe与dll的组织形式发布的,而咱们经常使用的插件加载形式为exe和dll。果开发进程级别的插件能够考虑以exe的形式组织插件(java也同样),你能够以建立,终止进程的方式,随意加载,卸载所需插件。不过这种方式要多考虑多进程通讯方面的问题,这里不讨论这种形式,主要关注进程内dll形式的插件实现使用的方法。先看下net的一些基本概念,如应用程序域,程序集。函数

应用程序域:性能

  应用程序域是一种边界,它由公共语言运行库围绕同一应用程序范围内建立的对象创建(即,从应用程序入口点开始,沿着对象激活的序列的任何位置)。应用程序域有助于将在一个应用程序中建立的对象与在其余应用程序中建立的对象隔离,以使运行时行为能够预知。在一个单独的进程中能够存在多个应用程序域。在net中加载到应用程序域中的dll是不能卸载,咱们能够每次建立一个新的应用程序域加载dll,完成逻辑后卸载这个应用程序域。优化

应用程序集:spa

  一个.NET应用程序能够由多个程序集拼装而成的。程序集,简单来讲,就是一个以公共语言运行库(CLR)为宿主的、版本化的、自描述的二进制文件。尽管显示中.NET程序集和以往Win32二进制文件(包括遗留的COM服务对象)的文件扩展名(*.exe或*.dll)彻底相同,可是二者的内部构成几乎彻底不一样。程序集能够促进代码重用、肯定类型边界、可版本化的单元、自描述的、可配置的。

Net主要靠Assembly实现dll的动态加载, Assembly由多个加载dll的static方法,这边只关注加载指定路径下dll的方法LoadFile

 

示例:

有一个plugin.dll 里面有一个实现IPlugin接口的类Test.Plugins.TestPlugin

Assembly assembly = Assembly.LoadFile("dll的路径\plugin.dll");
Type type = Assembly.GetType("Test.Plugins.TestPlugin");
IPlugin plugin = Activator. CreateInstance(type) as IPlugin;
plugin.Load();

 

PS若是单纯这样加载的dll实在默认的应用程序域,没法被卸载的,若是须要卸载dll能够新建一个应用程序域去加载,不用时卸载这个应用程序域。

3. c/c++

  c/c++就没有标准的动态加载的实现方式了,依平台而定这边记录了下Liunx与windows。程序有两种连接方式,一种是静态连接,一种是动态连接。了解c/c++的动态加载前,先看下这两个概念。

  所谓静态连接就是在编译连接时直接将须要的执行代码拷贝到调用处,优势就是在程序发布的时候就不须要的依赖库,也就是再也不须要带着库一块发布,程序能够独立执行,可是体积可能会相对大一些。(所谓库就是一些功能代码通过编译链接后的可执行形式。)

  所谓动态连接就是在编译的时候不直接拷贝可执行代码,而是经过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操做系统,操做系统负责将须要的动态库加载到内存中,而后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时链接的目的。优势是多个程序能够共享同一段代码,而不须要在磁盘上存储多个拷贝,缺点是因为是运行时加载,可能会影响程序的前期执行性能。

  Windows下是以dll形式的(和net的dll有必定的区别),而liunx则是以so的形式。虽然Windows和Liunx的API有一点差别,可是大致的机制仍是一致。都是分加载动态连接库,查找指定符号地址,调用指定过程,完成释放动态连接库。

  下面先来看下windows的,windows提供了多个API来实现动态加载动态连接库和调用里面的方法。

  LoadLibrary 加载指定的动态连接库得到对应的模块句柄(还有其余高级版本)

  GetProcAddress 按照指定的符号名称在加载的动态连接库中查找函数地址

  FreeLibrary 释放加载的动态连接库

 

示例:

  有一个动态连接库plugin.dll,里面有一个插件接口IPlugin、一个插件实现类TestPlugin和一个导出函数int CreatePlugin(IPlugin** outPlugin)

 

typedef int (*CreatePlugin)(IPlugin** outPlugin);
HMODULE handle = LoadLibrary(L"dll所在目录\plugin.dll");

if (handle == nullptr)
{
    printf_s("load dll fail");
    return -1;
}

CreatePlugin create = (CreatePlugin)GetProcAddress(handle, "CreatePlugin");

if (create == nullptr)
{
    printf_s("get proc address fail");
    return -1;
}

IPlugin* plugin = nullptr;
Int res = create(&plugin);

if (res!=0 || plugin == nullptr)
{
     printf_s("load plugin fail");
     return -1;
}

plugin->Load();
FreeLibrary(handle);

 

PS通常dll都是导出函数,因此都是先导出指定函数,做为建立导出类的入口点,后续类的建立都依赖这个入口点。Dll如何实现函数如何导出这里就不说了。

 

说完windows咱们在看下liunx,其实和加载调用过程和windows差很少的,liunx提供了:

dlopen 按指定模式加载指定路径下的动态库。

dlsym按照指定的符号名称在加载的动态连接库中查找函数地址

dlclose 关闭释放动态连接库

 

示例:

和windows同样的状况,有一个动态连接库plugin.so,里面有一个插件接口IPlugin、一个插件实现类TestPlugin和一个导出函数int CreatePlugin(IPlugin** outPlugin)

typedef int (*CreatePlugin)(IPlugin** outPlugin);
void* handle = dlopen("so所在目录/plugin.so", RTLD_LAZY);

if (handle ==NULL)
{
     printf("load dll fail");
     return -1;
}

CreatePlugin create = (CreatePlugin)dlsym(handle, "CreatePlugin");

if (create == NULL)
{
     printf("get proc sym fail");
     return -1;
}

IPlugin* plugin = NULL;
Int res = create(&plugin);

if (res!=0 || plugin == NULL)
{
    printf("load plugin fail");
    return -1;
}

plugin->Load();
dlclose(handle);

 

PS:liunx和windows动态加载动态库的流程仍是很类似的,可是因为平台的差别,中间仍是有许多细节须要注意的,这边只记录大致的流程,细节后续的在讨论。

 

3、     Android与Windows Phone插件化

Android插件化

  看完上面这些,咱们再来看看,两个移动平台的状况,首先看下android,因为android的上层开发通常性基于java(那些游戏引擎暂时除外),因此动态加载和java仍是基本上同样的,可是因为Dalvik与java虚拟机的差别,实质仍是有一些差距,流程上大体是一致。

  Dalvik虚拟机和java虚拟机的差别导致,没法直接加载class文件,必须将class转化为dex,Dalvik才能去加载。因此在开发插件的步骤上咱们要多一步jar到dex的转化。在动态加载的api上,android提供两个加载dex的类DexClassLoader和PathClassLoader。

  DexClassLoader顾名思义就是加载Dex的ClassLoader,它能够加载dex文件或包含dex的jar和apk(dex必须是classes.dex这样命名并在根目录下),可是实际使用时发现有些机型没法加载直接dex形式,因此使用时建议用jar或apk形式。

  PathClassLoader看名字可能认为它就是加载指定路径下的模块,实际上PathClassLoader加载的路径已经被限定死,只能加载/data/app路径下的dex。因此通常状况下,都是使用DexClassLoader实现插件机制。

 

示例:

有一个plugin.jar里面有一个实现IPlugin接口的类com.test.TestPlugin

ClassLoader loader = (IPlugin) new DexClassLoader("jar所在的目录/plugin.jar","dextop目录","so的查找目录",父ClassLoader);
Class<?> pluginCls = loader.loadClass("com.test.TestPlugin");
IPlugin plugin = (IPlugin)pluginCls. newInstance();
plugin.load();

 

  到这里android初步的插件化是能够实现了,但是在使用的时候,会发现这样插件化,没法支持android的res机制,每次实现插件里面的UI只能靠code实现,真心不是通常的烦。那接下我来看下,有什么办法能将res也集成到插件里面。

  首先咱们先分析下,没法使用res的缘由,android项目在编译的时候会res进行一次编译,对资源进行优化,并为每个res下的资源都生成一个对应的id。在使用的时候靠这个id在运行时android会靠这个id和资源映射表arsc找到相应资源,所这样形成咱们在开发插件时,及时把res放到jar,android也没法按照id找到对应资源。这里的关键是Resource对象,在应用的那个Resource里面是找不到插件内带的那些资源的,因此若是咱们能构造一个和插件里面res关联的Resource,那么利用这个Resource(其实还包括Theme)咱们就能顺利访问到插件里的资源。

那如何建立插件的Resources呢,这里有两个方法:

  1.直接构造Resources咱们能够看下Resources的定义能够发现它有这么一个构造Resources(AssetManager, DisplayMetrics, Configuration)这里能够看出建立Resources所须要的三个参数,先看下后面两个参数这两个参数一个和显示分辨率相关一个是单纯的配置,天然咱们能够直接使用app的Resources里的这两个参数,那么AssetManager呢,这个就与资源相关了,显然咱们要建立一个与插件的资源相关联的的AssetManager,那咱们在看下AssetManager的定义能够发现一个主要的方法addAssetPath(String),看它的实现,这个String应该是对应资源所在的apk路径(即插件位置)这样咱们就能构建一个本身的AssetManager了。

  如今三个参数都全了,是否是能够构建出插件的Resources了,拿到这个Resources咱们就能访问里面的大部分资源了,对了时大部分资源,不是所有,这个后续会说明的,接下来先讲下下一个方法。

  2.经过某个方法直接加在apk得到这个apk的Context,那样我能就能拿到全部与这个Context相关的资源。这里咱们能够观察activity的启动过程,能够发现当某个apk中的组件第一被加载的时候都会去查找这个组件所在的Application是否已经被建立,若是没有被建立都会,先加载apk建立Application,这里android以Package Name为标识查找Application,查看这部分源码咱们会发现Application是由LoadedApk建立的,而LoadedApk是从ActivityThread的getPackageInfo开头的几个方法里,再仔细浏览的话,发现有个getPackageInfoNoCheck方法,须要得到一个applicationinfo为参数(还有别的参数,这个4如下与以上不一样),于彷佛咱们只要拿到Apk的ApplicationInfo就能建立Application并得到Resources,实际上也很接近了。

  可是是还有些问题要解决,若是咱们直接PackageManager().getPackageArchiveInfo得到PackageInfo得到里面的ApplicationInfo,进行上述操做,会坑爹发现报了个某某路劲没有访问权限的错误,这是因为LoadedApk在makeApplication时候,须要加载apk里的dex,这时所指定的dexopt路径是应用是没有权限访问。是否是感受到这里就坑了,只能默默的回去继续看源码,看着这茫茫的代码晕了一段时间后,会发现LoadedApk在建立加载dex前会判断mClassLoader是否是为null,若是不是就不会去加载dex,如今好了咱们只要本身去加载dex,再经过反射去设置mClassLoader这一步就过了。

  再拿到这个Application后,其实咱们基本是用它的Resources,而不是直接用这个Application去建立View,因为这个Application是未安装的,若是你用这个Application建立EditText它去访问剪切板服务就GameOver了。

Windows Phone

  过完android,再来看下Windows Phone,因为Windows Phone生态系统的性质,微软为了杜绝未经审核的应用在商店上线,防止恶意的程序对Windows Phone的破坏,动态加载这部分API是被严格限制的。

  先来看下Windows Phone 下的net,实际上和PC上的net同样也是由Assembly的Load方法进行动态加载,不过Windows Phone公开了一个带AssemblyName参数的重载,只能加载应用安装目录和GAC下的DLL流程上和net基本没差。

  再看下Windows Phone下的win32 API,Windows Phone的win32有公开动态加载动态库的API可是也和net同样作了限制LoadLibrary被LoadPackagedLibrary取代,只能加载安装目录下的动态库,而另外两个API与PC的一致。

  到这里能够发现微软已经隐藏了全部能够加载外部代码的API,那就没办法加载外部的代码了吗,若是咱们去查看Windows Phone的System32下的dll会发现,其实答案是否认的,仍是有办法加载的,虽说微软只公开的LoadPackagedLibrary,可是LoadLibrary仍是存在的,这里咱们就看看怎么去访问这个未公开的LoadLibrary。

在这以前咱们先看几个和后续操做有关的结构体

//线程环境块
typedef struct _TEB
{
    //BYTE fill[0x30]; x86
    NT_TIB nt_tib;
    PVOID EnvironmentPointer;
    CLIENT_ID id;
    PVOID ActiveRpcHandle;
    PVOID ThreadLocalStoragePointer;
    PEB* currentPEB;
}TEB;
 
//进程环境块
typedef struct _PEB
{
//  BYTE fill[0x0c]; x86
    BYTE Reserved1[2];
    BYTE BeingDebugged;
    BYTE Reserved2[1];
    PVOID Reserved3[2];
    PEB_LDR_DATA* ldr;
}PEB;

//进程加载的模块信息
typedef struct _PEB_LDR_DATA
{
//  BYTE fill[0x1c]; x86
    ULONG Length;
    BOOLEAN Initialized;
    PVOID SsHandle ;
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    MODULE_LIST_ENTRY* initModuleList;
}PEB_LDR_DATA;

 
//模块链表实体
typedef struct _MODULE_LIST_ENTRY
{
    struct  _MODULE_LIST_ENTRY* Flink;
    struct  _MODULE_LIST_ENTRY* Blink;
    DWORD* baseAddress;
}MODULE_LIST_ENTRY;

 

在这几个结构体里面就包含了我要找的LoadLibrary所在的模块句柄,既PEB的ldr这个链表里面。因此只要咱们能得到PEB就能在ldr里面找的已经加载的模块,那怎么得到PEB,看TEB咱们能够发现TEB里面包含PEB那又变成得到TEB,而得到TEB的API是公开的NtCurrentTeb,有了这个就好办了

HMODULE NativeInterop::getKernelModule()
{
  TEB* teb = NtCurrentTeb();
  return (HMODULE) teb->currentPEB->ldr->initModuleList->Flink->baseAddress;
}

这样就获取到LoadLibrary的所在模块的句柄了,不过这是默认这个模块加载顺序肯定的状况下,因此建议在加载模块名称的判断来肯定模块位置。

相关文章
相关标签/搜索