Androidプラグイン化探索(四)インストールフリー運転Activity(下)


転載は本文のmaplejawからのブログを明記してください。http://blog.csdn.net/maplejaw_)【Androidプラグイン化探索(一)類キャリアDexClass Loader】【Androidプラグイン化探索(二)リソースローディング】【Androidプラグイン化探索(三)インストールフリー運転Activity(上)】
前編では、インストールなしでActivityを起動する2つの方法を紹介しました。しかし、どちらの方法にも欠陥があります。Android Manifest.xmlに登録しなければなりません。今日は他のいくつかのリストファイルに登録する必要がない起動方法を探しています。
静的代理起動activity
前の数編の探査により、DexClass Loaderにクラスをロードでき、AserManagerによってリソースをロードすることができることを知っています。しかし、Activityには、ライフサイクルという悩みがあります。私達は宿主の中のActivityにライフサイクルがあると知っていますが、宿主Activityを借りて復活してもいいですか?
まず、私たちは宿主の中でActivityを定義し、ProxyActivityと名づけて、宿主リストにピットを占うために登録します。この時ProxyActivityはライフサイクルがあります。これは間違いないです。今はプラグインの中のActivityを普通のクラスの反射として呼び出せばいいです。普通のクラスなら、setContentView、findView ByIdも当然効果がないので、ProxyActivityのsetContantViewを呼び出す必要があります。つまり、私達が毎回起動するActivityはProxyActivityであり、レイアウトもProxyActivityにロードされています。findView ByIdも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が復活させられました。もちろん上はオンクリアー方法だけを反射しました。プラグイン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のfindView ById、set ContentView、startActivityなどが機能しなくなり、間接的にProxyActivityを呼び出す方法しかないという欠点があります。つまり、BaseActivityがset ContentViewを書き換えてProxyActivityにさせないと、そのサブクラスはthis文法を使用できなくなります。全部thatに変えます。that.findView ById、that.set ContentView、that.startActivityなど。
Instructionを置換する
上記の方法は、プラグインの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内部でInstructionの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が呼び出され、ActivityManager NativeはBinderオブジェクトであり、ActivityManagerServiceに接続されています。ActivityManagerServiceでは最終的にActivity StockSupervisorのstartActivity MayWaitメソッドを呼び出します。ActivityStockSupervisorは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);
             //..
            //       
        }
startActivity MayWaitメソッドの内部はまた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オブジェクトです。つまりAppplication Threadです。mInstrumentation.execStartActivityで渡されたのです。ソースを振り返るのは覚えていません。Application Threadのソースコードは以下の通りです。
        @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です。Instructivityを通じて一つのActivityを実装します。実はActivityの起動フローは簡単にできます。
  • ActivityでstartActivity
  • を実行します。
  • Instruction実行execStartActivity
  • AMSは一連の権限検証とスタック管理を行う。
  • InstructivityはnewActivityを実行し、Activityを実行します。
  • また、簡単に見られます。プラグインActivityを起動してから3ステップ目は通過できません。今はどうすればいいですか?第3ステップはどうしても隠しきれないということは、第3ステップのActivityを検証してからこそ、頭をひねることができるということです。それなら、私達の考えはProxyActivityと同じです。ピットを占めるActivityをやります。プラグインのActivityを起動するには、最後に必ずプラグインActivityに置き換えます。つまり、最後のステップで足を動かして、newActivityをInstructivity実行時にプラグインActivityに置き換えます。
    アイデアがいいです。どうやって実現しますか
    実装
    前の編でClass LoaderとDexElementを交替したことを覚えていますか?私たちは古い技術を使って、インストラムメントを交替します。そうすると、私たちはやりたい放題になります。ちょっと興奮します。修正方法は以下の通りです。
        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();
            }
    
        }
    私たちが必要なHookInstructionに置き換えられたことが分かります。HookInstructionのコードは以下の通りです。
    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);
    テストに合格しました。しかし、この方法は前の記事と同じようにリソースローディングの問題があります。上記の記事では、反射的にLoadedAkのリソースディレクトリを修正しました。しかし、その方法の弊害は私たちも指摘しました。だからここは考え方を変えます。プラグインのActivityにloadResourceを追加すればいいです。Activity一つのContect。すべてのプラグイン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-appkはProxyActivityという方式を使用しており、SmallはInstructionを修正する方式を使用している。
    本文のソースコードの住所:https://github.com/maplejaw/HotPluginDemo