1. 程式人生 > >Android外掛化探索(四)免安裝執行Activity(下)

Android外掛化探索(四)免安裝執行Activity(下)

在上一篇中,我們介紹了兩種免安裝啟動Activity的方法。但是那兩種方法都有缺陷,必須在AndroidManifest.xml中註冊。那麼今天,我們來探索其它幾種不需要在清單檔案中註冊的啟動方式。

靜態代理啟動activity

通過前幾篇的探索我們知道,通過DexClassLoader可以載入類,通過AsserManager可以載入資源。但是Activity確有一個令人苦惱的問題——生命週期。
我們知道宿主中的Activity都有生命週期,那我們可不可以藉助宿主Activity來借屍還魂?

首先我們在宿主中定義一個Activity,取名為ProxyActivity,並且在宿主清單中註冊用來佔坑。這個時候ProxyActivity是有生命週期的,這點毋容置疑。那麼我們現在只需把外掛中的Activity當做一個普通的類反射呼叫即可,既然是一個普通類那麼setContentView,findViewById自然也沒有效果了,所以需要呼叫ProxyActivity的setContentView。也就是說,其實我們每次啟動的Activity是ProxyActivity,佈局也是載入到ProxyActivity,findViewById也是從ProxyActivity中尋找,然後在ProxyActivity的各個生命週期被呼叫的時候反射呼叫外掛中的相應方法。

說的可能有點抽象,還是直接上程式碼吧,ProxyActivity的程式碼如下。

public class ProxyActivity extends Activity {

    public static final String EXTRA_DEX_PATH = "extra_dex_path";
    public static final String EXTRA_ACTIVITY_NAME = "extra_activity_name";

    private String mClass;
    private String mDexPath;

    @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //獲取外掛dex路徑 mDexPath = getIntent().getStringExtra(EXTRA_DEX_PATH); //要啟動的Activity的完整類名 mClass = getIntent().getStringExtra(EXTRA_ACTIVITY_NAME); //載入資源 loadResources(mDexPath); //啟動外掛Activity
performLaunchActivity(savedInstanceState); } protected void performLaunchActivity(Bundle savedInstanceState) { File dexOutputDir = this.getDir("dex", Context.MODE_PRIVATE); //初始化classloader DexClassLoader dexClassLoader = new DexClassLoader(mDexPath, dexOutputDir.getAbsolutePath(), null, ClassLoader.getSystemClassLoader()); //注意:以下只是把外掛中的Activity當作一個普通的類進行反射呼叫 try { Class<?> localClass = dexClassLoader.loadClass(mClass); Constructor<?> localConstructor = localClass .getConstructor(); Object instance = localConstructor.newInstance();//初始化外掛Acitivity物件。 //獲取外掛Activity的setProxy方法,這個方法是我們事先在外掛中約定好的 Method setProxy = localClass.getMethod("setProxy", Activity.class); setProxy.setAccessible(true); //呼叫外掛Activity的setProxy方法 setProxy.invoke(instance, this);//將ProxyActivity物件傳給外掛Activity,用於setContentView等等 //獲取外掛Activity中的onCreate方法。 Method onCreate = localClass.getDeclaredMethod("onCreate", Bundle.class); onCreate.setAccessible(true); //呼叫外掛Activity中的onCreate方法。 onCreate.invoke(instance, savedInstanceState);//將savedInstanceState傳給外掛 } catch (Exception e) { e.printStackTrace(); } } //替換資源。 private AssetManager mAssetManager; private Resources.Theme mTheme; protected void loadResources(String dexPath) { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, dexPath); mAssetManager = assetManager; } catch (Exception e) { e.printStackTrace(); } Resources superRes = super.getResources(); mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration()); mTheme = mResources.newTheme(); mTheme.setTo(super.getTheme()); } private Resources mResources; @Override public AssetManager getAssets() { return mAssetManager == null ? super.getAssets() : mAssetManager; } @Override public Resources getResources() { return mResources == null ? super.getResources() : mResources; } }

然後在外掛中定義一個BaseActivity。讓其他Acitivity實現它即可。

public class BaseActivity extends Activity {

    public static final String EXTRA_DEX_PATH = "extra_dex_path";
    public static final String EXTRA_ACTIVITY_NAME = "extra_activity_name";


    protected Activity that;  //指向外掛Activity



    /**
     * 將代理Activity傳給外掛Activity
     * @param proxyActivity
     */
    public void setProxy(Activity proxyActivity) {
        that = proxyActivity;  
    }  

    @Override  
    protected void onCreate(Bundle savedInstanceState) {
       //由於外掛Activity已經不是真正意義上的Activity了,這裡遮蔽掉super.onCreate。
    }
    //由於外掛Activity已經不是真正意義上的Activity了,只能把佈局給ProxyActivity來顯示
    @Override  
    public void setContentView(int layoutResID) {
        that.setContentView(layoutResID);
    }  
}

接下來,我們的外掛中的Activity就可以這麼寫。

public class TestActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(R.layout.activity_test);
    }


}

