LayoutInflater hook點
在Activity裏執行setContentView或者inflate佈局文件最終都會走到如下代碼:
LayoutInflater.java
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
try {
View view;
if (mFactory2 != null) {
//1 hook點 mFactory2優先
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
//2 hook點
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
//如果fatory2、factory都返回null則進入函數體
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
//framework.jar裏的View,即安卓原生View控件。 例如name是TextView、Button。 會執行createView(name, "android.view.", attrs); 即添加包名後執行createView函數
view = onCreateView(parent, name, attrs);
} else {
//自定義View( 含support庫裏的View), name是包名+類名。通過反射實例化
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
}
}
通過代碼得到結論, 在inflate時優先使用mFactory2和mFactory實例化, 如果都實例化失敗時執行createView函數實例化View, 而實例化是用類反射的方式實現的,需要完整的包名和類名; 如果是安卓原生控件需要添加android.view前綴。
測試代碼:
public class TestFragmentActivity extends Activity {
private final String TAG = "brycegao/Main";
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
long startTime = System.currentTimeMillis();
View view = null;
try {
//這裏可以“偷樑換柱”, 實例化其它View對象或修改view屬性
view = getLayoutInflater().createView(name, null, attrs);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
long cost = System.currentTimeMillis() - startTime;
Log.d(TAG, "加載佈局:" + name + "耗時:" + cost);
int n = attrs.getAttributeCount();
for (int i = 0; i < n; i++) {
Log.e(TAG, attrs.getAttributeName(i) + " , " + attrs.getAttributeValue(i));
}
//hook控件或屬性
if (view != null && view instanceof Button) {
((Button) view).setText("我是hook替換的標題");
}
return view;
}
@Override public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
setContentView(R.layout.activity_main);
}
回調函數裏有所有xml佈局文件裏定義的屬性。
按鈕的標題變更爲“我是HOOK替換的標題”了, 即在Activity的hook函數裏可以篡改佈局文件裏的View屬性或實例化其它View對象。
HOOK可以統一管理inflate過程, 具體的應用場景包括:
1、 統計View的inflate時間;
2、歸口統一修改View的屬性或者實例化其它View對象, 例如全局替換字體。 可以在基類Activity的onCreate函數裏替換所有TextView的字體。
3、換膚需求。
4、在hook函數使用new方式實例化自定義View, 但JDK8優化了反射性能, 刪除了synchronized同步機制。 參考 https://www.jianshu.com/p/b80ebc5d507c
安卓修改了invoke方法, 直接調用native方法。
@CallerSensitive
// Android-changed: invoke(Object, Object...) implemented natively.
@FastNative
public native Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;