日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > Android >内容正文

Android

Android动态加载插件APK

發(fā)布時間:2025/1/21 Android 112 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Android动态加载插件APK 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

問題起因

我曾經(jīng)在開發(fā)Android Application的過程中遇到過那個有名的65k方法數(shù)的問題。如果你開發(fā)的應(yīng)用程序變得非常龐大,你八成會遇到這個問題。

這個問題實際上體現(xiàn)為兩個方面:
一、65k方法數(shù)
Android的APK安裝包將編譯后的字節(jié)碼放在dex格式的文件中,供Android的JVM加載執(zhí)行。不幸的是,單個dex文件的方法數(shù)被限制在了65536之內(nèi),這其中除了我們自己實現(xiàn)的方法之外,還包括了我們用到的Android Framework方法、其他library包含的方法。如果我們的方法總數(shù)超過了這個限制,那么我們在嘗試打包時,會拋出如下異常:

Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536

在比較新的Android構(gòu)建工具下可能是如下異常:

trouble writing output: Too many field references: 131000; max is 65536. You may try using --multi-dex option.

二、APK安裝失敗
Android官方推薦了一個叫做MultiDex的工具,用來在打包時將方法分散放到多個dex內(nèi),以此來解決65K方法數(shù)的問題。但是,除此之外,方法數(shù)過多還會帶來dex文件過大的問題。

在安裝APK時,系統(tǒng)會運行一個叫做dexopt的程序,dexopt會使用Dalvik LinearAlloc緩沖區(qū)來存儲應(yīng)用的方法信息。在Android 2.x的系統(tǒng)中,該緩沖區(qū)大小僅為5M,當(dāng)我們的dex文件過大超過該緩沖區(qū)大小時,就會遇到APK安裝失敗的問題。

思路

對于如上的兩個問題,有個非常有名的方案,就是采用動態(tài)加載插件化APK的方法。

插件化APK的思路為:將部分代碼分離出來放在另外的APK中,做成插件APK的形式,在我們的應(yīng)用程序啟動后,在使用時動態(tài)加載該插件APK中的內(nèi)容。

該思路簡單來說便是將部分代碼放在了另外一個獨立的APK中,而不是放在我們自己的dex中。這樣一方面減少了我們自己dex中方法總數(shù),另一方面也減小了dex文件的大小,因此可以解決如上兩個方面的問題。對于這個插件APK包含的類,我們可以在使用到的時候再加載進來,這便是動態(tài)加載的思路。

要實現(xiàn)插件化APK,我們只需要解決如下3個問題:

  • 如何生成插件APK

  • 如何加載插件APK

  • 如何使用插件APK中的內(nèi)容

類加載器

在實現(xiàn)插件化APK之前,我們需要先了解一下Android中的類加載機制,作為實現(xiàn)動態(tài)加載的基礎(chǔ)。

在Android中,我們通過ClassLoader來加載應(yīng)用程序運行需要的類。ClassLoader是一個抽象類,我們需要繼承該類來實現(xiàn)具體的類加載器的行為。在Android中,ClassLoader的實現(xiàn)類采用了代理模型(Delegation Model)來執(zhí)行類的加載。每一個ClassLoader類都有一個與之相關(guān)聯(lián)的父加載器,當(dāng)一個ClassLoader類嘗試加載某個類時,首先會委托其父加載器加載該類。如果父加載器成功加載了該類,則不會再由該子加載器進行加載;如果父加載器未能加載成功,則再由子加載器進行類加載的動作。

在Android中,我們一般使用DexClassLoader和PathClassLoader進行類的加載。

  • DexClassLoader: 可以從.jar或者.apk文件中加載類;

  • PathClassLoader: 只能從系統(tǒng)內(nèi)存中已安裝的內(nèi)容中加載類。

對于我們的插件化APK,顯然需要使用DexClassLoader進行自定義類加載。我們看一下DexClassLoader的構(gòu)造方法:

/*** Create DexClassLoader* @param dexPath String: the list of jar/apk files containing classes and resources, delimited by File.pathSeparator, which defaults to ":" on Android* @param optimizedDirectory String: directory where optimized dex files should be written; must not be null* @param librarySearchPath String: the list of directories containing native libraries, delimited by File.pathSeparator; may be null* @param parent ClassLoader: the parent class loader*/ DexClassLoader (String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)

從以上可以看到,該構(gòu)造方法的入?yún)⒅谐酥付ǜ鞣N加載路徑外,還需要指定一個父加載器,以此實現(xiàn)我們以上提到的類加載代理模型。

步驟規(guī)劃

