編輯:關於Android編程
我曾經在開發Android Application的過程中遇到過那個有名的65k方法數的問題。如果你開發的應用程序變得非常龐大,你八成會遇到這個問題。
這個問題實際上體現為兩個方面:
一、65k方法數
Android的APK安裝包將編譯後的字節碼放在dex格式的文件中,供Android的JVM加載執行。不幸的是,單個dex文件的方法數被限制在了65536之內,這其中除了我們自己實現的方法之外,還包括了我們用到的Android Framework方法、其他library包含的方法。如果我們的方法總數超過了這個限制,那麼我們在嘗試打包時,會拋出如下異常:
Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536
在比較新的Android構建工具下可能是如下異常:
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
二、APK安裝失敗
Android官方推薦了一個叫做MultiDex的工具,用來在打包時將方法分散放到多個dex內,以此來解決65K方法數的問題。但是,除此之外,方法數過多還會帶來dex文件過大的問題。
在安裝APK時,系統會運行一個叫做
dexopt的程序,dexopt會使用
Dalvik LinearAlloc緩沖區來存儲應用的方法信息。在Android 2.x的系統中,該緩沖區大小僅為5M,當我們的dex文件過大超過該緩沖區大小時,就會遇到APK安裝失敗的問題。
思路
對於如上的兩個問題,有個非常有名的方案,就是采用動態加載插件化APK的方法。
插件化APK的思路為:將部分代碼分離出來放在另外的APK中,做成插件APK的形式,在我們的應用程序啟動後,在使用時動態加載該插件APK中的內容。
該思路簡單來說便是將部分代碼放在了另外一個獨立的APK中,而不是放在我們自己的dex中。這樣一方面減少了我們自己dex中方法總數,另一方面也減小了dex文件的大小,因此可以解決如上兩個方面的問題。對於這個插件APK包含的類,我們可以在使用到的時候再加載進來,這便是動態加載的思路。
要實現插件化APK,我們只需要解決如下3個問題:
如何生成插件APK
如何加載插件APK
如何使用插件APK中的內容
類加載器
在實現插件化APK之前,我們需要先了解一下Android中的類加載機制,作為實現動態加載的基礎。
在Android中,我們通過
ClassLoader來加載應用程序運行需要的類。
ClassLoader是一個抽象類,我們需要繼承該類來實現具體的類加載器的行為。在Android中,
ClassLoader的實現類采用了
代理模型(Delegation Model)來執行類的加載。每一個
ClassLoader類都有一個與之相關聯的父加載器,當一個
ClassLoader類嘗試加載某個類時,首先會委托其父加載器加載該類。如果父加載器成功加載了該類,則不會再由該子加載器進行加載;如果父加載器未能加載成功,則再由子加載器進行類加載的動作。
在Android中,我們一般使用
DexClassLoader和
PathClassLoader進行類的加載。
DexClassLoader: 可以從.jar或者.apk文件中加載類;
PathClassLoader: 只能從系統內存中已安裝的內容中加載類。
對於我們的插件化APK,顯然需要使用
DexClassLoader進行自定義類加載。我們看一下
DexClassLoader的構造方法:
/**
* 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)
從以上可以看到,該構造方法的入參中除了指定各種加載路徑外,還需要指定一個父加載器,以此實現我們以上提到的類加載代理模型。
步驟規劃
為了讓整個coding過程變得簡單,我們來實現一個簡單得不能再簡單的功能:在主Activity上以"年-月-日"的格式顯示當前的日期。為了讓插件APK的整個思路清晰一點,我們想要實現如下設定:
提供一個插件化APK,提供一個生成日期的方法;
應用程序主Activity中通過插件APK中的方法獲取到該日期,顯示在TextView中。
有了如上的鋪墊,我們現在可以明確我們的實現步驟:
創建我們的Application;
創建一個共享接口的library module;
生成插件APK;
實現自定義類加載器;
實現動態加載。
好了,讓我們開始coding吧!
1. 創建Application
在
Android Studio中創建一個
Application,作為我們最終需要發布的應用程序。
該Application暫時不需要做特別的配置,你只要實現一個
MainActivity,然後顯示一個
TextView就可以了!
這時,你的工程可能長這個樣子:
2. 創建共享接口
在創建插件APK之前,我們還需要再做一些准備。
由於我們將一部分方法放到了插件APK裡,這也就意味著,我們在自己的app module中對這些方法是不可見的,這就需要有一個機制讓
app module中使用這些方法變成可能。
在這裡,我們采用一個公共的接口來進行方法的定義。你可以理解為我們在
app和
插件APK之間搭了一座橋,我們在
app module中使用接口定義的這些方法,而方法的具體實現放在了
插件APK中。
我們創建一個
library module,命名為
library。在該
library module中,我們創建一個
TestInterface接口,在該接口中定義如下方法:
/**
* 定義方法: 將時間戳轉換成日期
* @param dateFormat 日期格式
* @param timeStamp 時間戳,單位為ms
*/
String getDateFromTimeStamp(String dateFormat, long timeStamp);
如上注釋所示,該方法將給定的時間戳按照指定的格式轉換成一個日期字符串。我們期待在
插件APK中實現該方法,並且在
app中通過該方法獲取到我們需要的日期。
為了讓
插件APK引用該library定義的接口,我們需要生成一個jar包,首先,在
library module的
gradle腳本中增加如下配置:
android.libraryVariants.all { variant ->
def name = variant.buildType.name
if (name.equals(com.android.builder.core.BuilderConstants.DEBUG)) {
return; // Skip debug builds.
}
def task = project.tasks.create "jar${name.capitalize()}", Jar
task.dependsOn variant.javaCompile
task.from variant.javaCompile.destinationDir
artifacts.add('archives', task);
}
然後在工程根目錄執行如下命令:
./gradlew :library:jarRelease
然後就可以在該library module的
/build/libs目錄下看到一個
library.jar包。
此時,你的工程是這樣的:
3. 生成插件APK
我們終於要實現我們的插件APK了!
在工程中創建一個module,類型選擇為application(而不是library),取名為plugin。
將上一步中生成的
library.jar放到該plugin module的
libs目錄下,在gradle腳本中添加
provided files('libs/library.jar')
便可以引用library中定義的共享接口了。
正如如上所說,我們在該plugin module中做方法的具體實現,因此,我們創建一個
TestUtil類,實現如上定義的
TestInterface接口定義的方法:
/**
* 測試插件包含的工具類
* Created by Anchorer on 16/7/31.
*/
public class TestUtil implements TestInterface {
/**
* 將時間戳轉換成日期
* @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放在應用程序app module的SourceSet下,供app module的類加載器進行加載。為此,我們在plugin的gradle腳本中添加如下配置:
buildTypes {
release {
minifyEnabled false
proguardFiles 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目錄下。
最後,在工程根目錄執行:
./gradlew :plugin:assembleRelease
便可以在
/app/src/main/assets/plugin目錄下生成了一個plugin.apk文件。到此為止,我們便生成了我們的
插件APK。
此時,我們的工程長這個樣子,這已經是我們工程的最終樣子了:
4. 實現自定義類加載器
有了插件APK,接下來我們需要在應用程序運行時,在需要的時候加載這個APK中的內容。實現我們自己的類加載器,我們分為如下兩個步驟:
將該APK復制到SD卡中;
從SD卡中加載該APK。
我們實現一個
PluginLoader類,來執行插件的加載。在這個類中,實現如上提供的兩個關鍵方法。
首先,將APK復制到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;
}
其次,我們要創建自己的
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的構造方法,其中第一個參數是我們
插件APK的路徑,最後一個參數是
Application生成的父ClassLoader。
5. 實現動態加載
實現了自己的類加載器之後,我們使用該
ClassLoader進行類的加載就可以了!
使用
ClassLoader加載類,我們調用
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;
}
有了這個加載方法之後,我們就可以加載以上實現的
TestUtil類了:
TestInterface testManager = (TestInterface) mPluginLoader.newInstance("org.anchorer.pluginapk.plugin.TestUtil");
mMainTextView.setText(testPlugin.getDateFromTimeStamp("yyyy-MM-dd", System.currentTimeMillis()));
至此為止,代碼全部完成。啟動應用程序,我們可以看到主界面成功顯示了當前的日期。
源碼地址:https://github.com/JoeySheng/Plugin.git
Android手勢密碼LockPatternView、LockPasswordUtils、LockPatternUtils在使用別人寫的這個手勢密碼的時候,我們通常是有自
在前面的博文中,小編簡單的介紹了如何制作圓角的按鈕以及圓角的圖片,伴著鍵盤和手指之間的舞步,迎來新的問題,不知道小伙伴有沒有這樣的經歷,以App為例,點擊頭像的時候,會從
寫了一個月應用層代碼,感覺寫嘔了,最近在研究插件化動態加載方面的東西。本文需要解決的作業:在Activity自身的跳轉中進行Hook。先簡要說下遇到的幾個坑以及後面的學習
一、能做什麼你只需要傳url,JavaBean就可以在回調方法裡面得到想要的結果,你會發現你的代碼裡面沒有了子線程、沒有了handle,鏈式的變成使得代碼更加清晰。1.1