宿主中啟動程式碼,如下。

                String path= Environment.getExternalStorageDirectory().getAbsolutePath()+"/2.apk";

                //獲得包管理器
                PackageManager pm = getPackageManager();
                PackageInfo packageInfo=pm.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES);
                String packageName=packageInfo.packageName;

                //啟動Activity
                Intent intent=new Intent(this,ProxyActivity.class);
                intent.putExtra(ProxyActivity.EXTRA_DEX_PATH,path);
                intent.putExtra(ProxyActivity.EXTRA_ACTIVITY_NAME,packageName+".TestActivity");
                startActivity(intent);

經過測試,我們的外掛Activity借屍還魂的被啟動了。當然上面只反射了onCreate方法。為了使外掛Activity具有完整的生命週期,我們還需反射onStart,onResume等等。通常我們會定義一個map來儲存,然後再呼叫即可。

    private HashMap<String,Method> mActivityLifecircleMethods=new HashMap<>();
    protected void instantiateLifecircleMethods(Class<?> localClass) {

        String[] methodNames = new String[] {
                "onRestart",
                "onStart",
                "onResume",
                "onPause",
                "onStop",
                "onDestroy"
        };
        for (String methodName : methodNames) {
            Method method = null;
            try {
                method = localClass.getDeclaredMethod(methodName);
                method.setAccessible(true);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
            mActivityLifecircleMethods.put(methodName, method);
        }
        }

     @Override
   protected void onStart() {
        Method method= mActivityLifecircleMethods.get("onStart");
        if(method!=null){
         try {
            method.invoke(mRemoteActivity);
        } catch (Exception e) {
            e.printStackTrace();
       }
        }

        super.onStart();
    }
    //.....
    //省略了部分原始碼

但是這種方法有個缺陷,由於外掛Activity已經不是真正意義上的Activity了,也就是說原本Activity的findViewById、setContentView和startActivity等等都已經不起作用,只能間接呼叫ProxyActivity的方法。換言之,如果BaseActivity沒有重寫setContentView使其指向ProxyActivity,那麼其子類將不能使用this語法。全部改用that。that.findViewById,that.setContentView,that.startActivity等等。

替換Instrumentation

上面方法有一個缺陷,由於外掛中的Activity已經不是真正意義的Activity,導致其嚴重依賴that語法。雖然說,比起之前的那些方法已經很好用了。但是,有沒有更好用的方法?在介紹其他方法之前,我們來看一下Activity的啟動過程。

startActivity原始碼解讀

startActivity開始。

    @Override
    public void startActivity(Intent intent) {
        this.startActivity(intent, null);
    }

       @Override
    public void startActivity(Intent intent, @Nullable Bundle options) {
        if (options != null) {
            startActivityForResult(intent, -1, options);
        } else {
            // Note we want to go through this call for compatibility with
            // applications that may have overridden the method.
            startActivityForResult(intent, -1);
        }
    }

最終會走startActivityForResult方法。

    public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {
        if (mParent == null) {
            //Instrumentation執行啟動Activity
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);

            if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }
            if (requestCode >= 0) {
                mStartedActivity = true;
            }

            cancelInputsAndStartExitTransition(options);

        } else {
            //內部也是呼叫Instrumentation的execStartActivity
            if (options != null) {
                mParent.startActivityFromChild(this, intent, requestCode, options);
            } else {

                mParent.startActivityFromChild(this, intent, requestCode);
            }
        }
    }

