編輯:關於Android編程
先回顧下前文描述的使用方法:
public class MainApplication extends Application {
private static final String TAG = " andrew";
private static final String APATCH_PATH = "/out.apatch";
private static final String DIR = "apatch";//補丁文件夾
/**
* patch manager
*/
private PatchManager mPatchManager;
@Override
public void onCreate() {
super.onCreate();
// initialize
mPatchManager = new PatchManager(this);
mPatchManager.init("1.0");
Log.d(TAG, "inited.");
// load patch
mPatchManager.loadPatch();
try {
// .apatch file path
String patchFileString = Environment.getExternalStorageDirectory()
.getAbsolutePath() + APATCH_PATH;
mPatchManager.addPatch(patchFileString);
Log.d(TAG, "apatch:" + patchFileString + " added.");
//復制且加載補丁成功後,刪除下載的補丁
File f = new File(this.getFilesDir(), DIR + APATCH_PATH);
if (f.exists()) {
boolean result = new File(patchFileString).delete();
if (!result)
Log.e(TAG, patchFileString + " delete fail");
}
} catch (IOException e) {
Log.e(TAG, "", e);
}
}
}
mPatchManager = new PatchManager(this);
SP_VERSION 更多象征app的版本,該值不變時,打補丁;改變時,清空補丁
// patch extension
private static final String SUFFIX = ".apatch";//後綴名
private static final String DIR = "apatch";//補丁文件夾
private static final String SP_NAME = "_andfix_";
private static final String SP_VERSION = "version";//熱更新補丁時,版本不變,自動加載補丁;apk完整更新發布時,版本提升,本地會自動刪除以前加載在apatch文件夾裡的補丁,防止二次載入過時補丁
/**
* context
*/
private final Context mContext;
/**
* AndFix manager
*/
private final AndFixManager mAndFixManager;
/**
* patch directory
*/
private final File mPatchDir;
/**
* patchs
*/
private final SortedSet mPatchs;
/**
* classloaders
*/
private final Map mLoaders;
/**
* @param context context
*/
public PatchManager(Context context) {
mContext = context;
mAndFixManager = new AndFixManager(mContext);//初始化AndFixManager
mPatchDir = new File(mContext.getFilesDir(), DIR);//初始化存放patch補丁文件的文件夾, data/data/包名/files/patch
mPatchs = new ConcurrentSkipListSet();//初始化存在Patch類的集合,此類適合大並發
mLoaders = new ConcurrentHashMap();//初始化存放類對應的類加載器集合
}
大致就是從SharedPreferences讀取以前存的版本和你傳過來的版本進行比對,如果兩者版本不一致就刪除本地patch,否則調用initPatchs()這個方法
/**
* initialize
*
* @param appVersion App version
*/
public void init(String appVersion) {
if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
Log.e(TAG, "patch dir create error.");
return;
} else if (!mPatchDir.isDirectory()) {//如果遇到同名的文件,則將該同名文件刪除
mPatchDir.delete();
return;
}
//在該文件下放入一個名為_andfix_的SharedPreferences文件
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);//存儲關於patch文件的信息
//根據你傳入的版本號和之前的對比,做不同的處理
String ver = sp.getString(SP_VERSION, null);
//根據版本號加載補丁文件,版本號不同清空緩存目錄
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
cleanPatch();//刪除本地patch文件
sp.edit().putString(SP_VERSION, appVersion).commit();//並把傳入的版本號保存
} else {
initPatchs();//初始化patch列表,把本地的patch文件加載到內存
}
}
分析下initPatchs()它做了什麼,其實代碼很簡單,就是把mPatchDir文件夾下的文件作為參數傳給了addPatch(File)方法,然後調用addPatch()方法
private void initPatchs() {
File[] files = mPatchDir.listFiles();
for (File file : files) {
addPatch(file);
}
}
/**
* add patch file
*
* @param file
* @return patch
*/
//把擴展名為.apatch的文件傳給Patch做參數,初始化對應的Patch,
//並把剛初始化的Patch加入到我們之前看到的Patch集合mPatchs中
private Patch addPatch(File file) {
Patch patch = null;
if (file.getName().endsWith(SUFFIX)) {
try {
patch = new Patch(file);//實例化Patch對象
mPatchs.add(patch);//把patch實例存儲到內存的集合中,在PatchManager實例化集合
} catch (IOException e) {
Log.e(TAG, "addPatch", e);
}
}
return patch;
}
可以看到裡面有JarFile, JarEntry, Manifest, Attributes,通過它們一層層的從Jar文件中獲取相應的值,提到這裡大家可能會奇怪,明明是.patch文件,怎麼又變成Jar文件了?其實是通過阿裡打補丁包工具生成補丁的時候寫入相應的值,補丁文件其實就相到於jar包,只不過它們的擴展名不同而已
public class Patch implements Comparable {
private static final String ENTRY_NAME = "META-INF/PATCH.MF";
private static final String CLASSES = "-Classes";
private static final String PATCH_CLASSES = "Patch-Classes";
private static final String CREATED_TIME = "Created-Time";
private static final String PATCH_NAME = "Patch-Name";
/**
* patch file
*/
private final File mFile;
/**
* name
*/
private String mName;
/**
* create time
*/
private Date mTime;
/**
* classes of patch
*/
private Map> mClassesMap;
public Patch(File file) throws IOException {
mFile = file;
init();
}
@SuppressWarnings("deprecation")
private void init() throws IOException {
JarFile jarFile = null;
InputStream inputStream = null;
try {
jarFile = new JarFile(mFile);//使用JarFile讀取Patch文件
JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);//獲取META-INF/PATCH.MF文件
inputStream = jarFile.getInputStream(entry);
Manifest manifest = new Manifest(inputStream);
Attributes main = manifest.getMainAttributes();
mName = main.getValue(PATCH_NAME);//獲取PATCH.MF屬性Patch-Name
mTime = new Date(main.getValue(CREATED_TIME));//獲取PATCH.MF屬性Created-Time
mClassesMap = new HashMap>();
Attributes.Name attrName;
String name;
List strings;
for (Iterator it = main.keySet().iterator(); it.hasNext();) {
attrName = (Attributes.Name) it.next();
name = attrName.toString();
//判斷name的後綴是否是-Classes,並把name對應的值加入到集合中,對應的值就是class類名的列表
if (name.endsWith(CLASSES)) {
strings = Arrays.asList(main.getValue(attrName).split(","));
if (name.equalsIgnoreCase(PATCH_CLASSES)) {
mClassesMap.put(mName, strings);
} else {
mClassesMap.put(
name.trim().substring(0, name.length() - 8),// remove
// "-Classes"
strings);
}
}
}
} finally {
if (jarFile != null) {
jarFile.close();
}
if (inputStream != null) {
inputStream.close();
}
}
}
public String getName() {
return mName;
}
public File getFile() {
return mFile;
}
public Set getPatchNames() {
return mClassesMap.keySet();
}
public List getClasses(String patchName) {
return mClassesMap.get(patchName);
}
public Date getTime() {
return mTime;
}
@Override
public int compareTo(Patch another) {
return mTime.compareTo(another.getTime());
}
}
這個方法就是遍歷mPatchs中每個patch的每個類,mPatchs就是上文介紹的存儲patch的一個集合。根據補丁名找到對應的類,做為參數傳給fix()
/**
* load patch,call when application start
*/
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set patchNames;
List classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
for (String patchName : patchNames) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);
}
}
}
總結 一下, java 層的功能就是找到補丁文件,根據補丁中的注解找到將要替換的方法然後交給jni層去處理替換方法的操作
遍歷dexFile文件中所有的類, 如果有需要修改的類集合中在這個Dex文件中找到了一樣的類,則使用loadClass(String, ClassLoader)加載這個類, 然後調用fixClass(String, ClassLoader)修復這個類
/**
* fix
*
* @param file
* patch file
* @param classLoader
* classloader of class that will be fixed
* @param classes
* classes will be fixed
*/
public synchronized void fix(File file, ClassLoader classLoader,
List classes) {
if (!mSupport) {
return;
}
//判斷patch文件的簽名
if (!mSecurityChecker.verifyApk(file)) {// security check fail
return;
}
try {
File optfile = new File(mOptDir, file.getName());
boolean saveFingerprint = true;
if (optfile.exists()) {
// need to verify fingerprint when the optimize file exist,
// prevent someone attack on jailbreak device with
// Vulnerability-Parasyte.
// btw:exaggerated android Vulnerability-Parasyte
// http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html
if (mSecurityChecker.verifyOpt(optfile)) {
saveFingerprint = false;
} else if (!optfile.delete()) {
return;
}
}
//加載patch文件中的dex
final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE);
if (saveFingerprint) {
mSecurityChecker.saveOptSig(optfile);
}
ClassLoader patchClassLoader = new ClassLoader(classLoader) {
@Override
protected Class findClass(String className)
throws ClassNotFoundException {
Class clazz = dexFile.loadClass(className, this);
if (clazz == null
&& className.startsWith("com.alipay.euler.andfix")) {
return Class.forName(className);// annotation’s class
// not found
}
if (clazz == null) {
throw new ClassNotFoundException(className);
}
return clazz;
}
};
Enumeration entrys = dexFile.entries();
Class clazz = null;
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
if (classes != null && !classes.contains(entry)) {
continue;// skip, not need fix
}
clazz = dexFile.loadClass(entry, patchClassLoader);//獲取有bug的類文件
if (clazz != null) {
fixClass(clazz, classLoader);
}
}
} catch (IOException e) {
Log.e(TAG, "pacth", e);
}
}
/**
* fix class
*
* @param clazz
* class
*/
private void fixClass(Class clazz, ClassLoader classLoader) {
//使用反射獲取這個類中所有的方法
Method[] methods = clazz.getDeclaredMethods();
//MethodReplace是這個庫自定義的Annotation,標記哪個方法需要被替換
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
//獲取此方法的注解,因為有bug的方法在生成的patch的類中的方法都是有注解的
//還記得對比過程中生成的Annotation注解嗎
//這裡通過注解找到需要替換掉的方法
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();//獲取注解中clazz的值,標記的類
meth = methodReplace.method();//獲取注解中method的值,需要替換的方法
if (!isEmpty(clz) && !isEmpty(meth)) {
//所有找到的方法,循環替換
replaceMethod(classLoader, clz, meth, method);
}
}
}
/**
* replace method
*
* @param classLoader classloader
* @param clz class
* @param meth name of target method
* @param method source method
*/
private void replaceMethod(ClassLoader classLoader, String clz,
String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
Class clazz = mFixedClass.get(key);//判斷此類是否被fix
if (clazz == null) {// class not load
Class clzz = classLoader.loadClass(clz);
// initialize target class
clazz = AndFix.initTargetClass(clzz);//初始化class
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());//根據反射獲取到有bug的類的方法(有bug的apk)
AndFix.addReplaceMethod(src, method);//src是有bug的方法,method是補丁方法
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
調用jni替換,src是有bug的方法,method是補丁方法
private static native boolean setup(boolean isArt, int apilevel);
private static native void replaceMethod(Method dest, Method src);
private static native void setFieldFlag(Field field);
public static void addReplaceMethod(Method src, Method dest) {
try {
replaceMethod(src, dest);//調用了native方法,next code
initFields(dest.getDeclaringClass());
} catch (Throwable e) {
Log.e(TAG, "addReplaceMethod", e);
}
}
總結 一下, java 層的功能就是找到補丁文件,根據補丁中的注解找到將要替換的方法然後交給jni層去處理替換方法的操作
替換原來方法的處理方式我們看起來會有點熟悉,一般的java hook差不多都是這樣的套路,在jni中找到要替換方法的Method對象,修改它的一些屬性,讓它指向新方法的Method對象。
以上所有的過程是在應用MainApplication的onCreate中被調用,所以當應用重啟後,原方法和補丁方法都被加載到內存中,並完成了替換,在後面的運行中就會執行補丁中的方法了。
AndFix的優點是像正常修復bug那樣來生成補丁包,但可以看出無論是dexposed還是AndFix,都利用了java hook的技術來替換要修復的方法,這就需要我們理解dalvik虛擬機加載、運行java方法的機制,並要掌握libdvm中一些關鍵的數據結構和函數的使用。
static jboolean setup(JNIEnv* env, jclass clazz, jboolean isart,
jint apilevel) {
isArt = isart;
LOGD("vm is: %s , apilevel is: %i", (isArt ? "art" : "dalvik"),
(int )apilevel);
if (isArt) {
return art_setup(env, (int) apilevel);
} else {
return dalvik_setup(env, (int) apilevel);
}
}
static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,
jobject dest) {
if (isArt) {
art_replaceMethod(env, src, dest);
} else {
dalvik_replaceMethod(env, src, dest);
}
}
根據上層傳過來的 isArt 判斷調用 Dalvik 還是 Art 的方法。
以 Dalvik 為例,繼續往下分析,代碼在 dalvik_method_replace.cpp 中
dalvik_setup 方法
extern jboolean __attribute__ ((visibility ("hidden"))) dalvik_setup(
JNIEnv* env, int apilevel) {
jni_env = env;
void* dvm_hand = dlopen("libdvm.so", RTLD_NOW);
if (dvm_hand) {
...
//使用dlsym方法將dvmCallMethod_fnPtr函數指針指向libdvm.so中的 //dvmCallMethod方法,也就是說可以通過調用該函數指針執行其指向的方法
//下面會用到dvmCallMethod_fnPtr
dvmCallMethod_fnPtr = dvm_dlsym(dvm_hand,
apilevel > 10 ?
"_Z13dvmCallMethodP6ThreadPK6MethodP6ObjectP6JValuez" :
"dvmCallMethod");
...
}
}
替換方法的關鍵在於 native 層怎麼影響內存裡的java代碼,我們知道 java 代碼裡將一個方法聲明為 native 方法時,對此函數的調用就會到 native 世界裡找,AndFix原理就是將一個不是native的方法修改成native方法,然後在 native 層進行替換,通過 dvmCallMethod_fnPtr 函數指針來調用 libdvm.so 中的 dvmCallMethod() 來加載替換後的新方法,達到替換方法的目的。 Jni 反射調用 java 方法時要用到一個 jmethodID 指針,這個指針在 Dalvik 裡其實就是 Method 類,通過修改這個類的一些屬性就可以實現在運行時將一個方法修改成 native 方法。
看下 dalvik_replaceMethod(env, src, dest);
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
//設置為初始化完畢
clz->status = CLASS_INITIALIZED;
//meth是將要被替換的方法
Method* meth = (Method*) env->FromReflectedMethod(src);
//target是新的方法
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);
meth->jniArgInfo = 0x80000000;
//修改method的屬性,將meth設置為native方法
meth->accessFlags |= ACC_NATIVE;
int argsSize = dvmComputeMethodArgsSize_fnPtr(meth);
if (!dvmIsStaticMethod(meth))
argsSize++;
meth->registersSize = meth->insSize = argsSize;
//將新的方法信息保存到insns
meth->insns = (void*) target;
//綁定橋接函數,java方法的跳轉函數
meth->nativeFunc = dalvik_dispatcher;
}
static void dalvik_dispatcher(const u4* args, jvalue* pResult,
const Method* method, void* self) {
Method* meth = (Method*) method->insns;
meth->accessFlags = meth->accessFlags | ACC_PUBLIC;
if (!dvmIsStaticMethod(meth)) {
Object* thisObj = (Object*) args[0];
ClassObject* tmp = thisObj->clazz;
thisObj->clazz = meth->clazz;
argArray = boxMethodArgs(meth, args + 1);
if (dvmCheckException_fnPtr(self))
goto bail;
dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
dvmCreateReflectMethodObject_fnPtr(meth), &result, thisObj,
argArray);
thisObj->clazz = tmp;
} else {
argArray = boxMethodArgs(meth, args);
if (dvmCheckException_fnPtr(self))
goto bail;
dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
dvmCreateReflectMethodObject_fnPtr(meth), &result, NULL,
argArray);
}
bail: dvmReleaseTrackedAlloc_fnPtr((Object*) argArray, self);
通過 dalvik_dispatcher 這個跳轉函數完成最後的替換工作,到這裡就完成了兩個方法的替換,有問題的方法就可以被修復後的方法取代。ART的替換方法就不講了,原理上差別不大。
// 緩存目錄data/data/package/file/apatch/會緩存補丁文件
// 即使原目錄被刪除也可以打補丁
/**
* add patch at runtime
*
* @param path patch path
* @throws IOException
*/
public void addPatch(String path) throws IOException {
File src = new File(path);
File dest = new File(mPatchDir, src.getName());
if (!src.exists()) {
throw new FileNotFoundException(path);
}
if (dest.exists()) {
Log.d(TAG, "patch [" + src.getName() + "] has be loaded.");
boolean deleteResult = dest.delete();
if (deleteResult)
Log.e(TAG, "patch [" + dest.getPath() + "] has be delete.");
else {
Log.e(TAG, "patch [" + dest.getPath() + "] delete error");
return;
}
}
//拷貝文件
FileUtil.copyFile(src, dest);// copy to patch's directory
Patch patch = addPatch(dest);
if (patch != null) {
loadPatch(patch);
}
}
遇到一個問題:昨天模擬器工作還正常,今天eclipse就識別不了了。後來發現是360手機助手占用了5555端口造成的,我就納悶了,平時這個也不是自動啟動,今天就啟動了。廢
先看一個效果圖本節課程實現完成右圖效果(三步)以及保存塗鴉過的圖片步驟【1】將背景Bitmap圖片畫到底層canvas上 bitmapBackground = Bitma
Android 自定義雙向滑動SeekBar ,一些需要價格區間選擇的App可能需要用到1. 自定義MySeekBar 繼承 View,先給一張效果圖。2.原理:自定義a