本文來自http://blog.csdn.net/liuxian13183/ ,引用必須註明出處!
Java的註解、反射等機制的產生,讓動態代理成爲可能,一般通過全限定名+類名,找到類,可以invoke它的構造方法以及其他方法,可以獲取它的參數(Field)名稱和值。
註解一般用在代碼的註釋上、代碼審查上(有沒有按標準寫,比如inspect)、代碼注入(hook,asbectj),需要考慮的是,在何時注入(編譯期還運行期)
基礎反射:獲取類的變量名和變量值
public static Map<String, Object> toMap(Object obj) {
Map<String, Object> reMap = new HashMap<>();
Class<?> clz = obj.getClass();//獲取此對象運行期的Class對象
while (clz != null) {
Field[] fields = clz.getDeclaredFields();//獲得類裏聲明的變量
for (Field field : fields) {
try {
String fieldName = field.getName();//獲得變量名稱
if (!extraParam(fieldName)) {
Field f = clz.getDeclaredField(fieldName);
f.setAccessible(true);
Object o = f.get(obj);//獲得變量值
reMap.put(fieldName, o);
}
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
clz = clz.getSuperclass();//獲得父類的Class對象,繼續遞歸
}
return reMap;
}
private static boolean extraParam(String fieldName) {
//以下三種變量,通常不需要寫入
return fieldName.equals("serialVersionUID")//用於根據變量產生,在恢復本地文件時,如果id不同,則類不同,就不需要再恢復(說明緩存已經過期)
|| fieldName.equals("shadow$_klass_")//API20以後出現,運行期對象的Class變量
|| fieldName.equals("shadow$_monitor_");//API20以後出現
}
反射一般用在動態將json和Object互相轉化,執行相關底層代碼,比如設置某個類的Accessible爲false,防止別人hook修改
例:阿里的FastJson解析:
@Override public <T> T json2Object(String json, Class<T> clazz) {
return JSON.parseObject(json, clazz);
}
@Override public String object2Json(Object instance) {
return JSON.toJSONString(instance);
}
例Java的默認註解策略:
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
默認,編譯時被拋棄
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
默認被編譯器保解釋,但在運行時拋棄
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
被編譯時解釋,運行時仍保存,可以直接被使用
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
例Hook:
hook一事看似神祕,其實並不是那麼難,希望各位看官看過本文之後能有所收穫。
本次是hook Android的點擊事件,也就是OnClickListener,hook的意義在於你能在調用setOnClickListener後做些其他的事,其他一些你想和所有點擊事件一起處理的事,那麼在這裏,我就以埋點爲例吧。
先來展示下效果:
public void onClick(View view) {
Map map = new HashMap();
switch (view.getId()) {
case R.id.btn_hook1:
map.put("巴", "掌");
map.put("菜", "比");
break;
case R.id.btn_hook2:
map.put("TF-Boys", "嘿嘿嘿");
map.put("id", "111");
break;
}
view.setTag(R.id.id_hook, map);
}
我在onClick內幹了三件事:
1、new HashMap
2、map塞你想埋點的數據
3、把數據傳到對應的view裏
然後點擊按鈕會彈出一個Toast
那麼有意思的地方來了,我們並沒有在點擊事件裏彈Toast,那這個Toast哪來的呢?嘿嘿嘿,當然是hook的啦。
Hook
下面開始hook過程:
整個過程濃縮下來就是四個字--移花接木!
分析源代碼
首先來看看android.view.View中的這塊代碼,mOnClickListener變量靜靜的在這裏(這裏還有別的事件哦,比如OnLongClickListener等,大家學完之後可以試着hook下別的),我們需要做的就是移花接木,把自己的花替換掉這個木,mOnClickListener是ListenerInfo這個類的成員變量,那繼續看看ListenerInfo在View的哪裏被初始化了,因爲我們最開始拿到的只有View這一個對象。
沒錯,找到了,getListenerInfo()幹了這件事,我們從這個方法入手先把ListenerInfo拿下,然後再移花接木。
技術方案已經有了,那麼就開始着手擼碼。
實現
hook的過程就是充分利用java反射機制的過程,幾行代碼搞定,我們來看看:
//先拿下View的Class對象
Class clazzView = Class.forName("android.view.View");
//再把getListenerInfo拿到
Method method = clazzView.getDeclaredMethod("getListenerInfo");
//由於getListenerInfo並不是pulic方法,所以需要修改爲可訪問
method.setAccessible(true);
//繼續拿下ListenerInfo內部類的Class對象
Class clazzInfo = Class.forName("android.view.View$ListenerInfo");
//拿到主角mOnClickListener成員變量
Field field = clazzInfo.getDeclaredField("mOnClickListener");
//截止到這,我們已經完成了百分之95了,只剩最後一步,那就是把我們的木接進來
//那麼這裏先暫時停留下,我們把木給創建好。
//挖個坑 --> 待會填
由於移花接木有個本質不能忘,那就是尊重原有類型,因此,我們的木也得實現View.OnClickListener接口:
public static class HookListener implements View.OnClickListener {
private View.OnClickListener mOriginalListener;
//直接在構造函數中傳進來原來的OnClickListener
public HookListener(View.OnClickListener originalListener) {
mOriginalListener = originalListener;
}
@Override public void onClick(View v) {
if (mOriginalListener != null) {
mOriginalListener.onClick(v);
}
StringBuilder sb = new StringBuilder();
sb.append("hook succeed.\n");
//拿到之前傳遞的參數
Object obj = v.getTag(R.id.id_hook);
//下面的操作可以猥瑣欲爲了
if (obj != null && obj instanceof HashMap && !((Map) obj).isEmpty()) {
for (Map.Entry<String, String> entry : ((Map<String, String>) obj).entrySet()) {
sb.append("key => ")
.append(entry.getKey())
.append(" ")
.append("value => ")
.append(entry.getValue())
.append("\n");
}
} else {
sb.append("params => null\n");
}
Toast.makeText(v.getContext(), sb.toString(), Toast.LENGTH_LONG).show();
}
}
以上代碼就是我們的木,爲了看起來更簡單,我直接通過構造函數把原來的花(OnClickListener)給傳過來了,然後在新的HookListener的onClick()裏把原來的事件繼續完成,並加上自己想猥瑣欲爲的一些事情。
那麼繼續填上之前埋的坑:
field.set(listenerInfo, new HookListener((View.OnClickListener) field.get(listenerInfo)));
接木的過程幹了兩件事,一個是把原有的OnClickListener傳給HookListener,二是把新的HookListener替換進ListenerInfo,perfect。
至此,移花接木就完成了,簡單吧。
合適的調用hook
我們把hook方法都寫好了,最後就是調用你需要hook的View了,在大多數情況下,你可以把hook這件事交給Base去做,遍歷當前rootView所有的View,然後每個都調用hook,本文的重點不是這,我就不贅述了。
小結
本文僅僅以埋點爲例,= = 其實我覺得埋點這個栗子並不太好,妹的都傳了這麼多參數過來了,還在乎在這裏調用一下自己的tracker?不管了,沒有栗子會讓本次hook感覺很無力,希望各位同學看過後能對hook不再懵逼,其實和自定義View一樣簡單的啦。
Sample代碼已同步到github上,有問題可以提issue => https://github.com/JeasonWong/ClickTracker