為了讓整個coding過程變得簡單,我們來實現(xiàn)一個簡單得不能再簡單的功能:在主Activity上以"年-月-日"的格式顯示當(dāng)前的日期。為了讓插件APK的整個思路清晰一點,我們想要實現(xiàn)如下設(shè)定:

  • 提供一個插件化APK,提供一個生成日期的方法;

  • 應(yīng)用程序主Activity中通過插件APK中的方法獲取到該日期,顯示在TextView中。

有了如上的鋪墊,我們現(xiàn)在可以明確我們的實現(xiàn)步驟:

  • 創(chuàng)建我們的Application;

  • 創(chuàng)建一個共享接口的library module;

  • 生成插件APK;

  • 實現(xiàn)自定義類加載器;

  • 實現(xiàn)動態(tài)加載。

好了,讓我們開始coding吧!

1. 創(chuàng)建Application

在Android Studio中創(chuàng)建一個Application,作為我們最終需要發(fā)布的應(yīng)用程序。
該Application暫時不需要做特別的配置,你只要實現(xiàn)一個MainActivity,然后顯示一個TextView就可以了!

這時,你的工程可能長這個樣子:

2. 創(chuàng)建共享接口

在創(chuàng)建插件APK之前,我們還需要再做一些準(zhǔn)備。
由于我們將一部分方法放到了插件APK里,這也就意味著,我們在自己的app module中對這些方法是不可見的,這就需要有一個機制讓app module中使用這些方法變成可能。

在這里,我們采用一個公共的接口來進行方法的定義。你可以理解為我們在app和插件APK之間搭了一座橋,我們在app module中使用接口定義的這些方法,而方法的具體實現(xiàn)放在了插件APK中。

我們創(chuàng)建一個library module,命名為library。在該library module中,我們創(chuàng)建一個TestInterface接口,在該接口中定義如下方法:

/*** 定義方法: 將時間戳轉(zhuǎn)換成日期* @param dateFormat 日期格式* @param timeStamp 時間戳,單位為ms*/ String getDateFromTimeStamp(String dateFormat, long timeStamp);

如上注釋所示,該方法將給定的時間戳按照指定的格式轉(zhuǎn)換成一個日期字符串。我們期待在插件APK中實現(xiàn)該方法,并且在app中通過該方法獲取到我們需要的日期。

為了讓插件APK引用該library定義的接口,我們需要生成一個jar包,首先,在library module的gradle腳本中增加如下配置:

