Android插件化(二)動態加載Activity 和生命周期

我們在上篇文章中主要講了加載jar包和加載apk換膚,沒看的可以直接戳著個鏈接:

1.Android插件化(一) 動態加載技術

這篇文章主要講解如何利用動態代理技術Hook掉系統的AMS服務,來實現攔截Activity的啟動流程,這種hook原理方式來自DroidPlugin
。代碼量不是很多,為了更容易的理解,需要掌握JAVA的反射,動態代理技術,以及Activity的啟動流程。

1、尋找Hook點的原則
Android中主要是依靠分析系統源碼類來做到的,首先我們得找到被Hook的對象,我稱之為Hook點;什么樣的對象比較好Hook呢?一般來說,靜態變量和單例變量是相對不容易改變,是一個比較好的hook點,而普通的對象有易變的可能,每個版本都不一樣,處理難度比較大。我們根據這個原則找到所謂的Hook點。

2.尋找Hook點
通常我們啟動一個Activity.這中間發生了什么,我們如何Hook,來實現Activity啟動的攔截呢?

    /**
     * hookActivity(啟動一個無注冊的activity)
     */
    public void hookActivity(View view) {
        Intent intent = new Intent(this, OtherActivity.class);
        intent.putExtra(DLConstants.EXTRA_CLASS, "plugin.charles.com.plugindemo.plugin.OtherActivity");
        intent.putExtra(DLConstants.EXTRA_PACKAGE, "plugin.charles.com.plugindemo");
        startActivity(intent);
    }

我們目的是要攔截startActivity()方法 跟蹤源碼,跟著這個方法一步一步跟蹤,會發現它最后在startActivityForResult里面調用了Instrument對象的execStartActivity方法;其實這個類相當于啟動Activity的中間者,啟動Activity中間都是由它來操作的

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        IApplicationThread whoThread = (IApplicationThread) contextThread;
        ....
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess(who);
            
        //通過ActivityManagerNative.getDefault()獲取一個對象,開始啟動新的Activity
            int result = ActivityManagerNative.getDefault()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
                        
                        
            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
            throw new RuntimeException("Failure from system", e);
        }
        return null;
    }

對于ActivityManagerNative這個東東,熟悉Activity/Service啟動過程的都不陌生

public abstract class ActivityManagerNative extends Binder implements IActivityManager

繼承了Binder,實現了一個IActivityManager接口,這就是為了遠程服務通信做準備的"Stub"類,一個完整的AID L有兩部分,一個是個跟服務端通信的Stub,一個是跟客戶端通信的Proxy。ActivityManagerNative就是Stub,閱讀源碼發現在ActivityManagerNative 文件中還有個ActivityManagerProxy,這里就多不扯了。

static public IActivityManager getDefault() {
    return gDefault.get();
}

ActivityManagerNative.getDefault()獲取的是一個IActivityManager對象,由IActivityManager去啟動Activity,IActivityManager的實現類是ActivityManagerService,ActivityManagerService是在另外一個進程之中,所有Activity 啟動是一個跨進程的通信的過程,所以真正啟動Activity的是通過遠端服務ActivityManagerService來啟動的。

private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
        protected IActivityManager create() {
            IBinder b = ServiceManager.getService("activity");
            if (false) {
                Log.v("ActivityManager", "default service binder = " + b);
            }
            IActivityManager am = asInterface(b);
            if (false) {
                Log.v("ActivityManager", "default service = " + am);
            }
            return am;
        }

其實gDefalut借助Singleton實現的單例模式,而在內部可以看到先從ServiceManager中獲取到AMS遠端服務的Binder對象,然后使用asInterface方法轉化成本地化對象,我們目的是攔截startActivity,所以改變IActivityManager對象可以做到這個一點,這里gDefault又是靜態的,根據Hook原則,這是一個比較好的Hook點。

