編輯:關於Android編程
今年真是熱補丁框架的洪荒之力爆發的一年,短短幾個月內,已經出現了好幾個熱修復的框架了,基本上都是大同小異,這裡我就不過多的去評論這些框架。只有自己真正的去經歷過,你才會發現其中的大寫的坑
事實上,現在出現的大多數熱修復的框架,穩定性和兼容性都還達不到要求,包括阿裡的Andfix,據同事說,我們自己的app原本沒有多少crash,接入了andfix倒引起了一部分的crash,這不是一個熱修復框架所應該具有的“變態功能”。雖然阿裡百川現在在大力推廣這套框架,我依舊不看好,只是其思路還是有學習價值的。
Dex的熱修復目前來看基本上有四種方案:
阿裡系的從native層入手,見AndFix QQ空間的方案,插樁,見安卓App熱補丁動態修復技術介紹 微信的方案,見微信Android熱補丁實踐演進之路,dexDiff和dexPatch,方法很牛逼,需要全量插入,但是這個全量插入的dex中需要刪除一些過早加載的類,不然同樣會報class is pre verified異常,還有一個缺點就是合成占內存和內置存儲空間。微信讀書的方式和微信類似,見Android Patch 方案與持續交付,不過微信讀書是miniloader方式,啟動時容易ANR,在我錘子手機上變現出來特別明顯,長時間的卡圖標現象。 美團的方案,也就是instant run的方案,見Android熱更新方案Robust此外,微信的方案是多classloader,這種方式可以解決用multidex方式在部分機型上不生效patch的問題,同時還帶來一個好處,這種多classloader的方式使用的是instant run的代碼,如果存在native library的修復,也會帶來極大的方便。
而native libraray的修復,目前來說,基本上有兩種方案。。
類似multidex的dex方式,插入目錄到數組最前面,具體文章見Android熱更新之so庫的熱更新,需要處理系統的兼容性問題,系統分隔線是Android 6.0 第二種方式需要依賴多classloader,在構造BaseDexClassLoader的時候,獲取原classloader的native library,通過環境變量分隔符(冒號),將patch的native library與原目錄進行連接,patch目錄在前,這樣同樣可以達到修復的目的,缺點是需要依賴dex的熱修復,優點是應用native library時不需要處理兼容性問題,當然從patch中釋放出來的時候也需要處理兼容性問題。第二種方式的實現可以看看BaseDexClassLoader的構造函數
BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent)
只需要在修復dex的同時,如果有native library,則獲取原來的路徑與patch的路徑進行連接,偽代碼如下:
nativeLibraryPath = 獲取與原始路徑;
nativeLibraryPath = patchNativeLibraryPath + File.pathSeparator + nativeLibraryPath;
IncrementalClassLoader inject = IncrementalClassLoader.inject(
classLoader,
nativeLibraryPath,
optDir.getAbsolutePath(),
dexList);
而這種方式需要強依賴dex的修復,如果沒有dex,就無能為例了,實際情況基本上是兩種方式交叉使用,在沒有dex的情況下,使用另外一種方式。
而native library還有一個坑,就是從patch中釋放so的過程,這個過程需要處理兼容性,在android 21以下,通過下面這個函數去釋放
com.android.internal.content.NativeLibraryHelper.copyNativeBinariesIfNeededLI
而在andrdod 21及以上,則通過下面的這幾個函數去釋放
com.android.internal.content.NativeLibraryHelper$Handle.create() com.android.internal.content.NativeLibraryHelper.findSupportedAbi() com.android.internal.content.NativeLibraryHelper.copyNativeBinaries()
而對於資源的熱修復,其實主要還是和插件化的思路是一樣的,具體實現可以參考兩個
Atlas或者攜程的插件化框架 Instant run的資源處理方式,甚至可以做到運行期立即生效。本篇文章就來說說資源的熱修復的實現思路,在這之前,需要貼兩個鏈接,以下文章的內容基於這兩個鏈接去實現,所以務必先看看,不然會一臉懵逼。一個是instant run的源碼,自備梯子,另一個是馮老師寫的一個類,這個類在Atlas中出現過,後來被馮老師重寫了,同樣自備梯子。
instant-run源碼 Hack.java實現重要的事情說三遍
自備梯子
自備梯子
自備梯子
資源的熱修復實現,主要由一下幾個步驟組成:
提前感知系統兼容性,不兼容則不進行後續操作 服務器端生成patch的資源,客戶端應用patch的資源 替換系統AssetManger,加入patch的資源對於第一步,我們需要先看看instant run對於資源部分的實現,其偽代碼如下
AssetManager newAssetManager = new AssetManager();
newAssetManager.addAssetPath(externalResourceFile)
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
newAssetManager.ensureStringBlocks();
// Find the singleton instance of ResourcesManager
ResourcesManager resourcesManager = ResourcesManager.getInstance();
// Iterate over all known Resources objects
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
for (WeakReference wr : resourcesManager.mActiveResources.values()) {
Resources resources = wr.get();
// Set the AssetManager of the Resources instance to our brand new one
resources.mAssets = newAssetManager;
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
}
代碼很簡單,通過調用addAssetPath將patch的資源加到新建的AssetManager對象中,然後將內存中所有Resources對象中的AssetManager對象替換為新建的AssetManager對象。當然還需要處理兼容性問題,對於兼容性問題,則需要用到馮老師的Hack類(這裡我為了與原來馮老師沒有重寫前的Hack類做區分,將其重命名了HackPlus,意思你懂的),具體Hack過程請參考Atlas或者攜程的插件化框架的實現,然後基於instant run進行實現,當然這種方式有一部分資源是修復不了了,比如notification。
主要的分界線是Android 19 和 Android N坑麼,你沒遇到,總是說沒有,遇到了,坑無數。
首先需要拿到App運行後內存中的Resources對象
Android N,通過ResourcesManager中的mResourceReferences去獲取Resources對象,是個ArrayList對象 Android 19到Android N(不含N),通過ResourcesManager中的mActiveResources去獲取Resources對象,是個ArrayMap對象 Android 19以下,通過ActivityThread的mActiveResources去獲取Resources對象,是個HashMap對象。 接著就是替換Resources中的AssetManager對象這裡我已經基本實現了反射檢測系統支持性相關的代碼,主要就是對以上分析的內容做反射檢測,一旦發生異常,則不再進行資源的修復,代碼如下(HackPlus的源碼見上面的Hack.java的源碼):
mAssertionErr;
public AssertionArrayException(String str) {
super(str);
this.mAssertionErr = new ArrayList();
}
public void addException(AssertionException hackAssertionException) {
this.mAssertionErr.add(hackAssertionException);
}
public void addException(List list) {
this.mAssertionErr.addAll(list);
}
public List getExceptions() {
return this.mAssertionErr;
}
public static AssertionArrayException mergeException(AssertionArrayException assertionArrayException, AssertionArrayException assertionArrayException2) {
if (assertionArrayException == null) {
return assertionArrayException2;
}
if (assertionArrayException2 == null) {
return assertionArrayException;
}
AssertionArrayException assertionArrayException3 = new AssertionArrayException(assertionArrayException.getMessage() + ";" + assertionArrayException2.getMessage());
assertionArrayException3.addException(assertionArrayException.getExceptions());
assertionArrayException3.addException(assertionArrayException2.getExceptions());
return assertionArrayException3;
}
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
for (AssertionException hackAssertionException : this.mAssertionErr) {
stringBuilder.append(hackAssertionException.toString()).append(";");
try {
if (hackAssertionException.getCause() instanceof NoSuchFieldException) {
Field[] declaredFields = hackAssertionException.getHackedClass().getDeclaredFields();
stringBuilder.append(hackAssertionException.getHackedClass().getName()).append(".").append(hackAssertionException.getHackedFieldName()).append(";");
for (Field field : declaredFields) {
stringBuilder.append(field.getName()).append(File.separator);
}
} else if (hackAssertionException.getCause() instanceof NoSuchMethodException) {
Method[] declaredMethods = hackAssertionException.getHackedClass().getDeclaredMethods();
stringBuilder.append(hackAssertionException.getHackedClass().getName()).append("->").append(hackAssertionException.getHackedMethodName()).append(";");
for (int i = 0; i < declaredMethods.length; i++) {
if (hackAssertionException.getHackedMethodName().equals(declaredMethods[i].getName())) {
stringBuilder.append(declaredMethods[i].toGenericString()).append(File.separator);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
stringBuilder.append("@@@@");
}
return stringBuilder.toString();
}
}
" data-snippet-id="ext.27e0015bab58b1c74cae9139390abd6d" data-snippet-saved="false" data-codota-status="done">//這個類用於保存hack過程中發生的異常,一旦mAssertionErr不為空,則表示當前系統不支持資源的熱修復,直接return,不進行修復
public class AssertionArrayException extends Exception {
private static final long serialVersionUID = 1;
private List mAssertionErr;
public AssertionArrayException(String str) {
super(str);
this.mAssertionErr = new ArrayList();
}
public void addException(AssertionException hackAssertionException) {
this.mAssertionErr.add(hackAssertionException);
}
public void addException(List list) {
this.mAssertionErr.addAll(list);
}
public List getExceptions() {
return this.mAssertionErr;
}
public static AssertionArrayException mergeException(AssertionArrayException assertionArrayException, AssertionArrayException assertionArrayException2) {
if (assertionArrayException == null) {
return assertionArrayException2;
}
if (assertionArrayException2 == null) {
return assertionArrayException;
}
AssertionArrayException assertionArrayException3 = new AssertionArrayException(assertionArrayException.getMessage() + ";" + assertionArrayException2.getMessage());
assertionArrayException3.addException(assertionArrayException.getExceptions());
assertionArrayException3.addException(assertionArrayException2.getExceptions());
return assertionArrayException3;
}
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
for (AssertionException hackAssertionException : this.mAssertionErr) {
stringBuilder.append(hackAssertionException.toString()).append(";");
try {
if (hackAssertionException.getCause() instanceof NoSuchFieldException) {
Field[] declaredFields = hackAssertionException.getHackedClass().getDeclaredFields();
stringBuilder.append(hackAssertionException.getHackedClass().getName()).append(".").append(hackAssertionException.getHackedFieldName()).append(";");
for (Field field : declaredFields) {
stringBuilder.append(field.getName()).append(File.separator);
}
} else if (hackAssertionException.getCause() instanceof NoSuchMethodException) {
Method[] declaredMethods = hackAssertionException.getHackedClass().getDeclaredMethods();
stringBuilder.append(hackAssertionException.getHackedClass().getName()).append("->").append(hackAssertionException.getHackedMethodName()).append(";");
for (int i = 0; i < declaredMethods.length; i++) {
if (hackAssertionException.getHackedMethodName().equals(declaredMethods[i].getName())) {
stringBuilder.append(declaredMethods[i].toGenericString()).append(File.separator);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
stringBuilder.append("@@@@");
}
return stringBuilder.toString();
}
}
//具體Hack類,主要Hack AssetManager相關類,
public class AndroidHack {
private static final String TAG = "AndroidHack";
//exception
public static AssertionArrayException exceptionArray;
//resources
public static HackPlus.HackedClass AssetManager;
public static HackedMethod0 AssetManager_construct;
public static HackPlus.HackedMethod1 AssetManager_addAssetPath;
public static HackedMethod0 AssetManager_ensureStringBlocks;
//>=19
public static HackedClass
使用的時候,只要在加載patch資源前,調用如下方法進行檢測
if(!AndroidHack.defineAndVerify()){
//不加載patch資源
return;
}
//加載patch資源邏輯
patch資源的生成比較麻煩,我們放在最後面說明,現在假設我們有一個包含整個apk的資源的文件,需要運行時替換,現在來實現上面的加載patch資源的邏輯,具體邏輯上面反射的時候已經說明了,這時候只需要調用上面反射獲取的包裝類,進行替換即可,直接看代碼中的注釋:
public class ResourceLoader {
private static final String TAG = "ResourceLoader";
public static boolean patchResources(Context context, File patchResource) {
try {
if (context == null || patchResource == null){
return false;
}
if (!patchResource.exists()) {
return false;
}
//通過構造函數new一個AssetManager對象
AssetManager newAssetManager = AndroidHack.AssetManager_construct.invoke().statically();
//調用AssetManager對象的addAssetPath方法添加patch資源
int cookie = AndroidHack.AssetManager_addAssetPath.invokeWithParam(patchResource.getAbsolutePath()).on(newAssetManager);
//添加成功時cookie必然大於0
if (cookie == 0) {
Logger.e(TAG, "Could not create new AssetManager");
return false;
}
// 在Android 19以前需要調用這個方法,但是Android L後不需要,實際情況Andorid L上調用也不會有問題,因此這裡不區分版本
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
AndroidHack.AssetManager_ensureStringBlocks.invoke().on(newAssetManager);
//獲取內存中的Resource對象的弱引用
Collection> references;
if (Build.VERSION.SDK_INT >= 24) {
// Android N,獲取的是一個ArrayList,直接賦值給references對象
// Find the singleton instance of ResourcesManager
Object resourcesManager = AndroidHack.ResourcesManager_getInstance.invoke().statically();
//noinspection unchecked
references = (Collection>) AndroidHack.ResourcesManager_mResourceReferences.on(resourcesManager).get();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
//Android 19以上 獲得的是一個ArrayMap,調用其values方法後賦值給references
// Find the singleton instance of ResourcesManager
Object resourcesManager = AndroidHack.ResourcesManager_getInstance.invoke().statically();
@SuppressWarnings("unchecked")
ArrayMap> arrayMap = AndroidHack.ResourcesManager_mActiveResources.on(resourcesManager).get();
references = arrayMap.values();
} else {
//Android 19以下,通過ActivityThread獲取得到的是一個HashMap對象,通過其values方法獲得對象賦值給references
Object activityThread = AndroidHack.getActivityThread();
@SuppressWarnings("unchecked")
HashMap> map = (HashMap>) AndroidHack.ActivityThread_mActiveResources.on(activityThread).get();
references = map.values();
}
//遍歷獲取到的Ressources對象的弱引用,將其AssetManager對象替換為我們的patch的AssetManager
for (WeakReference wr : references) {
Resources resources = wr.get();
// Set the AssetManager of the Resources instance to our brand new one
if (resources != null) {
if (Build.VERSION.SDK_INT >= 24) {
Object resourceImpl = AndroidHack.Resources_ResourcesImpl.get(resources);
AndroidHack.ResourcesImpl_mAssets.set(resourceImpl, newAssetManager);
} else {
AndroidHack.Resources_mAssets.set(resources, newAssetManager);
}
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
}
return true;
} catch (Throwable throwable) {
Logger.e(TAG, throwable);
throwable.printStackTrace();
}
return false;
}
}
這樣一來,就在Appliction啟動的時候完成了資源的熱修復,當然我們也可以像instant run那樣,把activity也處理,不過我們簡單起見,讓其重啟生效,所以activity就不處理了。
於是,我們Appliction的onCreate()中的代碼就變成了下面這個樣子
if (hasResourcePatch){
if (!AndroidHack.defineAndVerify()) {
//不加載patch資源
return;
}
//加載patch資源邏輯
File file = new File("/path/to/patchResource.apk");
ResourceLoader.patchResources(this, file);
}
這裡有一個坑。
patch應用成功後,如果要刪除patch,patch文件的刪除一定要謹慎,最好先通過配置文件標記patch不可用,下次啟動時檢測該標記,然後再刪除,運行期刪除正在使用的patch文件會導致所有進程的重啟,Application中的所有邏輯會被初始化一次。
還差最後一步,patch的資源從哪裡來,這裡主要講兩種方式。
直接下發整個apk文件,全量的資源,想怎麼用就怎麼用,當然缺點很明顯,文件太大了,下載容易出錯,不過也最簡單。 下發patch部分的資源,在客戶端和沒改變的資源合成新的apk,這種方式的優點是文件小,缺點是合成時占內存,需要開啟多進程去合成,比較復雜,沒有辦法校驗合成文件的md5值。無論哪一種方式,都需要public.xml去固定資源id。
這裡討論的是第二種方式,所以給出精簡版的實現思路:
首先需要生成public.xml,public.xml的生成通過aapt編譯時添加-P參數生成。相關代碼通過gradle插件去hook Task無縫加入該參數,有一點需要注意,通過appt生成的public.xml並不是可以直接用的,該文件中存在id類型的資源,生成patch時應用進去編譯的時候會報resource is not defined,解決方法是將id類型的資源單獨記錄到ids.xml文件中,相當於一個聲明過程,編譯的時候和public.xml一樣,將ids.xml也參與編譯即可。
/**
* 添加aapt addition -P選項
*/
String processResourcesTaskName = variant.variantData.getScope().getGenerateRClassTask().name
ProcessAndroidResources processResourcesTask = (ProcessAndroidResources) project.tasks.getByName(processResourcesTaskName)
Closure generatePubicXmlClosure = {
if (processResourcesTask) {
//添加-P 參數,生成public.xml
AaptOptions aaptOptions = processResourcesTask.aaptOptions
File outPublicXml = new File(outputDir, PUBLIC_XML)
aaptOptions.additionalParameters("-P", outPublicXml.getAbsolutePath())
processResourcesTask.setAaptOptions(aaptOptions)
}
}
/**
* public.xml中對一些選項進行剔除,目前處理id類型資源,不然應用的時候編譯不過,會報resource is not defined,主要是生成一個ids.xml,相當於對這部分資源進行聲明
*/
Closure handlePubicXmlClosure = {
if (processResourcesTask) {
File outPublicXml = new File(outputDir, PUBLIC_XML)
if (outPublicXml.exists()) {
SAXReader reader = new SAXReader();
Document document = reader.read(outPublicXml);
Element root = document.getRootElement();
List childElements = root.elements();
File idsFile = new File(outPublicXml.getParentFile(), IDS_XML)
if (idsFile.exists()) {
idsFile.delete()
}
if (!idsFile.exists()) {
idsFile.getParentFile().mkdirs()
idsFile.createNewFile()
}
idsFile.append("")
idsFile.append("\n")
idsFile.append("")
idsFile.append("\n")
for (Element child : childElements) {
String attrName = child.attribute("name").value
String attrType = child.attribute("type").value
if ("id".equalsIgnoreCase(attrType)) {
String value = child.asXML()
idsFile.append(" - \n")
project.logger.error "write id item ${attrName}"
}
}
idsFile.append("
")
}
}
}
if (processResourcesTask) {
processResourcesTask.doFirst(generatePubicXmlClosure);
processResourcesTask.doLast(handlePubicXmlClosure)
}
在編譯資源之前,將public.xml和ids.xml文件拷貝到資源目錄values下,並檢測values.xml文件中是否有已經定義的id類型的資源,如果有,則從ids.xml文件中將其刪除,否則會報resource is already defined的異常,也會編譯不過去。
/**
* 應用public.xml
*/
String mergeResourcesTaskName = variant.variantData.getScope().getMergeResourcesTask().name
MergeResources mergeResourcesTask = (MergeResources) project.tasks.getByName(mergeResourcesTaskName)
Closure applyPubicXmlClosure = {
if (mergeResourcesTask != null) {
if (oldTinkerDir != null && needApplyPublicXml) {
File publicXmlFile = new File(oldTinkerDir, "${dirName}/${PUBLIC_XML}")
if (publicXmlFile.exists()) {
File toDir = new File(mergeResourcesTask.outputDir, "values")
project.copy {
project.logger.error "\n$variant.name:copy a ${PUBLIC_XML} from ${publicXmlFile.getAbsolutePath()} to ${toDir}/${PUBLIC_XML}"
from(publicXmlFile.getParentFile()) {
include PUBLIC_XML
rename PUBLIC_XML, "${PUBLIC_XML}"
}
into(toDir)
}
} else {
logger.error("${publicXmlFile.absolutePath} does not exist")
}
File valuesFile = new File(mergeResourcesTask.outputDir, "values/values.xml")
File oldIdsFile = new File(oldTinkerDir, "${dirName}/${IDS_XML}")
if (valuesFile.exists() && oldIdsFile.exists()) {
SAXReader valuesReader = new SAXReader();
Document valuesDocument = valuesReader.read(valuesFile);
Element valuesRoot = valuesDocument.getRootElement()
List publicIds = valuesRoot.selectNodes("item[@type='id']")
if (publicIds != null && publicIds.size() != 0) {
Set existIdItems = new HashSet();
for (Element element : publicIds) {
existIdItems.add(element.attribute("name").value)
}
logger.error "existIdItems:${existIdItems}"
SAXReader oldIdsReader = new SAXReader();
Document oldIdsDocument = oldIdsReader.read(oldIdsFile);
Element oldIdsRoot = oldIdsDocument.getRootElement();
List oldElements = oldIdsRoot.elements();
if (oldElements != null && oldElements.size() != 0) {
File newIdsFile = new File(mergeResourcesTask.outputDir, "values/${IDS_XML}")
newIdsFile.append("")
newIdsFile.append("\n")
newIdsFile.append("")
newIdsFile.append("\n")
for (Element element : oldElements) {
String itemName = element.attribute("name").value
if (!existIdItems.contains(itemName)) {
newIdsFile.append(" ${element.asXML()}\n")
} else {
logger.error "already exist id item ${itemName}"
}
}
newIdsFile.append(" ")
}
}
} else {
logger.error("${valuesFile.absolutePath} does not exist")
}
} else {
logger.error "res not changed.not to apply public.xml"
}
}
}
if (mergeResourcesTask) {
mergeResourcesTask.doLast(applyPubicXmlClosure);
}
這樣一來,按照正常流程去編譯,生成的apk安裝包就可以獲得了,然後將這個new.apk和有問題的old.apk進行差量算法,這裡只考慮資源相關文件,即assets目錄,res目錄,arsc文件,AndroidManifest.xml文件,相關算法如下:
對比new.apk和old.apk中的所有資源相關的文件。 對於新增資源文件,則直接壓入patch.apk中。 對於刪除的資源文件,則不處理到patch.apk中。 對於改變的資源文件,如果是assets或者res目錄中的資源,則直接壓縮到patch.apk中,如果是arsc文件,則使用bsdiff算法計算其差量文件,壓入patch.apk,文件名不變。 對於改變和新增的文件,通過一個meta文件去記錄其原始文件的adler32和合成後預期文件的adler32值,以及文件名,這是個文本文件,直接壓縮到patch.apk中去。 對patch.apk進行簽名。這樣做的好處是能將資源patch文件盡可能的減小到最低,實際情況嚴重下來,res目錄下的資源文件大小都非常小,沒有必要去進行diff,所以直接使用原文件,而arsc文件則相對比較大,在考慮文件大小和內存的兩個因素下,犧牲內存換大小還是ok的,所以在下發前,我們對其進行diff,生成diff文件,在客戶端進行合成最終的arsc文件。
客戶端下載到patch.apk後需要進行還原,還原的步驟如下:
考慮到客戶端jni的兼容性問題,bspatch算法全部使用java實現 首先校驗patch.apk的簽名 讀取壓縮包中meta文件,判斷哪些文件是新增文件,哪些文件是改變的文件。 遍歷patch.apk中的文件,如果是新增文件,則壓縮到new.apk文件中去 如果是改變的文件,如果是assets和res文件夾下的資源,則直接壓縮到new.apk文件中,如果是arsc文件,則應用bspatch算法合成最終的arsc文件,壓縮到new.apk中 如果文件沒有改變,則直接復制old.apk中的原始文件到new.apk中 以上任何一個步驟都會去校驗合成時舊文件的adler32和合成後的adler32值和meta文件中記錄的是否符合 由於無法驗證合成後的文件的md5值(沒有記錄哪些文件被刪除了,加上壓縮算法等原因),需要使用一種方式在加載前進行驗證,這裡使用crc32值。 合成成功後計算new.apk文件的crc32值,計算方式進行改進,不計算所有文件內容的crc32,為了快速計算,只計算文件的某一個特定段的crc32值,比如文件從200字節開始到2000字節部分的crc32值,並保存在sharePrefrences中,加載patch前進行校驗crc32,校驗不通過,則直接刪除patch文件,當然這種計算方式有一定概率會把錯誤的文件當成正確的,畢竟計算的不是完整的文件,當然正確的文件是一定不會當成錯誤的,這種低概率事件可以接受。這種方式的兼容性如何?簡單自測了下,4.0-7.0的模擬器運行全部通過,當然不排除國產奇葩ROM的兼容性,所以這裡我不宣稱100%兼容。
無圖言屌,沒圖你說個jb,先上一張沒有進行熱修復的圖:

熱修復之後的效果圖

最後送上一句話:
apktool編譯和反編譯apk與ecplise多渠道打包
想自己做個apk,還在為素材而苦惱嗎?看到優秀的apk設計,還在為怎麼看到別人的實現代碼而苦惱嗎?看著AndroidStudio 多渠道打包那麼爽,而自己坑爹的還在用Ec
Android登陸界面實現清除輸入框內容和震動效果
本文為大家分享Android登陸界面實現清除輸入框內容和震動效果的全部代碼,具體內容如下:效果圖:主要代碼如下自定義的一個EditText,用於實現有文字的時候顯示可以清
Android動畫之二:View Animation
如上一篇博客《Android動畫之一:Drawable Animation》所說,android動畫主要分為三大部分,上一篇博客已經講解Drawable Animatio
Android中gson、jsonobject解析JSON的方法詳解
JSON的定義: 一種輕量級的數據交換格式,具有良好的可讀和便於快速編寫的特性。業內主流技術為其提供了完整的解決方案(有點類似於正則表達式 ,獲得了當今大部分語言的支持)