android.libraryVariants.all { variant ->def name = variant.buildType.nameif (name.equals(com.android.builder.core.BuilderConstants.DEBUG)) {return; // Skip debug builds.}def task = project.tasks.create "jar${name.capitalize()}", Jartask.dependsOn variant.javaCompiletask.from variant.javaCompile.destinationDirartifacts.add('archives', task); }

然后在工程根目錄執(zhí)行如下命令:

./gradlew :library:jarRelease

然后就可以在該library module的/build/libs目錄下看到一個library.jar包。

此時,你的工程是這樣的:

3. 生成插件APK

我們終于要實現(xiàn)我們的插件APK了!
在工程中創(chuàng)建一個module,類型選擇為application(而不是library),取名為plugin。

將上一步中生成的library.jar放到該plugin module的libs目錄下,在gradle腳本中添加

provided files('libs/library.jar')

便可以引用library中定義的共享接口了。

正如如上所說,我們在該plugin module中做方法的具體實現(xiàn),因此,我們創(chuàng)建一個TestUtil類,實現(xiàn)如上定義的TestInterface接口定義的方法:

/*** 測試插件包含的工具類* Created by Anchorer on 16/7/31.*/ public class TestUtil implements TestInterface {/*** 將時間戳轉(zhuǎn)換成日期* @param dateFormat 日期格式* @param timeStamp 時間戳,單位為ms*/public String getDateFromTimeStamp(String dateFormat, long timeStamp) {DateFormat format = new SimpleDateFormat(dateFormat);Date date = new Date(timeStamp);return format.format(date);}}

這樣一來,插件部分的代碼就寫完了!接下來,我們需要生成一個插件APK,將該APK放在應(yīng)用程序app module的SourceSet下,供app module的類加載器進行加載。為此,我們在plugin的gradle腳本中添加如下配置:

buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'applicationVariants.all { variant ->variant.outputs.each { output ->def apkName = "plugin.apk"output.outputFile = file("$rootProject.projectDir/app/src/main/assets/plugin/" + apkName)}}}}

該腳本將生成的apk放在app的assets目錄下。

最后,在工程根目錄執(zhí)行:

./gradlew :plugin:assembleRelease

便可以在/app/src/main/assets/plugin目錄下生成了一個plugin.apk文件。到此為止,我們便生成了我們的插件APK。

此時,我們的工程長這個樣子,這已經(jīng)是我們工程的最終樣子了:

4. 實現(xiàn)自定義類加載器

有了插件APK,接下來我們需要在應(yīng)用程序運行時,在需要的時候加載這個APK中的內(nèi)容。實現(xiàn)我們自己的類加載器,我們分為如下兩個步驟:

  • 將該APK復(fù)制到SD卡中;

  • 從SD卡中加載該APK。

我們實現(xiàn)一個PluginLoader類,來執(zhí)行插件的加載。在這個類中,實現(xiàn)如上提供的兩個關(guān)鍵方法。

首先,將APK復(fù)制到SD卡的代碼比較簡單:

/*** 將插件APK保存至SD卡* @param pluginName 插件APK的名稱*/ private boolean savePluginApkToStorage(String pluginName) {String pluginApkPath = this.getPlguinApkDirectory() + pluginName;File plugApkFile = new File(pluginApkPath);if (plugApkFile.exists()) {try {plugApkFile.delete();} catch (Throwable e) {}}BufferedInputStream inStream = null;BufferedOutputStream outStream = null;try {InputStream stream = TestApplication.getInstance().getAssets().open("plugin/" + pluginName);inStream = new BufferedInputStream(stream);outStream = new BufferedOutputStream(new FileOutputStream(pluginApkPath));final int BUF_SIZE = 4096;byte[] buf = new byte[BUF_SIZE];while(true) {int readCount = inStream.read(buf, 0, BUF_SIZE);if (readCount == -1) {break;}outStream.write(buf,0, readCount);}} catch(Exception e) {return false;} finally {if (inStream != null) {try {inStream.close();} catch (IOException e) {}inStream = null;}if (outStream != null) {try {outStream.close();} catch (IOException e) {}outStream = null;}}return true; }

其次,我們要創(chuàng)建自己的DexClassLoader:

DexClassLoader classLoader = null; try {String apkPath = getPlguinApkDirectory() + pluginName;File dexOutputDir = TestApplication.getInstance().getDir("dex", 0);String dexOutputDirPath = dexOutputDir.getAbsolutePath();ClassLoader cl = TestApplication.getInstance().getClassLoader();classLoader = new DexClassLoader(apkPath, dexOutputDirPath, null, cl); } catch(Throwable e) {}

這里我們使用如上提到的DexClassLoader的構(gòu)造方法,其中第一個參數(shù)是我們插件APK的路徑,最后一個參數(shù)是Application生成的父ClassLoader。

5. 實現(xiàn)動態(tài)加載

實現(xiàn)了自己的類加載器之后,我們使用該ClassLoader進行類的加載就可以了!

使用ClassLoader加載類,我們調(diào)用loadClass(String className)就可以了。這一步比較簡單:

/*** 加載指定名稱的類* @param className 類名(包含包名)*/ public Object newInstance(String className) {if (mDexClassLoader == null) {return null;}try {Class<?> clazz = mDexClassLoader.loadClass(className);Object instance = clazz.newInstance();return instance;} catch (Exception e) {Log.e(Const.LOG, "newInstance className = " + className + " failed" + " exception = " + e.getMessage());}return null; }

有了這個加載方法之后,我們就可以加載以上實現(xiàn)的TestUtil類了:

TestInterface testManager = (TestInterface) mPluginLoader.newInstance("org.anchorer.pluginapk.plugin.TestUtil"); mMainTextView.setText(testPlugin.getDateFromTimeStamp("yyyy-MM-dd", System.currentTimeMillis()));

至此為止,代碼全部完成。啟動應(yīng)用程序,我們可以看到主界面成功顯示了當(dāng)前的日期。

源碼

該示例工程的源代碼我放到了自己的GitHub上:
Github/Anchorer/PluginApk

這個工程對代碼進行了一定程度的封裝:

  • PluginManager: 該類統(tǒng)一提供了創(chuàng)建類加載器和加載具體類的所有入口;

  • PluginLoader: 該類具體創(chuàng)建了類加載器,執(zhí)行具體的加載類的行為;

  • MainActivity: 主Activity,展示了如何調(diào)用插件內(nèi)的方法。

參考

提供一些我自己在探索過程中參考的文章:
1. ClassLoader
2. DexClassLoader
3. multidex
4. 動態(tài)加載基礎(chǔ)

總結(jié)

以上是生活随笔為你收集整理的Android动态加载插件APK的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。