3.我們來攔截startActivity()方法,并且打印日志

 public class HookUtils {
    private Class<?> proxyActivity;
    private Context context;

    public HookUtils(Class<?> proxyActivity, Context context) {
        this.proxyActivity = proxyActivity;
        this.context = context;
    }

    public void hookAms() {

        //一路反射,直到拿到IActivityManager的對象
        try {
            Class<?> ActivityManagerNativeClss = Class.forName("android.app.ActivityManagerNative");
            Field defaultFiled = ActivityManagerNativeClss.getDeclaredField("gDefault");
            defaultFiled.setAccessible(true);
            Object defaultValue = defaultFiled.get(null);
            //反射SingleTon
            Class<?> SingletonClass = Class.forName("android.util.Singleton");
            Field mInstance = SingletonClass.getDeclaredField("mInstance");
            mInstance.setAccessible(true);
            //到這里已經拿到ActivityManager對象
            Object iActivityManagerObject = mInstance.get(defaultValue);


            //開始動態代理,用代理對象替換掉真實的ActivityManager,瞞天過海
            Class<?> IActivityManagerIntercept = Class.forName("android.app.IActivityManager");

            AmsInvocationHandler handler = new AmsInvocationHandler(iActivityManagerObject);

            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{IActivityManagerIntercept}, handler);

            //現在替換掉這個對象
            mInstance.set(defaultValue, proxy);


        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private class AmsInvocationHandler implements InvocationHandler {

        private Object iActivityManagerObject;

        private AmsInvocationHandler(Object iActivityManagerObject) {
            this.iActivityManagerObject = iActivityManagerObject;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

            Log.i("HookUtil", method.getName());
            if ("startActivity".contains(method.getName())) {
                //Todo
                Log.e("HookUtil", "Activity已經開始啟動");
            }

            return method.invoke(iActivityManagerObject, args);
        }
    }

然后在Application配置下

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        HookUtils hookUtil = new HookUtils(ProxyActivity.class, this);
        hookUtil.hookAms();
    }
}

這里的proxyActivity 可以提前在清單文件中注冊,這個屬于占坑位,下面會講無需注冊啟動Activity,就需要再宿主中先注冊個占坑位的Activity.
最后我們只需要 調用上面第二點里面的hookActivity()方法就可以。
看看執行效果


image.png

可以看到我們已經進行攔截了,并且打印我們想要的日志.

4.無需注冊Activity,并且啟動
如下:我們將下面這個OtherActivity 在清單文件中不注冊了。當我們去點擊跳轉時肯定就會報錯,

"Unable to find explicit activity class "
                + ((Intent)intent).getComponent().toShortString()
                + "; have you declared this activity in your AndroidManifest.xml?");
   /**
    * hookActivity(啟動一個無注冊的activity)
    */
   public void hookActivity(View view) {
       Intent intent = new Intent(this, OtherActivity.class);
       intent.putExtra(DLConstants.EXTRA_CLASS, "plugin.charles.com.plugindemo.plugin.OtherActivity");
       intent.putExtra(DLConstants.EXTRA_PACKAGE, "plugin.charles.com.plugindemo");
       startActivity(intent);
   }

上面已經攔截了啟動Activity流程,在invoke中我們可以得到啟動參數intent信息,那么就在這里,我們可以自己構造一個假的Activity信息的intent,這個Intent啟動的Activity是在清單文件中注冊的,當真正啟動的時候(ActivityManagerService校驗清單文件之后),用真實的Intent把代理的Intent在調換過來,然后啟動即可。

首先獲取真實啟動參數intent信息,在攔截地方添加如下代碼

       @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

            Log.i("HookUtil", method.getName());
            //我要在這里搞點事情
            if ("startActivity".contains(method.getName())) {
                Log.e("HookUtil", "Activity已經開始啟動");

                //替換Intent 先找到原始的intent,然后直接偽造一個Intent 給AMS
                Intent intent = null;
                int index = 0;
                for (int i = 0; i < args.length; i++) {
                    Object arg = args[i];
                    if (arg instanceof Intent) {
                        //說明找到了startActivity的Intent參數
                        intent = (Intent) args[i];
                        //這個意圖是不能被啟動的,因為Acitivity沒有在清單文件中注冊
                        index = i;
                    }
                }

                Intent proxyIntent = new Intent();
                ComponentName componentName = new ComponentName(context, proxyActivity);
                proxyIntent.setComponent(componentName);
                proxyIntent.putExtra(DLConstants.EXTRA_CLASS, intent.getStringExtra(DLConstants.EXTRA_CLASS));
                proxyIntent.putExtra(DLConstants.EXTRA_PACKAGE, intent.getStringExtra(DLConstants.EXTRA_PACKAGE));

                proxyIntent.putExtra("oldIntent", intent);
                args[index] = proxyIntent;
            }

            return method.invoke(iActivityManagerObject, args);
        }

有了上面的兩個步驟,這個代理的Intent是可以通過ActivityManagerService檢驗的,因為我在清單文件中注冊過

     <!-- 替身Activity, 用來欺騙AMS  -->
     <activity android:name=".hook.ProxyActivity"></activity>

為了不啟動ProxyActivity,現在我們需要找一個合適的時機,把真實的Intent換過了來,啟動我們真正想啟動的Activity。看過Activity的啟動流程的朋友,我們都知道這個過程是由Handler發送消息來實現的,可是通過Handler處理消息的代碼來看,消息的分發處理是有順序的,下面是Handler處理消息的代碼:

public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

