參考1:http://www.lxweimin.com/p/f2e5b7b7f72b
參考2(Android動態加載Activity原理):https://blog.csdn.net/cauchyweierstrass/article/details/51087198
參考3:(Android類加載之PathClassLoader和DexClassLoader)http://www.lxweimin.com/p/4b4f1fa6633c
1. 原理:
坑位的概念是指在AndroidManifest中注冊,但并沒有真正的實現類,只作為其他Activity啟動的坑位。
Hook點為ClassLoader,Android中的ClassLoader有兩個,分別為DexClassLoader和PathClassLoader,用于加載APK的是PathClassLoader,他們的區別是:
DexClassLoader:能夠加載自定義的jar/apk/dex
PathClassLoader:只能加載系統中已經安裝過的apk
所以Android系統默認的類加載器為PathClassLoader,這個也就是需要Hook的地方,而DexClassLoader可以像JVM的ClassLoader一樣提供動態加載。
2. 預熱知識
這里需要有關于ClassLoader和Activity啟動的知識:
在啟動一個新的Activity的時候,AMS會對其進行很多檢測,例如是否在AndroidManifest中注冊,是否有權限啟動等等。如果這些都通過,那么需要判斷當前的進程是否存在,不存在需要先調用ActivityThread.main()方法,開啟線程循環以及啟動Application。最終會通過ActivityThread的Handler發送一條為“BIND_APPLICATION”的消息,通過這個消息,Handler來處理這次Application的創建過程。這里會創建Application、LoadedApk等。
- LoadedApk對象是APK文件在內存中的表示,APl文件的相關信息,諸如Apk文件的代碼和資源,甚至代碼里面的Activity,Service等組件的信息我們都可以通過此對象獲取。注意:這里會創建一個ClassLoader作為類加載器,也就是我們需要Hook的。
LoadedApk.java
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader == null) {
createOrUpdateClassLoaderLocked(null /*addedPaths*/);
}
return mClassLoader;
}
}
- Activity的創建就是通過反射創建的,使用的就是上面提到的ClassLoader,所以我們只需要Hook住這個ClassLoader,通過類的雙親委派機制來實現我們自己的邏輯即可。
Activity啟動過程源碼分析如下:(ActivityThread發送一條“LAUNCH_ACTIVITY”的消息給對應的Handler,在處理LAUNCH_ACTIVITY的消息類型處執行handleLaunchAvtivity方法,在handlerLaunchActivity中又執行了PerformLaunchActivity()來完成Activity對象的創建和啟動過程。)
PerformLaunchActivity這個方法主要完成了五件事
- 從ActivityClientRecord中獲取待啟動的Activity的組件信息
- 通過Instrumentation的newActivity方法使用類加載器創建Activity對象
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;
// 1.創建ActivityClientRecord對象時沒有對他的packageInfo賦值,所以它是null
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo, Context.CONTEXT_INCLUDE_CODE);
}
// ...
Activity activity = null;
try {
// 2.非常重要??!這個ClassLoader保存于LoadedApk對象中,它是用來加載我們寫的activity的加載器
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
// 3.用加載器來加載activity類,這個會根據不同的intent加載匹配的activity
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
// 4.這里的異常也是非常非常重要的?。?!后面就根據這個提示找到突破口。。。
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
if (activity != null) {
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
// 從這里就會執行到我們通??吹降腶ctivity的生命周期的onCreate里面
mInstrumentation.callActivityOnCreate(activity, r.state);
// 省略的是根據不同的狀態執行生命周期
}
r.paused = true;
mActivities.put(r.token, r);
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
// ...
}
return activity;
}
newActivity的實現也比較簡單,就是通過類加載器來創建Activity對象
public Activity newActivity(ClassLoader cl, String className,Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
這里留意這個ClassLoader的loadClass方法,在后面hook填坑的時候起到關鍵作用
- 通過LoadedApk的makeApplication方法來嘗試創建Application,這里不貼源碼了,如果Application已經被創建過了,就不會重復創建,這就意味著一個應用只有一個Application對象,Application對象的創建也是通過Instrumentation來完成的,這個過程和Activity的創建一樣,都是通過類加載器來實現的,Application創建完畢后,系統會通過Instrumentation的callApplicationOnCreate來調用Application的onCreate方法。
- 創建ContextImpl對象通過Activity的attach方法來完成一些重要數據的初始化。
- 調用Activity的onCreate方法。
上面五個步驟沒貼源碼的步驟不是hook中的關鍵所以沒貼源碼,詳情請拜讀《Android開發藝術與探索》的p332
- 調用Activity的onCreate方法。
3. Hook代碼實現
- 創建HookUtils
public class HookUtils {
public static final String TAG="HookUtils";
public static void hookClassLoader(Application context) {
try {
// 獲取Application類的mLoadedApk屬性值
Object mLoadedApk = getFieldValue(context.getClass().getSuperclass(), context, "mLoadedApk");
if (mLoadedApk != null) {
// 獲取其mClassLoader屬性值以及屬性字段
final ClassLoader mClassLoader = (ClassLoader) getFieldValue(mLoadedApk.getClass(), mLoadedApk, "mClassLoader");
if (mClassLoader != null) {
Field mClassLoaderField = getField(mLoadedApk.getClass(), "mClassLoader");
// 替換成自己的ClassLoader
mClassLoaderField.set(mLoadedApk, new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 替換Activity
if (name.endsWith("MainActivity2")) {
Log.d(TAG, "loadClass: name = " + name);
name = name.replace("MainActivity2", "MainActivity3");
Log.d(TAG, "loadClass: 替換后name = " + name);
}
return mClassLoader.loadClass(name);
}
});
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 反射獲取屬性值
*
* @param c class
* @param o 對象
* @param fieldName 屬性名稱
* @return 值
* @throws NoSuchFieldException e
* @throws IllegalAccessException e
*/
public static Object getFieldValue(Class c, Object o, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = getField(c, fieldName);
if (field != null) {
return field.get(o);//返回指定對象上此 Field 表示的字段的值。
} else {
return null;
}
}
/**
* 反射獲取對象屬性
*
* @param aClass c
* @param fieldName 屬性名稱
* @return 屬性
* @throws NoSuchFieldException e
*/
private static Field getField(Class<?> aClass, String fieldName) throws NoSuchFieldException {
Field field = aClass.getDeclaredField(fieldName);
if (field != null) {
field.setAccessible(true);
}
return field;
}
}
這里主要是從Application中拿到mLoadedApk屬性的值,然后再通過反射獲取其mClassLoader屬性值,然后將mLoadedApk中的ClassLoader替換自定義的ClassLoader。因為Activity在啟動的時候要走下面這個方法:
public Activity newActivity(ClassLoader cl, String className,Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
所以我們使用下面自己定義的ClassLoader可以攔截要啟動的Activity替換成其他我們想要啟動的Activity,這就是填坑。一般這里對應的MainActivity2是個空白的Activity(只在清單文件里面注冊了,并沒有真正的實現類)。
mClassLoaderField.set(mLoadedApk, new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 替換Activity
if (name.endsWith("MainActivity2")) {
Log.d(TAG, "loadClass: name = " + name);
name = name.replace("MainActivity2", "MainActivity3");
Log.d(TAG, "loadClass: 替換后name = " + name);
}
return mClassLoader.loadClass(name);
}
});
4. 測試
- 在Application中進行初始化
public class MyApplication extends Application{
@Override
public void onCreate() {
super.onCreate();
HookUtils.hookClassLoader(this);
}
}
- 設置坑位,在AndroidManifest注冊一個不存在的Activity
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.houyl.hookdemo">
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".MainActivity2"/>
</application>
</manifest>
- 啟動Activity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void start(View view) {
Intent intent=new Intent();
ComponentName name=new ComponentName("com.example.houyl.hookdemo","com.example.houyl.hookdemo.MainActivity2");
intent.setComponent(name);
startActivity(intent);
// startActivity(new Intent(this,MainActivity2.class));
}
}
因為我們在前面已經將啟動Activity過程中的ClassLoader替換成了自定義的ClassLoader,啟動一個Activity的時候會走我們自定義的ClassLoader。
- 創建MainActivity3
運行結果:
可以看到,通過這種方式實現了不在AndroidManifest中注冊,但是可以啟動Activity的效果。這里可以應用到插件化中,如Replugin,編譯時自動注入坑位,運行時進行確定坑位。