可以看出,startActivityForResult內部呼叫了Instrumentation的execStartActivity方法。execStartActivity如下。

    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();
            //呼叫ActivityManagerNative的startActivity
            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,getDefault().startActivity,ActivityManagerNative是一個Binder物件,連線著ActivityManagerService。而在ActivityManagerService中最終會呼叫ActivityStackSupervisor中的startActivityMayWait方法。ActivityStackSupervisor是一個Activity棧管家,其作用不言而喻,即是用來管理Activity的棧的。

 final int startActivityMayWait(IApplicationThread caller, int callingUid,
            String callingPackage, Intent intent, String resolvedType,
            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
            IBinder resultTo, String resultWho, int requestCode, int startFlags,
            ProfilerInfo profilerInfo, WaitResult outResult, Configuration config,
            Bundle options, boolean ignoreTargetSecurity, int userId,
            IActivityContainer iContainer, TaskRecord inTask) {

            //..
            //省略了部分原始碼
     int res = startActivityLocked(caller, intent, resolvedType, aInfo,
                    voiceSession, voiceInteractor, resultTo, resultWho,
                    requestCode, callingPid, callingUid, callingPackage,
                    realCallingPid, realCallingUid, startFlags, options, ignoreTargetSecurity,
                    componentSpecified, null, container, inTask);
             //..
            //省略了部分原始碼
        }

startActivityMayWait方法內部又呼叫了startActivityLocked,總之經歷了一系列的許可權驗證和棧管理,最終呼叫realStartActivityLocked方法。

   final boolean realStartActivityLocked(ActivityRecord r,
            ProcessRecord app, boolean andResume, boolean checkConfig)
            throws RemoteException {

            //..
            //省略了部分原始碼
            //呼叫ApplicationThread.scheduleLaunchActivity
            app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
                    System.identityHashCode(r), r.info, new Configuration(mService.mConfiguration),
                    new Configuration(stack.mOverrideConfig), r.compat, r.launchedFromPackage,
                    task.voiceInteractor, app.repProcState, r.icicle, r.persistentState, results,
                    newIntents, !andResume, mService.isNextTransitionForward(), profilerInfo);

            //..
            //省略了部分原始碼
        return true;
    }

從上面可以看出會呼叫app.thread.scheduleLaunchActivity,那麼app.thread是什麼呢?其實是一個客戶端Binder物件,即ApplicationThread。是在mInstrumentation.execStartActivity中傳遞過去的,不記得回頭看一下原始碼。ApplicationThread的相關原始碼如下。

        @Override
   public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
                ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
                CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
                int procState, Bundle state, PersistableBundle persistentState,
                List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
                boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {

            updateProcessState(procState, false);

            ActivityClientRecord r = new ActivityClientRecord();

            r.token = token;
            r.ident = ident;
            r.intent = intent;
            r.referrer = referrer;
            r.voiceInteractor = voiceInteractor;
            r.activityInfo = info;
            r.compatInfo = compatInfo;
            r.state = state;
            r.persistentState = persistentState;

            r.pendingResults = pendingResults;
            r.pendingIntents = pendingNewIntents;

            r.startsNotResumed = notResumed;
            r.isForward = isForward;

            r.profilerInfo = profilerInfo;

            r.overrideConfig = overrideConfig;
            updatePendingConfiguration(curConfig);
            //Handler傳送訊息
            sendMessage(H.LAUNCH_ACTIVITY, r);
        }

