動態加載與插件化

  插件化備忘

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的所在模塊的句柄了,不過這是默認這個模塊加載順序肯定的狀況下,因此建議在加載模塊名稱的判斷來肯定模塊位置。

相關文章
相關標籤/搜索