在過去近三十年的職業(yè)生涯里,有幾年專注于運行時環(huán)境的開發(fā)與實現(xiàn)。在runtime中,動態(tài)加載技術(shù)是其中的基石之一。動態(tài)加載技術(shù)是指在系統(tǒng)運行過程中,根據(jù)需要把程序和數(shù)據(jù)從外存或網(wǎng)絡(luò)加載到內(nèi)存中的過程。其中,lazy loading(懶加載),也被稱為延遲加載,是動態(tài)加載技術(shù)的一種常見實現(xiàn)方式。
圖片
1. 什么是動態(tài)加載
所謂動態(tài)加載,指的是程序在運行期間需要調(diào)用某一模塊的功能時,由加載器將該模塊即時載入內(nèi)存,進行相應(yīng)的重定位處理后將控制權(quán)交還調(diào)用程序。動態(tài)加載機制運用動態(tài)鏈接的原理使得系統(tǒng)具有動態(tài)的加載和動態(tài)解析的能力,模塊只有在被調(diào)用執(zhí)行時才被鏈接,進入系統(tǒng)執(zhí)行。
動態(tài)加載一般分為下載、加載和卸載三個操作,其中下載完成從遠程下載目標(biāo)模塊到本地,加載操作來完成讀入模塊到內(nèi)存,然后對模塊未解析的外部引用進行解析(一般地,也就是符號解析和重定位)使之可以運行的過程。當(dāng)模塊不再使用時就從內(nèi)存中卸載。
1.1 動態(tài)加載中的基本概念——模塊
模塊是數(shù)據(jù)說明、可執(zhí)行語句等程序?qū)ο蟮募?,它單獨命名而且可以通過名字來訪問,模塊設(shè)計者可以通過有選擇地在接口上輸出其特性以達到控制其特性的目的。沒被輸出的特性不能被模塊外部訪問,因此防止了模塊被誤用并且保證了模塊的封裝獨立性。
在模塊化編程思想中,把程序分割成若干個獨立的模塊,然后逐塊編程和獨立編譯,形成獨立的可加載模塊,模塊在被加載前保持本身的獨立性。一個應(yīng)用可以由多個獨立模塊組成,獨立的模塊構(gòu)成一個應(yīng)用有兩種方法:靜態(tài)鏈接和動態(tài)鏈接。靜態(tài)鏈接是獨立模塊事先鏈接好,在解決了所有的外部引用之后,編譯生成一個可執(zhí)行文件,隨后裝入內(nèi)存就可以執(zhí)行。進程在執(zhí)行的過程中,代碼段和數(shù)據(jù)段的位置都不能改變。
可加載模塊作為編譯單元被獨立編譯,這就意味著編譯器會核實一個引入模塊的每個引用同其根模塊的一致性。模塊鏈?zhǔn)撬斜患虞d的目標(biāo)模塊依據(jù)模塊之間的依賴關(guān)系被動態(tài)添加的一個鏈表,查詢時只需要對該鏈表進行查找。在動態(tài)加載中,模塊直到被載入前都保持獨立。
一個模塊可能與系統(tǒng)中的其他模塊無關(guān),也有可能與其中的一些模塊進行交互,模塊之間的交互就是模塊間的通信。
1.2 動態(tài)加載的技術(shù)基礎(chǔ)——動態(tài)鏈接
動態(tài)鏈接是系統(tǒng)在運行過程中根據(jù)需要把外部獨立模塊的可執(zhí)行代碼鏈接到系統(tǒng)中使之成為運行系統(tǒng)的一部分的過程。動態(tài)鏈接在執(zhí)行過程中,允許在它的地址空間中增加、清除、取代或重定位目標(biāo)模塊。換句話說,允許程序發(fā)生變化。在程序執(zhí)行的生命周期內(nèi),程序可以加入新的模塊、清除舊的模塊、甚至可以演變成一個完全不同的程序。
動態(tài)鏈接的一個典型應(yīng)用就是動態(tài)鏈接庫。動態(tài)鏈接庫是一個可以被其它應(yīng)用程序共享的程序模塊,其中封裝了一些可以被共享的例程和資源。
動態(tài)鏈接庫是從C語言函數(shù)庫和Pascal庫單元的概念發(fā)展而來的。動態(tài)鏈接庫不用重復(fù)編譯或鏈接,一旦裝入內(nèi)存, 庫中的函數(shù)可以被系統(tǒng)中任何正在運行的應(yīng)用程序所使用,而不必再將動態(tài)鏈接庫的另一拷貝裝入內(nèi)存。動態(tài)鏈接所調(diào)用的函數(shù)代碼并沒有被拷貝到應(yīng)用程序的可執(zhí)行文件中去,而是僅僅在其中加入了所調(diào)用函數(shù)的描述信息(往往是一些重定位信息)。只有當(dāng)應(yīng)用程序被裝入內(nèi)存開始運行時,在操作系統(tǒng)的管理下,才在應(yīng)用程序與相應(yīng)的動態(tài)鏈接庫之間建立鏈接關(guān)系。
采用這種方法,動態(tài)鏈接庫達到了復(fù)用代碼的極限。另一個方便之處是對動態(tài)鏈接庫中函數(shù)的修改可以自動傳播到所有調(diào)用它的程序中,而不必對程序作任何改動或處理。
以windows 為例,動態(tài)鏈接庫的實現(xiàn)方法主要有兩種:
① 加載時動態(tài)鏈接(Load-time Dynamic Linking)
這種用法的前提是在編譯之前已經(jīng)明確知道要調(diào)用DLL中的哪幾個函數(shù),編譯時在目標(biāo)文件中只保留必要的鏈接信息,而不含DLL函數(shù)的代碼;當(dāng)程序執(zhí)行時,利用鏈接信息加載DLL函數(shù)代碼并在內(nèi)存中將其鏈接入調(diào)用程序的執(zhí)行空間中,其主要目的是便于代碼共享。
②運行時動態(tài)鏈接(Run-time Dynamic Linking)
這種方式是指在編譯之前并不知道將會調(diào)用哪些DLL函數(shù),完全是在運行過程中根據(jù)需要決定應(yīng)調(diào)用哪個函數(shù),并用LoadLibrary和GetProcAddress動態(tài)獲得DLL函數(shù)的入口地址。
在C/C++中,動態(tài)加載的功能可以很容易地利用動態(tài)鏈接庫來實現(xiàn)。Win32 API函數(shù)LoadLibrary和FreeLibrary提供了在運行時刻加載新的功能模塊和釋放內(nèi)存空間的功能。需要被更新的功能模塊被封裝在動態(tài)連接庫中,主程序利用LoadLibrary函數(shù)裝載該動態(tài)鏈接庫,然后調(diào)用其中的功能模塊。需要更新某功能模塊的時候,首先終止運行該功能模塊,利用FreeLibrary函數(shù)卸載現(xiàn)有的動態(tài)鏈接庫,通過網(wǎng)絡(luò)或者是其他通訊端口將新的動態(tài)鏈接庫文件發(fā)送到指定目錄下,然后通過再次調(diào)用LoadLibrary函數(shù)裝載新的動態(tài)鏈接庫就可以完成動態(tài)加載新模塊的功能。
2. 動態(tài)加載的一般原理
動態(tài)加載機制所涉及到的關(guān)鍵問題包括加載模塊的格式、模塊間通信機制、符號管理等等。
可動態(tài)加載的模塊有很多種格式類型。比如DOS下的.COM文件和.EXE文件,UNIX 的下a.out文件、ELF文件,以及.OBJ文件等都可以作為可加載模塊被鏈接入系統(tǒng)。對于可重定位的模塊還要包括處理目標(biāo)代碼時所需的擴展符號和重定位信息,比如符號表、重定位信息和字符串表等。模塊間的通信方法可以參照進程間的通信方法,不同進程之間的通信有很多種方法:消息隊列、管道和共享內(nèi)存等。符號管理是設(shè)計與實現(xiàn)動態(tài)加載機制的前提,也是符號解析和重定位技術(shù)的基礎(chǔ)。目標(biāo)模塊按照不同的類型讀入內(nèi)存后,不能馬上就融入系統(tǒng)運行,動態(tài)加載機制還需要對其進行處理,也就是解決模塊中符號的外部引用(符號解析)和重定位,這是動態(tài)加載過程中最重要的一個步驟。
動態(tài)加載是通過把符號的外部參考插入到運行時鏈接的目標(biāo)文件中而實現(xiàn),具有兩個特點:
①動態(tài)的加載,就是當(dāng)這個運行的模塊在需要的時候才被映射入運行模塊的虛擬內(nèi)存空間中。如果一個模塊在運行中要調(diào)用到另一模塊中的函數(shù),而在沒有調(diào)用這個模塊中的其它函數(shù)之前,不會把該模塊加載到系統(tǒng)中(也就是內(nèi)存映射)。
②動態(tài)的解析,就是存在于另一模塊中的函數(shù)被調(diào)用的時候,才會去把這個函數(shù)在虛擬內(nèi)存空間的起始地址解析出來,再寫到調(diào)用模塊中特定的存儲地址內(nèi)。例如,一個模塊調(diào)用了另一個模塊中的函數(shù),那么該函數(shù)的地址直到被調(diào)用的時候才會被解析出來。
3. 動態(tài)加載的價值
動態(tài)加載技術(shù)對提高系統(tǒng)性能,提升可擴展性,保證系統(tǒng)的可靠性,延長系統(tǒng)生命周期,降低系統(tǒng)開發(fā)成本都具有十分重要的意義。
對于程序中不可忽視的錯誤,或者不能完全滿足用戶的需求,或者使用過程中需要不斷地修改和升級等等,其對應(yīng)的修改和升級都會往往導(dǎo)致停機維護。而停機對于如金融處理系統(tǒng)、電信交換機系統(tǒng)、交通控制系統(tǒng),以及一些關(guān)鍵的軍事應(yīng)用上的衛(wèi)星系統(tǒng)等等,用戶不能忍受系統(tǒng)中斷服務(wù)。因此,系統(tǒng)的在線擴展目前已經(jīng)成為系統(tǒng)軟件的一個基本需求,在系統(tǒng)運行狀態(tài)下可以通過動態(tài)的添加模塊來配置系統(tǒng),也就是動態(tài)加載機制。動態(tài)加載也使系統(tǒng)的升級變得更為方便。升級時開發(fā)人員不必重新寫整個系統(tǒng),即可將升級限制在系統(tǒng)的一個或更多部分,例如如某種算法或某個數(shù)據(jù)表格。
動態(tài)模塊升級還僅取決于基礎(chǔ)系統(tǒng)提供的功能API(應(yīng)用編程接口),而非取決于基礎(chǔ)系統(tǒng)的靜態(tài)地址。這意味著,一個動態(tài)模塊可支持多個產(chǎn)品版本,只要所有版本提供的API相同即可。
另外,動態(tài)加載也是系統(tǒng)調(diào)試和功能完善的重要手段,具有動態(tài)加載功能的系統(tǒng)可以隨時更新系統(tǒng)的程序,十分便于系統(tǒng)的調(diào)試、維護和功能的完善。
4. 操作系統(tǒng)內(nèi)核的動態(tài)加載
Linux 內(nèi)核模塊是Linux中內(nèi)存加載的一個特殊部分,它可以在不重啟整個系統(tǒng)的情況下動態(tài)加載和卸載,具有很高的靈活性。在內(nèi)核模塊動態(tài)加載時,可以節(jié)約相當(dāng)多的系統(tǒng)資源,而且還能避免不必要的內(nèi)核模塊以及它們的依賴組件被不必要地加載到內(nèi)存中,從而提高系統(tǒng)的性能。
為了實現(xiàn)動態(tài)加載內(nèi)核模塊,Linux提供了系統(tǒng)調(diào)用(System call)機制。它是一種專門用于在操作系統(tǒng)中執(zhí)行某一操作的特殊函數(shù)。當(dāng)用戶或應(yīng)用程序要求加載內(nèi)核模塊時,系統(tǒng)會調(diào)用合適的系統(tǒng)調(diào)用。例如,可以使用“insmod”系統(tǒng)調(diào)用動態(tài)加載內(nèi)核模塊,在不需要的時候用rmmod命令卸載模塊。
Linux內(nèi)核動態(tài)加載機制的一般工作流程如下:
- 編寫內(nèi)核模塊代碼:開發(fā)者編寫內(nèi)核模塊代碼,實現(xiàn)特定的功能。這些代碼通常以C語言編寫,并使用Linux內(nèi)核提供的API進行編程。
- 編譯內(nèi)核模塊:開發(fā)者使用特定的編譯工具鏈,將內(nèi)核模塊代碼編譯成可加載的模塊文件(通常是.ko文件)。
- 加載內(nèi)核模塊:在系統(tǒng)運行期間,用戶可以通過執(zhí)行insmod命令,將編譯好的內(nèi)核模塊加載到內(nèi)核中。加載過程包括將模塊代碼映射到內(nèi)核地址空間、初始化模塊等步驟。
- 使用內(nèi)核模塊:一旦內(nèi)核模塊被加載,它的功能就可以被內(nèi)核和其他系統(tǒng)組件使用。例如,如果加載的是一個驅(qū)動程序模塊,那么內(nèi)核就可以通過該模塊與相應(yīng)的硬件設(shè)備通信。
- 卸載內(nèi)核模塊:當(dāng)不再需要某個內(nèi)核模塊時,用戶可以執(zhí)行rmmod命令將其從內(nèi)核中卸載。卸載過程包括清理模塊資源、撤銷模塊映射等步驟。
盡管有一些約束,使用eBPF 也可以實現(xiàn)內(nèi)容模塊中的細粒度動態(tài)加載。
5. Android 中的動態(tài)加載框架
Java反射通常用于檢測和改變應(yīng)用程序運行在虛擬機中的表現(xiàn)。使用方法Class.forName獲得該類的Class文件,對得到的類用getField方法獲得類的成員變量,用getMethod方法獲得類的成員方法,也可以通過得到的成員方法的invoke方法執(zhí)行該成員方法。對于得到的Field類型的成員變量,可以通過它的set方法替換掉該變量。
為完成Android的動態(tài)加載,需要修改系統(tǒng)底層源碼,用到了大量的Java反射方法。讓系統(tǒng)能啟動插件組件,使用反射方法替換掉應(yīng)用層發(fā)送給系統(tǒng)的組件名稱,從而讓系統(tǒng)啟動插件組件。對于不同插件的資源加載問題,同樣是通過反射執(zhí)行系統(tǒng)資源管理等方法來解決的。
參考DruidPlugin 的實現(xiàn), 我們可以得到如下的Android 動態(tài)加載框架:
圖片
下列簡要介紹每個模塊:
- 解析模塊:解析插件安裝包文件,獲得插件的所有信息。
- Manifest管理模塊:解析結(jié)果,管理多個插件的manifest文件的替換和更新。
- 資源加載模塊:管理宿主與多個插件程序編譯后生成的資源id索引表,以便正常地通過資源id加載資源。
- 代理插件模塊:實現(xiàn)插件與宿主間的替換工作,讓系統(tǒng)以能夠啟動插件組件。
- 生命周期管理模塊:使插件activity組件在宿主程序中能與普通的activity一樣,擁有所有的生命周期方法,能夠正常地運行。
- 插件管理模塊:用于管理諸如插件的添加、刪除、更新等操作。
- 安全模塊:保證框架的安全性,確定通過網(wǎng)絡(luò)通信得到的插件文件沒有被修改。
- 啟動插件模塊:生成多種插件的啟動器,用于加載與啟動插件。
該框架位于Framework層與應(yīng)用層之間,作為應(yīng)用程序與Android系統(tǒng)的中間橋梁,通過修改系統(tǒng)的方法調(diào)用,從而實現(xiàn)插件的加載。
其中, 插件啟動器的onCreate方法示例偽代碼如下:
1:thread <-Reflector.getActivityThread(app) //獲取ActivityThread
2:base <- getInstrumentationByReflect(thread) //獲得反射Instrumentation屬性
3:wrapper <- InstrumentationReplace(base) //修改
4:setInstrumentationByReflect(wrapper) //重新注入
5:setMessageHandlerByReflect(callBack) //反射設(shè)置ActivityThread的m Callback
動態(tài)加載的過程如下所示:
圖片
其中, activity,bundle,uri 等插件中涉及的組件和參數(shù)同樣需要做相應(yīng)的調(diào)整。
6. 前端系統(tǒng)的動態(tài)加載
對PHP 開發(fā)環(huán)境下MVC 模式的網(wǎng)站代碼設(shè)計來說,分離的組織代碼路徑的獲取是令人頭疼,也是代碼運行中最容易產(chǎn)生錯誤的地方。為此,創(chuàng)建一個動態(tài)路徑的加載應(yīng)用會極大方便編碼,提升開發(fā)效率。
用戶首先訪問入口頁面視圖,視圖請求控制器,控制器響應(yīng)特定行為,獲取相應(yīng)模型數(shù)據(jù),而后將處理結(jié)果反饋到視圖中呈現(xiàn)給用戶。因而,在訪問請求中需明確控制器和控制器執(zhí)行的行為名稱。為實現(xiàn)控制器類中方法能調(diào)用不同視圖和模型,需要在實例化類對象之前,加載類的定義,即要完成對不同存儲位置下類的引用。為優(yōu)化代碼的性能,節(jié)省無謂的精力消耗,應(yīng)用類自動加載方案。將自動加載類__autoLoad()方法運用pl_auto?load_register()重新注冊改寫,當(dāng)代碼解析為新引用類時,自動調(diào)用改寫方法,計算路由路徑地址予以實例化加載,以實現(xiàn)不同文件目錄下的類的自動加載。示例代碼如下:
private static function autoLoad($class_name){
$class_map=array('MySqlDB' => CORE_PATH."MySqlDB.class.php",'Base' => CORE_PATH."Base.class.php" );
if(isset($class_map[$class_name])) require $class_map[$class_name];
elseif(substr($class_name, -5) == "Model") require MODEL_PATH.$class_name.".class.php";
elseif(substr($class_name, -10) == "Controller") require __URL__.$class_name.".class.php";
}
作為是一種網(wǎng)頁優(yōu)化技術(shù),動態(tài)加載可以在網(wǎng)頁加載時延遲加載不必要的資源,以提高頁面的加載速度和性能。例如,在VUE中引入百度開源的echart包用于數(shù)據(jù)統(tǒng)計的繪圖。
<template>
<div>using echart in vue project</div>
<div id="c1" style="width: 600px; height: 400px"></div>
</template>
<script setup lang="ts">
import * as echarts from "echarts";
import { onMounted } from "vue";
onMounted(() => {
const dom = document.getElementById("c1");
var myChart = echarts.init(dom as HTMLElement);
myChart.setOption({
title: {
text: "ECharts Bar usage",
},
tooltip: {},
xAxis: {
data: ["AA", "BB", "CC", "DD", "EE", "FF"],
},
yAxis: {},
series: [
{
name: "產(chǎn)量",
type: "bar",
data: [5, 10, 15, 12, 10, 20],
},
],
});
});
</script>
采用普通的加載方式,可以在路由配置中直接導(dǎo)入該頁面:
import VueComponent from "@/pages/vue.vue";
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: "/login", component: () => import("@/pages/login.vue") },
{
path: "/dashboard",
component: Dashboard,
children: [
{ path: "/", component: () => import("@/pages/dashboard/index.vue") },
{ path: "/vue", component: VueComponent },
{ path: "/react", component: () => import("@/pages/react.vue") },
],
},
],
});
會發(fā)現(xiàn)下載的包很大有1M多,在一般的網(wǎng)絡(luò)條件下,至少5秒以上,用戶體驗交差。如果采用動態(tài)加載,性能則會有較大的提升。
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: "/login", component: () => import("@/pages/login.vue") },
{
path: "/statistic",
component: Statistic,
children: [
{ path: "/", component: () => import("@/pages/statistic/index.vue") },
{ path: "/vue", component: () => import("@/pages/vue.vue") },
{ path: "/react", component: () => import("@/pages/react.vue") },
],
},
],
});
采用延遲加載之后,加載時間可以回歸到1秒以內(nèi),用戶體驗的提升是顯著的。
7. 小結(jié)
動態(tài)加載技術(shù)的核心思想是在程序運行時才加載所需的模塊或組件,而不是在編譯時靜態(tài)鏈接。這種技術(shù)帶來了許多優(yōu)勢,如代碼的模塊化、解耦、易于維護和擴展等。
動態(tài)加載使得代碼更加模塊化,降低了系統(tǒng)的復(fù)雜度,還有助于提高代碼的可重用性,因為相同的模塊可以在多個地方使用。動態(tài)加載能夠?qū)崿F(xiàn)代碼的解耦,有助于提高團隊協(xié)作效率,并提高系統(tǒng)的可維護性。掌握動態(tài)加載技術(shù)需要對編程語言、操作系統(tǒng)、網(wǎng)絡(luò)通信等方面有深入的了解。因此,學(xué)習(xí)和實踐動態(tài)加載技術(shù)有助于程序員提高自己的系統(tǒng)架構(gòu)能力和編程技能。
動態(tài)加載技術(shù)在軟件開發(fā)領(lǐng)域具有廣泛的應(yīng)用場景。合理使用動態(tài)加載技術(shù)不僅可以提高系統(tǒng)的可維護性和可擴展性,還可以提升程序員的系統(tǒng)架構(gòu)能力和編程技能。因此,對于現(xiàn)代軟件開發(fā)者來說,掌握動態(tài)加載技術(shù)是非常必要的。