scheduleLaunchActivity中將相關資訊包裝到ActivityClientRecord然後傳到了Handler中,Handler中相關原始碼如下。

 public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    //處理啟動Activity
                    handleLaunchActivity(r, null);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } break;
      //..
      //省略了部分原始碼

處理啟動相關的程式碼在handleLaunchActivity中。


    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
          //..
          //省略了部分原始碼

        // Make sure we are running with the most recent config.
        handleConfigurationChanged(null, null);

        WindowManagerGlobal.initialize();

        //啟動Activity
        Activity a = performLaunchActivity(r, customIntent);

        if (a != null) {
            r.createdConfig = new Configuration(mConfiguration);
            Bundle oldState = r.state;
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed);

            if (!r.activity.mFinished && r.startsNotResumed) {

                try {
                    r.activity.mCalled = false;
                    mInstrumentation.callActivityOnPause(r.activity);

                    if (r.isPreHoneycomb()) {
                        r.state = oldState;
                    }
                    if (!r.activity.mCalled) {
                        throw new SuperNotCalledException(
                            "Activity " + r.intent.getComponent().toShortString() +
                            " did not call through to super.onPause()");
                    }

                } catch (SuperNotCalledException e) {
                    throw e;

                } catch (Exception e) {
                    if (!mInstrumentation.onException(r.activity, e)) {
                        throw new RuntimeException(
                                "Unable to pause activity "
                                + r.intent.getComponent().toShortString()
                                + ": " + e.toString(), e);
                    }
                }
                r.paused = true;
            }
        } else {

            try {
                ActivityManagerNative.getDefault()
                    .finishActivity(r.token, Activity.RESULT_CANCELED, null, false);
            } catch (RemoteException ex) {

            }
        }
    }

而真正執行啟動Activity的程式碼在performLaunchActivity中。

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {


        //..
        //省略了部分原始碼

        Activity activity = null;
        try {
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            //初始化Activity物件(傳入classloader,類名,intent)。
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }

        try {
            //如果Application未初始化就先初始化Application
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);


            if (activity != null) {
                 //初始化ContextImpl
                Context appContext = createBaseContextForActivity(r, activity);
                //初始化標題
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);

                if (customIntent != null) {
                    activity.mIntent = customIntent;
                }
                r.lastNonConfigurationInstances = null;
                activity.mStartedActivity = false;
                int theme = r.activityInfo.getThemeResource();
                if (theme != 0) {
                    activity.setTheme(theme);//設定主題
                }

                activity.mCalled = false;
                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    //呼叫onCreate
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }
                if (!activity.mCalled) {
                    throw new SuperNotCalledException(
                        "Activity " + r.intent.getComponent().toShortString() +
                        " did not call through to super.onCreate()");
                }
              //..
              //省略了部分原始碼

        return activity;
    }

程式碼有點長。核心的地方在這裡mInstrumentation.newActivity,通過Instrumentation來例項化一個Activity。
其實Activity的啟動流程我們可以簡化一下。

  1. Activity中執行startActivity
  2. Instrumentation執行execStartActivity
  3. AMS進行一系列的許可權驗證和棧管理。
  4. Instrumentation執行newActivity,例項化Activity。

而且很容易能看出來,啟動外掛Activity第3步無法通過。
那麼現在該怎麼辦?既然第3步實在瞞不過去,也就是說必須想辦法讓第3步中的Activity通過驗證之後,我們才能動歪腦筋。
既然這樣,我們的思路跟ProxyActivity是一樣的,搞一個佔坑的Activity。要想啟動外掛中的Activity,最後一定要想辦法把它替換成外掛Activity。也就是在最後一步動手腳,在Instrumentation執行newActivity時替換成外掛Activity。