handler處理消息的時候,首先去檢查是否實現了callback接口,如果有實現的話,那么會直接執行接口方法,然后才是handleMessage方法,最后才是執行重寫的handleMessage方法,我們一般大部分時候都是重寫了handleMessage方法,而ActivityThread主線程用的正是重寫的方法,這種方法的優先級是最低的,我們完全可以實現接口來替換掉系統Handler的處理過程.

這個Handler.Callback是一個接口,我們可以使用動態代理或者普通代理完成Hook,這里我們使用普通的靜態代理方式;創建一個自定義的Callback類:

    public void hookSystemHandler() {
        try {
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
            currentActivityThread.setAccessible(true);
            //獲取主線程對象
            Object activityThread = currentActivityThread.invoke(null);

            Field mH = activityThreadClass.getDeclaredField("mH");
            mH.setAccessible(true);
            Handler handler = (Handler) mH.get(activityThread);
            Field mCallback = Handler.class.getField("mCallback");
            mCallback.setAccessible(true);
            //設置自己實現的CallBack
            mCallback.set(handler, new ActivityThreadHandlerCallback(handler));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

自定義Callback類

public class ActivityThreadHandlerCallback implements Handler.Callback {
    private Handler handler;


    public ActivityThreadHandlerCallback(Handler handler) {
        this.handler = handler;
    }

    @Override
    public boolean handleMessage(Message msg) {
        //代表ActivityThread mH中的launch_activity
        if(msg.what == 100){
            handleLaunchActivity(msg);
        }
        handler.handleMessage(msg);
        return true;
    }

    private void handleLaunchActivity(Message msg) {
        //ActivityClientRecord
        Object obj = msg.obj;
        try {
            Field intentField  = obj.getClass().getDeclaredField("intent");
            intentField .setAccessible(true);
            Intent proxyInent  = (Intent) intentField.get(obj);
            Intent realIntent = proxyInent.getParcelableExtra("oldIntent");
            if (realIntent != null) {
                proxyInent.setComponent(realIntent.getComponent());
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }

}

最后在Application中配置

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        HookUtils hookUtil = new HookUtils(ProxyActivity.class, this);
        hookUtil.hookSystemHandler();
        hookUtil.hookAms();
    }
}

到這里,我們已經成功地繞過AMS,完成了『啟動沒有在AndroidManifest.xml中顯式聲明的Activity』的過程。執行hookActivity()方法就能成功跳入.

能正確收到生命周期回調嗎?雖然我們完成了『啟動沒有在AndroidManifest.xml中顯式聲明的Activity 』 但是啟動的OtherActivity是否有自己的生命周期呢,我們還需要額外的處理過程嗎?

需要額外處理

4.生命周期處理
把一個Activity類加載之后,怎么使插件里的Activity具有生命周期。
這里使用Activity代理模式。老套路,在宿主APK注冊一個ProxyActivity(代理Activity),就是作為占坑使用。每次打開插件APK里的某一個Activity的時候,都是在宿主里使用啟動ProxyActivity,然后在ProxyActivity的生命周期里方法中,調用插件中的Activity實例的生命周期方法,從而執行插件APK的業務邏輯。所以思路就來了。
第一、ProxyActivity中需要保存一個Activity實例,該實例記錄著當前需要調用插件中哪個Activity的生命周期方法。
第二、ProxyActivity如何調用插件apk中Activity的所有生命周期的方法,使用反射呢?還是其他方式。

這里借用dynamic-load-apk

經過優化,改成接口的方式,將activity的生命周期方法封裝一個接口,代理activity中實現一下這個接口,然后還是通過代理activity去調用插件activity實現的生命周期方法.

4.1: 接口類

public interface DLPlugin {

    public void onCreate(Bundle savedInstanceState);
    public void onStart();
    public void onRestart();
    public void onActivityResult(int requestCode, int resultCode, Intent data);
    public void onResume();
    public void onPause();
    public void onStop();
    public void onDestroy();
    public void attach(Activity proxyActivity);
    public void onSaveInstanceState(Bundle outState);
    public void onNewIntent(Intent intent);
    public void onRestoreInstanceState(Bundle savedInstanceState);
    public boolean onTouchEvent(MotionEvent event);
    public boolean onKeyUp(int keyCode, KeyEvent event);
    public void onWindowAttributesChanged(WindowManager.LayoutParams params);
    public void onWindowFocusChanged(boolean hasFocus);
    public void onBackPressed();
    public boolean onCreateOptionsMenu(Menu menu);
    public boolean onOptionsItemSelected(MenuItem item);
}

public interface DLAttachable {
    public void attach(DLPlugin proxyActivity);
}

4.2在代理類ProxyActivity中的實現

public class ProxyActivity extends AppCompatActivity implements DLAttachable {

    private DLProxyImpl impl = new DLProxyImpl(this);
    protected DLPlugin mRemoteActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        impl.onCreate(getIntent());
        Log.e("Charles2","代理的onCreate");
    }

    @Override
    protected void onStart() {
        if(mRemoteActivity != null){
            mRemoteActivity.onStart();
        }
        super.onStart();
        Log.e("Charles2","代理的onStart");
    }

    @Override
    protected void onRestart() {
        if(mRemoteActivity != null){
            mRemoteActivity.onRestart();
        }
        super.onRestart();
        Log.e("Charles2","代理的onRestart");
    }

    @Override
    protected void onResume() {
        if(mRemoteActivity != null){
            mRemoteActivity.onResume();
        }
        super.onResume();
        Log.e("Charles2","代理的onResume");
    }

    @Override
    protected void onPause() {
        if(mRemoteActivity != null){
            mRemoteActivity.onPause();
        }
        super.onPause();
        Log.e("Charles2","代理的onPause");
    }

    @Override
    protected void onDestroy() {
        if(mRemoteActivity != null){
            mRemoteActivity.onDestroy();
        }
        super.onDestroy();
        Log.e("Charles2","代理的onDestroy");
    }

    @Override
    public void attach(DLPlugin proxyActivity) {
        mRemoteActivity = proxyActivity;
    }

}

4.3 代理實現類

public class DLProxyImpl {
    private Activity mProxyActivity;
    private String mPackageName;
    private String mClass;
    protected DLPlugin mPluginActivity;

    public DLProxyImpl(Activity activity) {
        mProxyActivity = activity;
    }

    public void onCreate(Intent intent) {
        if (intent != null) {
            mPackageName = intent.getStringExtra(DLConstants.EXTRA_PACKAGE);
            mClass = intent.getStringExtra(DLConstants.EXTRA_CLASS);
        }
        launchTargetActivity();
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    protected void launchTargetActivity() {
        try {
            Class<?> localClass = mProxyActivity.getClassLoader().loadClass(mClass);
            //Class<?> localClass = getClassLoader().loadClass(mClass);
            Constructor<?> localConstructor = localClass.getConstructor(new Class[]{});
            Object instance = localConstructor.newInstance(new Object[]{});
            mPluginActivity = (DLPlugin) instance;

            ((DLAttachable) mProxyActivity).attach(mPluginActivity);
            // attach the proxy activity and plugin package to the mPluginActivity
            mPluginActivity.attach(mProxyActivity);

            Bundle bundle = new Bundle();
            mPluginActivity.onCreate(bundle);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public ClassLoader getClassLoader() {
        return PluginManager.getInstance(mProxyActivity).createDexClassLoader("");
    }

}

4.4. 最后看看我們的目標Activity-->otherActivity

public class OtherActivity extends DLBasePluginActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.e("Charles2", "plugin--onCreate");
        that.setContentView(R.layout.activity_other);
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.e("Charles2", "plugin--onResume");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.e("Charles2", "plugin--onStart");
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.e("Charles2", "plugin--onPause");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.e("Charles2", "plugin--onDestroy");
    }

}
public class DLBasePluginActivity extends Activity implements DLPlugin {
    /**
     * 代理activity,可以當作Context來使用,會根據需要來決定是否指向this
     */
    protected Activity mProxyActivity;
    /**
     * 等同于mProxyActivity,可以當作Context來使用,會根據需要來決定是否指向this<br/>
     * 可以當作this來使用
     */
    protected Activity that;
    private int type = 0;

    @Override
    public void attach(Activity proxyActivity) {
        mProxyActivity = (Activity) proxyActivity;
        that = mProxyActivity;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        if (type == 1) {
            super.onCreate(savedInstanceState);
        }
    }

    @Override
    public void onStart() {
        if (type == 1) {
            super.onStart();
        }
    }

    @Override
    public void onRestart() {
        if (type == 1) {
            super.onRestart();
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {

    }

    @Override
    public void onResume() {
        if (type == 1) {
            super.onResume();
        }
    }

    @Override
    public void onPause() {
        if (type == 1) {
            super.onPause();
        }
    }

    @Override
    public void onStop() {
        if (type == 1) {
            super.onStop();
        }
    }

    @Override
    public void onDestroy() {
        if (type == 1) {
            super.onDestroy();
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {

    }

    @Override
    public void onNewIntent(Intent intent) {

    }

    @Override
    public void onRestoreInstanceState(Bundle savedInstanceState) {

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return false;
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        return false;
    }

    @Override
    public void onWindowAttributesChanged(WindowManager.LayoutParams params) {

    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {

    }

    @Override
    public void onBackPressed() {

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        return false;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        return false;
    }

}

到這里全部就全部結束了。這里我們來看看效果,是否會走我們的生命周期.


image.png

demo地址:https://github.com/15189611/pluginDemo
參考:https://github.com/singwhatiwanna/dynamic-load-apk

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容