編輯:關於Android編程
Android熱補丁動態修復技術(三)這篇博文其實在4月8日的晚上已經發布了,然後緊接著寫第四篇,但是我將(四)保存到草稿箱時,發現已經發布的(三)消失了,取而代之的是第四篇博文。
在論壇問過版主,可能是因為我誤操作導致的,第三篇博文已經無法恢復。
真是手賤!寫了好幾天的東西啊,不過比起誤操作我更傾向認為這是csdn的bug……
markdown編輯器絕對有坑!光是寫新文章時不會自動清楚緩存我認為就是一個很嚴重的Bug了!
因為第三篇博文消失的原因,伴隨著演示的Demo項目也修改了很多內容,我也沒那麼精力重新寫一篇,就和第四篇博文合並在一起當做第三篇吧,這可能導致內容跨度有些大,並且不會像之前的博文這麼詳細,希望大家多多支持和理解。
上一篇博客中,我們再Application中成功注入了patch_dex.jar到ClassLoader中。
但是伴隨著CLASS_ISPREVERIFIED問題,解決方式就在在所有類的構造函數中添加一行代碼System.out.println(AntilazyLoad.class);
我們來分析一下如何在所有類的構造函數中添加System.out.println(AntilazyLoad.class);
第二點是可行的,但是AndroidStudio項目是使用Gradle構建的,編譯-打包-簽名都是自動化。
我們在什麼時候注入代碼?
看過我上一篇博文推薦的文章就知道,Gradle是通過一個一個Task執行完成整個流程的,其中肯定也有將所有class打包成dex的task。
(在gradle plugin 1.5 以上和以下版本有些不同)
Transfrom是Gradle 1.5以上新出的一個api,其實它也是Task,不過定義方式和Task有點區別。
對於熱補丁來說,Transfrom反而比原先的Task更好用。
在Transfrom這個api出來之前,想要在項目被打包成dex之前對class進行操作,必須自定義一個Task,然後插入到predex或者dex之前,在自定義的Task中可以使用javassist或者asm對class進行操作。
而Transform則更為方便,Transfrom會有他自己的執行時機,不需要我們插入到某個Task前面。Tranfrom一經注冊便會自動添加到Task執行序列中,並且正好是項目被打包成dex之前。
而本文就是使用Gradle1.5以上版本,下面則是Google對Transfrom的描述文檔。
http://tools.android.com/tech-docs/new-build-system/transform-api
有時候會訪問不了,你可能需要一把梯子……
Gradle可以看做是一個腳本,包含一系列的Task,依次執行這些task後,項目就打包成功了。
而Task有一個重要的概念,那就是inputs和outputs。
Task通過inputs拿到一些東西,處理完畢之後就輸出outputs,而下一個Task的inputs則是上一個Task的outputs。
例如:一個Task的作用是將java編譯成class,這個Task的inputs就是java文件的保存目錄,outputs這是編譯後的class的輸出目錄,它的下一個Task的inputs就會是編譯後的class的保存目錄了。
Gradle中除了Task這個重要的api,還有一個就是Plugin。
Plugin的作用是什麼呢,這一兩句話比較難以說明。
Gralde只能算是一個構建框架,裡面的那麼多Task是怎麼來的呢,誰定義的呢?
是Plugin,細心的網友會發現,在module下的build.gradle文件中的第一行,往往會有apply plugin : 'com.android.application'
亦或者apply plugin : 'com.android.library'
。
com.android.application
:這是app module下Build.gradle的
com.android.library
:這是app依賴的module中的Builde.gradle的
就是這些Plugin為項目構建提供了Task,使用不同的plugin,module的功能也就不一樣。
可以簡單的理解為: Gradle只是一個框架,真正起作用的是plugin。而plugin的主要作用是往Gradle腳本中添加Task。
當然,實際上這些是很復雜的東西,plugin還有其他作用這裡用不上。
我們可以自定義一個plugin,然後使用plugin注冊一個Transfrom。
在此之前,先教大家怎麼自定義一個plugin。
1. 新建一個module,選擇library module,module名字必須叫BuildSrc
2. 刪除module下的所有文件,除了build.gradle,清空build.gradle中的內容
3. 然後新建以下目錄 src-main-groovy
4. 修改build.gradle如下,同步
```
apply plugin: 'groovy'
repositories {
jcenter()
}
dependencies {
compile gradleApi()
compile 'com.android.tools.build:gradle:1.5.0'
compile 'org.javassist:javassist:3.20.0-GA'
}
```
5. 這時候就可以像普通module一樣新建package和類了,不過這裡的類是以groovy結尾,新建類的時候選擇file,並且以.groovy作為後綴。
Register就是我自定義個Plugin(無視黑色塗塊,Demo被我修改太多了,再次鄙視csdn)
代碼如下
package com.aitsuki.plugin
import org.gradle.api.Plugin;
import org.gradle.api.Project
/**
* Created by hp on 2016/4/8.
*/
public class Register implements Plugin {
@Override
public void apply(Project project) {
project.logger.error "================自定義插件成功!=========="
}
}
在app module下的buiil.gradle中添apply 插件
說明:如果plugin所在的module名不叫BuildSrc,這裡是無法apply包名的,會提示找不到。所以之前也說明取名一定要叫buildsrc<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4KCjxwPtTL0NDSu8/Cz+7Ev77Nv8nS1L+0tb2hsT09PT09PT09PT09PT09PT3X1Lao0uWy5bz+s8m5pqOhPT09PT09PT09PaGx1eK+5LuwwcsgPGJyPgq6zWdyYWRsZdPQudi1xMrks/a2vLvhz9TKvtTaZ3JhZGxlIGNvbnNvbGXV4rj2tLC/2tbQoaMgPGJyPgo8aW1nIHNyYz0="/uploadfile/Collfiles/20160415/20160415090956429.png" alt="這裡寫圖片描述" title="\">
新建一個groovy繼承Transfrom,注意這個Transfrom是要com.android.build.api.transform.Transform
這個包的
要先添加依賴才能導入此包,如下
dependencies {
compile gradleApi()
compile 'com.android.tools.build:gradle:1.5.0'
compile 'org.javassist:javassist:3.20.0-GA'
}
javassist待會要用到,順便添加進來了。
我們定義一個PreDexTransform,代碼如下
package com.aitsuki.plugin
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.gradle.api.Project
public class PreDexTransform extends Transform {
Project project
// 添加構造,為了方便從plugin中拿到project對象,待會有用
public PreDexTransform(Project project) {
this.project = project
}
// Transfrom在Task列表中的名字
// TransfromClassesWithPreDexForXXXX
@Override
String getName() {
return "preDex"
}
// 指定input的類型
@Override
Set getInputTypes() {
return TransformManager.CONTENT_CLASS
}
// 指定Transfrom的作用范圍
@Override
Set getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(Context context, Collection inputs,
Collection referencedInputs,
TransformOutputProvider outputProvider, boolean isIncremental)
throws IOException, TransformException, InterruptedException {
// inputs就是輸入文件的集合
// outputProvider可以獲取outputs的路徑
}
}
然後再Register這個plugin的apply方法中添加一下代碼,注冊Transfrom
def android = project.extensions.findByType(AppExtension)
android.registerTransform(new PreDexTransform(project))
再次運行項目(需要先clean項目,否則apply plugin不會重新編譯)
首先,我們看到自定義的PreDexTransfrom已經運行了,但是接下來的DexTransform卻報錯了。
那是因為我們自定義的Transfrom的transfrom方法為空,沒有將inputs輸出到outputs,DexTransfrom是在PreDexTransfrom下面,獲取到的inputs為空,所以就報錯了。
我們只需要在Tranfrom中將inputs文件復制到ouputs目錄就可以了,代碼如下。
// Transfrom的inputs有兩種類型,一種是目錄,一種是jar包,要分開遍歷
inputs.each {TransformInput input ->
input.directoryInputs.each {DirectoryInput directoryInput->
//TODO 這裡可以對input的文件做處理,比如代碼注入!
// 獲取output目錄
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
// 將input的目錄復制到output指定目錄
FileUtils.copyDirectory(directoryInput.file, dest)
}
input.jarInputs.each {JarInput jarInput->
//TODO 這裡可以對input的文件做處理,比如代碼注入!
// 重命名輸出文件(同目錄copyFile會沖突)
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if(jarName.endsWith(".jar")) {
jarName = jarName.substring(0,jarName.length()-4)
}
def dest = outputProvider.getContentLocation(jarName+md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
加入這段代碼到transform方法中再次運行就沒問題了,再次說明:要先Clean項目!
上面有兩個TODO注釋,我們在獲取inputs復制到outpus目錄之前,可以在這裡對class注入代碼!
我們先來看看Transfrom的inputs和outputs,這裡有個方法:
在app module下的build.gradle中添加以下代碼即可。
applicationVariants.all { variant->
def dexTask = project.tasks.findByName("transformClassesWithDexForDebug")
def preDexTask = project.tasks.findByName("transformClassesWithPreDexForDebug")
if(preDexTask) {
project.logger.error "======preDexTask======"
preDexTask.inputs.files.files.each {file ->
project.logger.error "inputs =$file.absolutePath"
}
preDexTask.outputs.files.files.each {file ->
project.logger.error "outputs =$file.absolutePath"
}
}
if(dexTask) {
project.logger.error "======dexTask======"
dexTask.inputs.files.files.each {file ->
project.logger.error "inputs =$file.absolutePath"
}
dexTask.outputs.files.files.each {file ->
project.logger.error "outputs =$file.absolutePath"
}
}
}
輸出如下:
glide和xutils是app依賴的jar包
hotpatch是我將application中加載dex的代碼抽取成獨立module後,app依賴此module的結果
其余的則是項目默認依賴的jar包。
得出一個結論,app依賴的module在dex之前會被打包成classes.jar,和其他依賴的jar包一起放到exploded-arr
這個目錄。
而依賴的module會放在exploded-arr\項目名\module名
這個目錄下
附上hotPatch這個將application中的代碼打包好的module
然後這是inputs =D:\aitsuki\HotPatchDemo\app\build\intermediates\exploded-aar\HotPatchDemo\hotpatch\unspecified\jars\classes.jar
解壓後的結果
建議先去了解下javassit的最基本使用方法,否則可能看不懂我在說什麼。
注入System.out.println(AntilazyLoad.class);
這行代碼的時候,如果javasssit找到AntilazyLoad.class這個類就會拋異常
所以創建AntilazyLoad.class,並且將AntilazyLoad.class所在的路徑append到ClassPool的classpath中。
首先我們建一個hack module,如下
制作方式在上一篇博客中就有。
將AntilazyLoad.class復制到同包名的文件夾下,然後運行打包命令,不重復贅述了。
然後將hack.jar放到app module中的assets文件夾中,如圖
然後我們在加載patch_dex之前就要先將這個hack加載進classLoader,加載hack的方式和步驟跟加載補丁是一摸一樣的,不再贅述,具體請直接看Demo,最後面有下載鏈接。
代碼量稍多,我就不那麼詳細的解釋了,這裡說下最基本的兩點
app module編譯後class文件保存在debug目錄,直接遍歷這個目錄使用javassist注入代碼就行了 app module依賴的module,編譯後會被打包成jar,放在exploded-aar這個目錄,需要將jar包解壓–遍歷注入代碼–重新打包成jar首先我們專門寫一個用來操作javassist注入代碼的inject類。
package com.aitsuki.plugin
import javassist.ClassPool
import javassist.CtClass
import org.apache.commons.io.FileUtils
/**
* Created by AItsuki on 2016/4/7.
* 注入代碼分為兩種情況,一種是目錄,需要遍歷裡面的class進行注入
* 另外一種是jar包,需要先解壓jar包,注入代碼之後重新打包成jar
*/
public class Inject {
private static ClassPool pool = ClassPool.getDefault()
/**
* 添加classPath到ClassPool
* @param libPath
*/
public static void appendClassPath(String libPath) {
pool.appendClassPath(libPath)
}
/**
* 遍歷該目錄下的所有class,對所有class進行代碼注入。
* 其中以下class是不需要注入代碼的:
* --- 1. R文件相關
* --- 2. 配置文件相關(BuildConfig)
* --- 3. Application
* @param path 目錄的路徑
*/
public static void injectDir(String path) {
pool.appendClassPath(path)
File dir = new File(path)
if(dir.isDirectory()) {
dir.eachFileRecurse { File file ->
String filePath = file.absolutePath
if (filePath.endsWith(".class")
&& !filePath.contains('R$')
&& !filePath.contains('R.class')
&& !filePath.contains("BuildConfig.class")
// 這裡是application的名字,可以通過解析清單文件獲得,先寫死了
&& !filePath.contains("HotPatchApplication.class")) {
// 這裡是應用包名,也能從清單文件中獲取,先寫死
int index = filePath.indexOf("com\\aitsuki\\hotpatchdemo")
if (index != -1) {
int end = filePath.length() - 6 // .class = 6
String className = filePath.substring(index, end).replace('\\', '.').replace('/','.')
injectClass(className, path)
}
}
}
}
}
/**
* 這裡需要將jar包先解壓,注入代碼後再重新生成jar包
* @path jar包的絕對路徑
*/
public static void injectJar(String path) {
if (path.endsWith(".jar")) {
File jarFile = new File(path)
// jar包解壓後的保存路徑
String jarZipDir = jarFile.getParent() +"/"+jarFile.getName().replace('.jar','')
// 解壓jar包, 返回jar包中所有class的完整類名的集合(帶.class後綴)
List classNameList = JarZipUtil.unzipJar(path, jarZipDir)
// 刪除原來的jar包
jarFile.delete()
// 注入代碼
pool.appendClassPath(jarZipDir)
for(String className : classNameList) {
if (className.endsWith(".class")
&& !className.contains('R$')
&& !className.contains('R.class')
&& !className.contains("BuildConfig.class")) {
className = className.substring(0, className.length()-6)
injectClass(className, jarZipDir)
}
}
// 從新打包jar
JarZipUtil.zipJar(jarZipDir, path)
// 刪除目錄
FileUtils.deleteDirectory(new File(jarZipDir))
}
}
private static void injectClass(String className, String path) {
CtClass c = pool.getCtClass(className)
if (c.isFrozen()) {
c.defrost()
}
def constructor = c.getConstructors()[0];
// 這裡需要輸入完整類名,否則javassist會報錯
constructor.insertAfter("System.out.println(com.aitsuki.hack.AntilazyLoad.class);")
c.writeFile(path)
}
}
下面這是解壓縮jar包的類
package com.aitsuki.plugin
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
/**
* Created by hp on 2016/4/13.
*/
public class JarZipUtil {
/**
* 將該jar包解壓到指定目錄
* @param jarPath jar包的絕對路徑
* @param destDirPath jar包解壓後的保存路徑
* @return 返回該jar包中包含的所有class的完整類名類名集合,其中一條數據如:com.aitski.hotpatch.Xxxx.class
*/
public static List unzipJar(String jarPath, String destDirPath) {
List list = new ArrayList()
if (jarPath.endsWith('.jar')) {
JarFile jarFile = new JarFile(jarPath)
Enumeration jarEntrys = jarFile.entries()
while (jarEntrys.hasMoreElements()) {
JarEntry jarEntry = jarEntrys.nextElement()
if (jarEntry.directory) {
continue
}
String entryName = jarEntry.getName()
if (entryName.endsWith('.class')) {
String className = entryName.replace('\\', '.').replace('/', '.')
list.add(className)
}
String outFileName = destDirPath + "/" + entryName
File outFile = new File(outFileName)
outFile.getParentFile().mkdirs()
InputStream inputStream = jarFile.getInputStream(jarEntry)
FileOutputStream fileOutputStream = new FileOutputStream(outFile)
fileOutputStream << inputStream
fileOutputStream.close()
inputStream.close()
}
jarFile.close()
}
return list
}
/**
* 重新打包jar
* @param packagePath 將這個目錄下的所有文件打包成jar
* @param destPath 打包好的jar包的絕對路徑
*/
public static void zipJar(String packagePath, String destPath) {
File file = new File(packagePath)
JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(destPath))
file.eachFileRecurse { File f ->
String entryName = f.getAbsolutePath().substring(packagePath.length() + 1)
outputStream.putNextEntry(new ZipEntry(entryName))
if(!f.directory) {
InputStream inputStream = new FileInputStream(f)
outputStream << inputStream
inputStream.close()
}
}
outputStream.close()
}
}
然後再Transfrom中這麼使用,我將整個類再貼一遍好了
package com.aitsuki.plugin
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
public class PreDexTransform extends Transform {
Project project
public PreDexTransform(Project project) {
this.project = project
}
@Override
String getName() {
return "preDex"
}
@Override
Set getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(Context context, Collection inputs,
Collection referencedInputs,
TransformOutputProvider outputProvider, boolean isIncremental)
throws IOException, TransformException, InterruptedException {
// 獲取到hack module的debug目錄,也就是Antilazy.class所在的目錄。
def libPath = project.project(':hack').buildDir.absolutePath.concat("\\intermediates\\classes\\debug")
// 將路徑添加到Classpool的classPath
Inject.appendClassPath(libPath)
// 遍歷transfrom的inputs
// inputs有兩種類型,一種是目錄,一種是jar,需要分別遍歷。
inputs.each {TransformInput input ->
input.directoryInputs.each {DirectoryInput directoryInput->
// 獲取output目錄
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
//TODO 這裡可以對input的文件做處理,比如代碼注入!
Inject.injectDir(directoryInput.file.absolutePath)
// 將input的目錄復制到output指定目錄
FileUtils.copyDirectory(directoryInput.file, dest)
}
input.jarInputs.each {JarInput jarInput->
//TODO 這裡可以對input的文件做處理,比如代碼注入!
String jarPath = jarInput.file.absolutePath;
String projectName = project.rootProject.name;
if(jarPath.endsWith("classes.jar") && jarPath.contains("exploded-aar"+"\\"+projectName)) {
Inject.injectJar(jarPath)
}
// 重命名輸出文件(同目錄copyFile會沖突)
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if(jarName.endsWith(".jar")) {
jarName = jarName.substring(0,jarName.length()-4)
}
def dest = outputProvider.getContentLocation(jarName+md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
}
}
然後運行項目(最後再重復一次:記得先clean項目!),成功注入補丁!不報錯了
關於SDCard:如果手機支持TF卡,那麼請將補丁復制到內部存儲。
還有這裡是下載地址, 補丁已經放在根目錄
http://download.csdn.net/detail/u010386612/9490542
補充一點:在上面代碼中,我們為所有的module編譯後的jar注入了代碼。
實際上在hotpatch這個module是不需要注入代碼的,因為這個module是用於加載dex的,而執行該module的時候,AntilazyLoad.class肯定沒加載進來,所以注入代碼毫無作用,應該排除這個module
這篇博文解決了class_ispreverified問題,並且成功使用javassist注入字節碼,完成了熱補丁框架的雛形。
但是還有幾個需要解決的問題
1. 補丁沒有簽名校驗,不安全,容易被惡意注入代碼
2. 混淆開啟的情況下,類名可能被更換,補丁打包不成功。
下一篇博文可能是關於混淆或者補丁簽名
破解Android程序通常的方法是將apk文件利用ApkTool反編譯,生成Smali格式的反匯編代碼,然後閱讀Smali文件的代碼來理解程序的運行機制,找到
本文實例講述了Android編程自定義AlertDialog(退出提示框)用法,分享給大家供大家參考,具體如下:有時候我們需要在游戲或應用中用一些符合我們樣式的提示框(A
前言現在看來其實更像是一篇知識概括,多處可能未講清楚,於是打算重寫事件分發,用一篇文章大致講清楚。首先,形式上筆者最先思考的是使用源碼,此者能從原理上講解分發機制,比起侃
一、背景這個選題很大,但並不是一開始就有這麼高大上的追求。最初之時,只是源於對Xposed的好奇。Xposed幾乎是定制ROM的神器軟件技術架構或者說方法了。它到底是怎麼