想法很美好。怎麼實現呢?

實現方式

還記得我們在上一篇中替換ClassLoader和合並DexElement嗎?那我們就故技重施,替換掉Instrumentation,這樣我們就能為所欲為了,想想就有點小激動。修改方法如下。

    private void hookInstrumentation(String path){
        try {

            File codeDir=getDir("dex", Context.MODE_PRIVATE);
            //建立類載入器,把dex載入到虛擬機器中
            ClassLoader classLoader = new DexClassLoader(path,codeDir.getAbsolutePath() ,null,
                    this.getClass().getClassLoader());



            //獲取ActivityThread的Class
            Class<?> activityThreadCls = Class.forName("android.app.ActivityThread");
            //獲取ActivityThread物件
            Method currentActivityThreadMethod=activityThreadCls.getMethod("currentActivityThread");
            Object currentActivityThread= currentActivityThreadMethod.invoke(null);

            // 反射獲取Instrumentation
            Field mInstrumentationField = activityThreadCls.getDeclaredField("mInstrumentation");
            mInstrumentationField.setAccessible(true);
            //  Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);

            //反射修改Instrumentation

            Instrumentation hookInstrumentation = new HookInstrumentation(classLoader);
            mInstrumentationField.set(currentActivityThread, hookInstrumentation);
        }catch (Exception e){
            e.printStackTrace();
        }

    }

可以看出我們替換成了自己需要的HookInstrumentation,HookInstrumentation的程式碼如下。

public class HookInstrumentation extends Instrumentation {
    private ClassLoader mClassLoader;
    public HookInstrumentation(ClassLoader classLoader){
        this.mClassLoader=classLoader;
    }
    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {

              String cls=intent.getStringExtra(HookUtil.EXTRA_ACTIVITY_NAME);
             if(cls!=null){

              cl=mClassLoader;//替換Classloader
              className = cls//替換className

             }


        return super.newActivity(cl, className, intent);
    }
}

宿主中的啟動程式碼如下。

 String path= Environment.getExternalStorageDirectory().getAbsolutePath()+"/2.apk";
                //修改Instrumentation
                hookInstrumentation( path);
                //啟動Activity
                Intent intent=new Intent(this,ProxyActivity.class);
                intent.putExtra(HookUtil.EXTRA_ACTIVITY_NAME,"com.maplejaw.hotplugin.PluginActivity");
                startActivity(intent1);

測試通過。但是這個方法跟上篇同樣存在資源載入問題。上篇我們用了反射修改LoadedApk中的資源目錄。但是那種方法的弊端我們也提過了。所以這裡換種思路。在外掛的Activity中加入loadResources即可,一個Activity一個Context。那我們就修改所有外掛Activity的資源指向目錄。

    @Override  
    protected void onCreate(Bundle savedInstanceState) {
        String path= Environment.getExternalStorageDirectory().getAbsolutePath()+"/2.apk";
        loadResources(path);
        super.onCreate(savedInstanceState);
    }

當然實際應用中我們不可能每次都去讀SD卡,用map之類的儲存即可。
經測試,成功啟動Activity,當然為了支援外掛內部的Activity跳轉,我們還需反射修改execStartActivity方法。

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {

        //如果是啟動外掛,則修改intent
        wrapIntent(who, intent);

        try {
            // 由於這個方法是隱藏的,因此需要使用反射呼叫;首先找到這個方法
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity", Context.class, IBinder.class, IBinder.class,
                    Activity.class, Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            return (ActivityResult) execStartActivity.invoke(mBase, who,
                    contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("do not support!!!" + e.getMessage());
        }
    }

最後

關於免安裝啟動Activity的方法探索完了。
其中dynamic-load-apk使用了ProxyActivity這種方式,Small使用了修改Instrumentation方式。