滴滴開源Android插件框架



[置頂] 滴滴開源Android插件化框架VirtualAPK原理分析

標籤: VirtualAPKAndroid插件化滴滴代理
390人閱讀 評論(0) 收藏 舉報
分類:

概述

滴滴出行公司的首個對外開源項目 - VirtualAPK。地址:https://github.com/didi/VirtualAPK

滴滴自行研發了這款插件化框架,功能全面、兼容性好,還能夠適用於有耦合的業務插件,這就是VirtualAPK存在的意義。業內認爲,在加載耦合插件方面,VirtualAPK可以說是開源方案的首選。據說滴滴打車裏面已經用上了,所以還是有必要一探究竟的~~

VirtualAPK 的工作流程如圖所示:

這裏寫圖片描述

VirtualAPK 對插件沒有額外的約束,原生的 apk 即可作爲插件。插件工程編譯生成 apk 後,即可通過宿主 App 加載,每個插件 apk 被加載後,都會在宿主中創建一個單獨的 LoadedPlugin 對象。如上圖所示,通過這些 LoadedPlugin 對象,VirtualAPK 就可以管理插件並賦予插件新的意義,使其可以像手機中安裝過的App一樣運行。

Activity 支持

Hook ActivityManagerService

插件化支持首先要解決的一點就是插件裏的Activity並未在宿主程序的 AndroidMainfest.xml 註冊,常規方法肯定無法直接啓動插件的Activity,這個時候就需要去了解Activity的啓動流程,關於啓動過程主要的幾個步驟請參考:淺析Android Activity的啓動過程

從上文中可知,Activity 啓動實際上是調用了 Instrumentation.execStartActivity 這個方法。源碼如下:

public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,  
            Intent intent, int requestCode, Bundle options) {  
    IApplicationThread whoThread = (IApplicationThread) contextThread;  
    if (mActivityMonitors != null) {  
        synchronized (mSync) {  
            final int N = mActivityMonitors.size();  
            for (int i=0; i<N; i++) { //先查找一遍看是否存在這個activity  
                final ActivityMonitor am = mActivityMonitors.get(i);  
                if (am.match(who, null, intent)) {  
                    am.mHits++;  
                    if (am.isBlocking()) {  
                        return requestCode >= 0 ? am.getResult() : null;  
                    }  
                    break;  
                }  
            }  
        }  
    }  
    try {  
        intent.migrateExtraStreamToClipData();  
        intent.prepareToLeaveProcess();  
        //這裏纔是真正打開activity的地方,其核心功能在whoThread中完成。  
        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); // 處理各種異常,如ActivityNotFound  
    } catch (RemoteException e) {  
    }  
    return null;  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

可見, startActivity 最終通過 ActivityManagerNative.getDefault() 遠程調用了AMS的startActivity方法, ActivityManagerNative 實際上就是 ActivityManagerService 這個遠程對象的 Binder 代理對象,每次需要與AMS交互時,需要通過這個 Binder 對象完成遠程IPC調用。

還不瞭解Binder的童鞋,可以看看老羅的Android進程間通信(IPC)機制Binder簡要介紹和學習計劃


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

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;
           }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

從這我們可以知道,ActivityManagerNative.getDefault() 實際上是返回了一個 IActivityManager 的單例對象。

那麼,VirtualApk 所要做的第一件事,就是把這個 AMS 代理對象保存起來。首先,我們可以看一下 VirtualApk 核心庫裏面 com.didi.virtualapk.PluginManager 這個類的初始化:


// 構造方法
private PluginManager(Context context) {
    Context app = context.getApplicationContext();
    if (app == null) {
        this.mContext = context;
    } else {
        this.mContext = ((Application)app).getBaseContext();
    }
    prepare();
}

// 初始化
private void prepare() {
    Systems.sHostContext = getHostContext();
    this.hookInstrumentationAndHandler();
    this.hookSystemServices();
}

/**
 * Hook 出一個IActivityManager,也就是 AMS 的代理對象
 */
private void hookSystemServices() {
    try {
        // 反射調用 ActivityManagerNative.getDefault(),實際上這在6.0中是公開的靜態方法,反射可能是考慮到版本兼容性吧?
        Singleton<IActivityManager> defaultSingleton = (Singleton<IActivityManager>) ReflectUtil.getField(ActivityManagerNative.class, null, "gDefault");
        // 通過動態代理的方式去創建代理對象,之後所有ActivityManagerNative中的方法被調用的時候都會經過這個代理
        IActivityManager activityManagerProxy = ActivityManagerProxy.newInstance(this, defaultSingleton.get());

        // Hook IActivityManager from ActivityManagerNative,實際上就是把 ActivityManagerNative 替換爲剛創建的 activityManagerProxy
        ReflectUtil.setField(defaultSingleton.getClass().getSuperclass(), defaultSingleton, "mInstance", activityManagerProxy);

        if (defaultSingleton.get() == activityManagerProxy) {
            // 兩者一樣,保存下來
            this.mActivityManager = activityManagerProxy;
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

實際上除了 startActivity 是調用 AMS 的方法以外,startService bindService 等方法,最終調用到AMS的裏的方法,這個我們在動態代理類 com.didi.virtualapk.delegate 也可以找到:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if ("startService".equals(method.getName())) {
        try {
            return startService(proxy, method, args);
        } catch (Throwable e) {
            Log.e(TAG, "Start service error", e);
        }
    } else if ("stopService".equals(method.getName())) {
        try {
            return stopService(proxy, method, args);
        } catch (Throwable e) {
            Log.e(TAG, "Stop Service error", e);
        }
    } else if ("stopServiceToken".equals(method.getName())) {
        try {
            return stopServiceToken(proxy, method, args);
        } catch (Throwable e) {
            Log.e(TAG, "Stop service token error", e);
        }
    } else if ("bindService".equals(method.getName())) {
        try {
            return bindService(proxy, method, args);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    } else if ("unbindService".equals(method.getName())) {
        try {
            return unbindService(proxy, method, args);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    } else if ("getIntentSender".equals(method.getName())) {
        try {
            getIntentSender(method, args);
        } catch (Exception e) {
            e.printStackTrace();
        }
    } else if ("overridePendingTransition".equals(method.getName())){
        try {
            overridePendingTransition(method, args);
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    try {
        // sometimes system binder has problems.
        return method.invoke(this.mActivityManager, args);
    } catch (Throwable th) {
        Throwable c = th.getCause();
        if (c != null && c instanceof DeadObjectException) {
            // retry connect to system binder
            IBinder ams = ServiceManager.getService(Context.ACTIVITY_SERVICE);
            if (ams != null) {
                IActivityManager am = ActivityManagerNative.asInterface(ams);
                mActivityManager = am;
            }
        }

        Throwable cause = th;
        do {
            if (cause instanceof RemoteException) {
                throw cause;
            }
        } while ((cause = cause.getCause()) != null);

        throw c != null ? c : th;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

所以實際上就等同於我們重寫了一些 ActivityService 的相關操作。

Hook Instrumentation

回過頭去看看 Instrumentation.execStartActivity 這個方法,在最後有這麼一句代碼:

checkStartActivityResult(result, intent); // 處理各種異常,如ActivityNotFound
  • 1
  • 1
static void checkStartActivityResult(int res, Object intent) {  
    if (res >= ActivityManager.START_SUCCESS) {  
        return;  
    }  

    switch (res) {  
        case ActivityManager.START_INTENT_NOT_RESOLVED:  
        case ActivityManager.START_CLASS_NOT_FOUND:  
            if (intent instanceof Intent && ((Intent)intent).getComponent() != null)  
                throw new ActivityNotFoundException(  
                        "Unable to find explicit activity class "  
                        + ((Intent)intent).getComponent().toShortString()  
                        + "; have you declared this activity in your AndroidManifest.xml?");  
            throw new ActivityNotFoundException(  
                    "No Activity found to handle " + intent);  
        case ActivityManager.START_PERMISSION_DENIED:  
            throw new SecurityException("Not allowed to start activity "  
                    + intent);  
        case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:  
            throw new AndroidRuntimeException(  
                    "FORWARD_RESULT_FLAG used while also requesting a result");  
        case ActivityManager.START_NOT_ACTIVITY:  
            throw new IllegalArgumentException(  
                    "PendingIntent is not an activity");  
        default:  
            throw new AndroidRuntimeException("Unknown error code "  
                    + res + " when starting " + intent);  
    }  
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

相信大家對上面的這些異常信息不陌生吧,其中最熟悉的非 Unable to find explicit activity class 莫屬了,如果 Activity 沒有在 AndroidMainfest.xml 註冊,此異常將會拋出。

那麼就得思考一個問題了,插件的 Activity 並未在宿主程序的 AndroidMainfest.xml 註冊,要如何才能繞過這一層檢測?

前文中提到,com.didi.virtualapk.PluginManager 這個類的初始化的時候,除了 Hook 出一個 AMS 代理對象以外,還 Hook 出一個 Instrumentation 對象。代碼如下:

private void hookInstrumentationAndHandler() {
    try {
        Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
        if (baseInstrumentation.getClass().getName().contains("lbe")) {
            // reject executing in paralell space, for example, lbe.
            System.exit(0);
        }

        // 創建自定義的 instrumentation,重寫了 newActivity() 等一些方法
        // baseInstrumentation 後面還會用到,也保存下來
        final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);

        // 獲取 ActivityThread 的實例
        Object activityThread = ReflectUtil.getActivityThread(this.mContext);

        // 用自定義的 instrumentation 替換掉 ActivityThread 裏面的 instrumentation
        ReflectUtil.setInstrumentation(activityThread, instrumentation);
        ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
        this.mInstrumentation = instrumentation;
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

既然 Activity 的啓動,最後走了 Instrumentation.execStartActivity 這個方法,那麼我們大概可以知道,Hook 出一個 Instrumentation 對象用來做什麼了,實際上就是用來幫助啓動插件的 Activity

啓動插件Activity

我們 Hook 了一個 VAInstrumentation 已替代系統的 Instrumentation,這樣當系統通過 ActivityThread 調用 它的的成員變量 mInstrumentation 的 newActivity() 等方法的時候,實際是調用我們 VAInstrumentationnewActivity()

實際上對於插件 Activity 啓動,採用的是宿主 manifest 中佔坑的方式來繞過系統校驗,然後再加載真正的activity。

什麼是佔坑?就是構造一系列假的 Activity 替身,在 AndroidMainfest.xml 裏面進行註冊,以繞過檢測,然後到了真正啓動 Activity 的時候,再把它變回,去啓動真正的目標 Activity。那麼這一步是怎麼做的呢?

我們可以打開核心庫裏面的 AndroidMainfest.xml 看看:

<application>
    <!-- Stub Activities -->
    <activity android:name=".A$1" android:launchMode="standard"/>
    <activity android:name=".A$2" android:launchMode="standard"
        android:theme="@android:style/Theme.Translucent" />

    <!-- Stub Activities -->
    <activity android:name=".B$1" android:launchMode="singleTop"/>
    <activity android:name=".B$2" android:launchMode="singleTop"/>
    <activity android:name=".B$3" android:launchMode="singleTop"/>
    <activity android:name=".B$4" android:launchMode="singleTop"/>
    <activity android:name=".B$5" android:launchMode="singleTop"/>
    <activity android:name=".B$6" android:launchMode="singleTop"/>
    <activity android:name=".B$7" android:launchMode="singleTop"/>
    <activity android:name=".B$8" android:launchMode="singleTop"/>

    <!-- Stub Activities -->
    <activity android:name=".C$1" android:launchMode="singleTask"/>
    <activity android:name=".C$2" android:launchMode="singleTask"/>
    <activity android:name=".C$3" android:launchMode="singleTask"/>
    <activity android:name=".C$4" android:launchMode="singleTask"/>
    <activity android:name=".C$5" android:launchMode="singleTask"/>
    <activity android:name=".C$6" android:launchMode="singleTask"/>
    <activity android:name=".C$7" android:launchMode="singleTask"/>
    <activity android:name=".C$8" android:launchMode="singleTask"/>

    <!-- Stub Activities -->
    <activity android:name=".D$1" android:launchMode="singleInstance"/>
    <activity android:name=".D$2" android:launchMode="singleInstance"/>
    <activity android:name=".D$3" android:launchMode="singleInstance"/>
    <activity android:name=".D$4" android:launchMode="singleInstance"/>
    <activity android:name=".D$5" android:launchMode="singleInstance"/>
    <activity android:name=".D$6" android:launchMode="singleInstance"/>
    <activity android:name=".D$7" android:launchMode="singleInstance"/>
    <activity android:name=".D$8" android:launchMode="singleInstance"/>

</application>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

可以發現,在清單裏面註冊了一堆假的 Activity。 ABCD分別對應不同的啓動模式,那麼,我們啓動插件的 Activity 的時候,是如何把它改爲清單裏面已註冊的這些假的 Activity 名呢?

VAInstrumentation 裏面,重寫了 startActivity 的必經之路,就是 execStartActivity() 方法:

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

    // 這裏面做了一系列操作,實際上就是查找插件裏面第一個符合隱式條件的第一個ResolveInfo,並設置進intent
    mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
    // null component is an implicitly intent
    if (intent.getComponent() != null) {
        Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(),
                intent.getComponent().getClassName()));
        // !!! 重頭戲在這裏,用那些註冊的假的StubActivity來替換真實的Activity,以繞過檢測 !!!
        this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
    }

    ActivityResult result = realExecStartActivity(who, contextThread, token, target,
                intent, requestCode, options);

    return result;

}

private ActivityResult realExecStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    ActivityResult result = null;
    try {
        Class[] parameterTypes = {Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class,
        int.class, Bundle.class};
        result = (ActivityResult)ReflectUtil.invoke(Instrumentation.class, mBase,
                "execStartActivity", parameterTypes,
                who, contextThread, token, target, intent, requestCode, options);
    } catch (Exception e) {
        e.printStackTrace();
    }

    return result;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

那麼,是如何替換 StubActivity 的呢? 跟進代碼:

public void markIntentIfNeeded(Intent intent) {
    if (intent.getComponent() == null) {
        return;
    }

    String targetPackageName = intent.getComponent().getPackageName();
    String targetClassName = intent.getComponent().getClassName();
    // 判斷是否是啓動插件的Activity
    if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
        // 做標記
        intent.putExtra(Constants.KEY_IS_PLUGIN, true);
        // 保存真實的意圖
        intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
        intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
        dispatchStubActivity(intent);
    }
}

/**
 * 真正的轉換就在這裏。根據啓動模式,轉換對應的 StubActivity
 */
private void dispatchStubActivity(Intent intent) {
    ComponentName component = intent.getComponent();
    String targetClassName = intent.getComponent().getClassName();
    LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);
    ActivityInfo info = loadedPlugin.getActivityInfo(component);
    if (info == null) {
        throw new RuntimeException("can not find " + component);
    }
    int launchMode = info.launchMode;
    Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
    themeObj.applyStyle(info.theme, true);

    // 實際上就是這一句,完成轉換
    String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
    Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
    intent.setClassName(mContext, stubActivity);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

繼續跟進代碼:

class StubActivityInfo {
    public static final int MAX_COUNT_STANDARD = 1;
    public static final int MAX_COUNT_SINGLETOP = 8;
    public static final int MAX_COUNT_SINGLETASK = 8;
    public static final int MAX_COUNT_SINGLEINSTANCE = 8;

    public static final String corePackage = "com.didi.virtualapk.core";

    // 這個格式,就是那些假的Activity的名字
    public static final String STUB_ACTIVITY_STANDARD = "%s.A$%d";
    public static final String STUB_ACTIVITY_SINGLETOP = "%s.B$%d";
    public static final String STUB_ACTIVITY_SINGLETASK = "%s.C$%d";
    public static final String STUB_ACTIVITY_SINGLEINSTANCE = "%s.D$%d";

    public final int usedStandardStubActivity = 1;
    public int usedSingleTopStubActivity = 0;
    public int usedSingleTaskStubActivity = 0;
    public int usedSingleInstanceStubActivity = 0;

    private HashMap<String, String> mCachedStubActivity = new HashMap<>();

    public String getStubActivity(String className, int launchMode, Theme theme) {
        String stubActivity= mCachedStubActivity.get(className);
        if (stubActivity != null) {
            return stubActivity;
        }

        TypedArray array = theme.obtainStyledAttributes(new int[]{
                android.R.attr.windowIsTranslucent,
                android.R.attr.windowBackground
        });
        boolean windowIsTranslucent = array.getBoolean(0, false);
        array.recycle();
        if (Constants.DEBUG) {
            Log.d("StubActivityInfo", "getStubActivity, is transparent theme ? " + windowIsTranslucent);
        }
        stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
        switch (launchMode) {
            case ActivityInfo.LAUNCH_MULTIPLE: {
                stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
                if (windowIsTranslucent) {
                    stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
                }
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_TOP: {
                usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_TASK: {
                usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {
                usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
                break;
            }

            default:break;
        }

        mCachedStubActivity.put(className, stubActivity);
        return stubActivity;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

到這一步,就基本清晰了。同樣的,既然變爲了 StubActivity,那麼真正啓動的時候還得變回來才行。來看一下重寫後的 newActivity() 方法:

@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    try {
        cl.loadClass(className);
    } catch (ClassNotFoundException e) {
        // 根據 intent 類型,去獲取相應的插件
        LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
        // 這裏就是從Intent中取出我們剛纔保存的真正的意圖
        String targetClassName = PluginUtil.getTargetActivity(intent);

        Log.i(TAG, String.format("newActivity[%s : %s]", className, targetClassName));

        if (targetClassName != null) {
            // mBase 是未替換之前的 Instrumentation 對象,所以這個實際上是交給系統原先的 Instrumentation 對象去執行,所以這個模式其實也可以理解爲與動態代理等同
            // plugin.getClassLoader() 是自己構造的一個 DexClassLoader,專門用於加載對應的apk裏面的類
            Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
            activity.setIntent(intent);

            try {
                // for 4.1+
                ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
            } catch (Exception ignored) {
                // ignored.
            }

            return activity;
        }
    }

    return mBase.newActivity(cl, className, intent);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

到這裏,插件的 Activity 啓動流程分析,就基本結束了。細節方面,沒法一步到位,還需要大家邊看源碼邊理解,這樣才能看得更透徹。

Service 支持

對於 Service 的支持,採用動態代理AMS,攔截 Service 相關的請求,將其中轉給Service Runtime去處理,Service Runtime會接管系統的所有操作。

對於我們動態代理AMS,在上一節 Activity支持 中已經介紹過了,那麼,簡單的來看一下 ActivityManagerProxy 是如何啓動一個Service的。

在執行 startService 等方法的時候,AMS 代理對象會相應的來執行以下這些方法:

private Object startService(Object proxy, Method method, Object[] args) throws Throwable {
    IApplicationThread appThread = (IApplicationThread) args[0];
    Intent target = (Intent) args[1];
    ResolveInfo resolveInfo = this.mPluginManager.resolveService(target, 0);
    if (null == resolveInfo || null == resolveInfo.serviceInfo) {
        // is host service
        return method.invoke(this.mActivityManager, args);
    }

    return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
}

private ComponentName startDelegateServiceForTarget(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
    Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
    return mPluginManager.getHostContext().startService(wrapperIntent);
}

private Intent wrapperTargetIntent(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
    // fill in service with ComponentName
    target.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
    String pluginLocation = mPluginManager.getLoadedPlugin(target.getComponent()).getLocation();

    // 這裏進行判斷,看是交給 LocalService,還是 RemoteService 處理
    boolean local = PluginUtil.isLocalService(serviceInfo);
    Class<? extends Service> delegate = local ? LocalService.class : RemoteService.class;
    Intent intent = new Intent();
    intent.setClass(mPluginManager.getHostContext(), delegate);
    intent.putExtra(RemoteService.EXTRA_TARGET, target);

    // 保存一下這個的Command,對應執行不同操作
    intent.putExtra(RemoteService.EXTRA_COMMAND, command);
    intent.putExtra(RemoteService.EXTRA_PLUGIN_LOCATION, pluginLocation);
    if (extras != null) {
        intent.putExtras(extras);
    }

    return intent;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

實際上包括我們調用 stopService,AMS 代理對象最後變換後的意圖,同樣也是上面代碼的最後兩個個方法 startDelegateServiceForTargetwrapperTargetIntent(),只不過 command 不一樣。

所以本質上 AMS 作爲代理,不管你執行啓動或者關閉插件裏面的 Service,他都是調用 LocalService 或者 RemoteService 的 startService 方法,在 LocalService 或者 RemoteService 的 onStartCommand 下,根據command 進行相應的操作。那麼我們來看一下 LocalService 的 onStartCommand 方法:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    if (null == intent || !intent.hasExtra(EXTRA_TARGET) || !intent.hasExtra(EXTRA_COMMAND)) {
        return START_STICKY;
    }

    Intent target = intent.getParcelableExtra(EXTRA_TARGET);
    int command = intent.getIntExtra(EXTRA_COMMAND, 0);
    if (null == target || command <= 0) {
        return START_STICKY;
    }

    ComponentName component = target.getComponent();
    LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);

    switch (command) {
        case EXTRA_COMMAND_START_SERVICE: {
            ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
            IApplicationThread appThread = mainThread.getApplicationThread();
            Service service;

            if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
                service = this.mPluginManager.getComponentsHandler().getService(component);
            } else {
                try {
                    service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();

                    Application app = plugin.getApplication();
                    IBinder token = appThread.asBinder();
                    Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                    IActivityManager am = mPluginManager.getActivityManager();

                    attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                    service.onCreate();
                    this.mPluginManager.getComponentsHandler().rememberService(component, service);
                } catch (Throwable t) {
                    return START_STICKY;
                }
            }

            service.onStartCommand(target, 0, this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());
            break;
        }
        case EXTRA_COMMAND_BIND_SERVICE: {
            ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
            IApplicationThread appThread = mainThread.getApplicationThread();
            Service service = null;

            if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
                service = this.mPluginManager.getComponentsHandler().getService(component);
            } else {
                try {
                    service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();

                    Application app = plugin.getApplication();
                    IBinder token = appThread.asBinder();
                    Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                    IActivityManager am = mPluginManager.getActivityManager();

                    attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                    service.onCreate();
                    this.mPluginManager.getComponentsHandler().rememberService(component, service);
                } catch (Throwable t) {
                    t.printStackTrace();
                }
            }
            try {
                IBinder binder = service.onBind(target);
                IBinder serviceConnection = PluginUtil.getBinder(intent.getExtras(), "sc");
                IServiceConnection iServiceConnection = IServiceConnection.Stub.asInterface(serviceConnection);
                iServiceConnection.connected(component, binder);
            } catch (Exception e) {
                e.printStackTrace();
            }
            break;
        }
        case EXTRA_COMMAND_STOP_SERVICE: {
            Service service = this.mPluginManager.getComponentsHandler().forgetService(component);
            if (null != service) {
                try {
                    service.onDestroy();
                } catch (Exception e) {
                    Log.e(TAG, "Unable to stop service " + service + ": " + e.toString());
                }
            } else {
                Log.i(TAG, component + " not found");
            }
            break;
        }
        case EXTRA_COMMAND_UNBIND_SERVICE: {
            Service service = this.mPluginManager.getComponentsHandler().forgetService(component);
            if (null != service) {
                try {
                    service.onUnbind(target);
                    service.onDestroy();
                } catch (Exception e) {
                    Log.e(TAG, "Unable to unbind service " + service + ": " + e.toString());
                }
            } else {
                Log.i(TAG, component + " not found");
            }
            break;
        }
    }

    return START_STICKY;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107

很顯然,在這裏面纔對應去控制了插件Service的生命週期。具體代碼就留給大家分析吧~~

ContentProvider 支持

動態代理 IContentProvider,攔截provider相關的請求,將其中轉給Provider Runtime去處理,Provider Runtime會接管系統的所有操作。

我們來看一下 com.didi.virtualapk.internal.PluginContentResolver 這個類:

public class PluginContentResolver extends ContentResolver {
    private ContentResolver mBase;
    private PluginManager mPluginManager;
    private static Method sAcquireProvider;
    private static Method sAcquireExistingProvider;
    private static Method sAcquireUnstableProvider;

    static {
        try {
            sAcquireProvider = ContentResolver.class.getDeclaredMethod("acquireProvider",
                    new Class[]{Context.class, String.class});
            sAcquireProvider.setAccessible(true);
            sAcquireExistingProvider = ContentResolver.class.getDeclaredMethod("acquireExistingProvider",
                    new Class[]{Context.class, String.class});
            sAcquireExistingProvider.setAccessible(true);
            sAcquireUnstableProvider = ContentResolver.class.getDeclaredMethod("acquireUnstableProvider",
                    new Class[]{Context.class, String.class});
            sAcquireUnstableProvider.setAccessible(true);
        } catch (Exception e) {
            //ignored
        }
    }

    public PluginContentResolver(Context context) {
        super(context);
        mBase = context.getContentResolver();
        mPluginManager = PluginManager.getInstance(context);
    }

    protected IContentProvider acquireProvider(Context context, String auth) {
        try {
            if (mPluginManager.resolveContentProvider(auth, 0) != null) {
                // 在這裏,去 hook 一個 IContentProvider 代理對象
                return mPluginManager.getIContentProvider();
            }

            return (IContentProvider) sAcquireProvider.invoke(mBase, context, auth);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

這個類是在構造 LoadedPlugin 的時候創建的 PluginContext 對象裏面的 getContentResolver() 裏面創建的。

class PluginContext extends ContextWrapper {

    private final LoadedPlugin mPlugin;

    public PluginContext(LoadedPlugin plugin) {
        super(plugin.getPluginManager().getHostContext());
        this.mPlugin = plugin;
    }

    @Override
    public ContentResolver getContentResolver() {
        // 創建代理支持
        return new PluginContentResolver(getHostContext());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

那麼,上面Hook 的 IContentProvider 代理對象,實際上是在 PluginManager 做的。

private void hookIContentProviderAsNeeded() {
    Uri uri = Uri.parse(PluginContentResolver.getUri(mContext));
    mContext.getContentResolver().call(uri, "wakeup", null, null);
    try {
        Field authority = null;
        Field mProvider = null;
        ActivityThread activityThread = (ActivityThread) ReflectUtil.getActivityThread(mContext);
        Map mProviderMap = (Map) ReflectUtil.getField(activityThread.getClass(), activityThread, "mProviderMap");
        Iterator iter = mProviderMap.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();
            Object key = entry.getKey();
            Object val = entry.getValue();
            String auth;
            if (key instanceof String) {
                auth = (String) key;
            } else {
                if (authority == null) {
                    authority = key.getClass().getDeclaredField("authority");
                    authority.setAccessible(true);
                }
                auth = (String) authority.get(key);
            }
            if (auth.equals(PluginContentResolver.getAuthority(mContext))) {
                if (mProvider == null) {
                    mProvider = val.getClass().getDeclaredField("mProvider");
                    mProvider.setAccessible(true);
                }
                IContentProvider rawProvider = (IContentProvider) mProvider.get(val);
                IContentProvider proxy = IContentProviderProxy.newInstance(mContext, rawProvider);
                mIContentProvider = proxy;
                Log.d(TAG, "hookIContentProvider succeed : " + mIContentProvider);
                break;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

這一塊的內容,最好根據滴滴提供的Demo,再來看,比較容易理解。

Uri bookUri = Uri.parse("content://com.ryg.chapter_2.book.provider/book");
ContentValues values = new ContentValues();
values.put("_id", 6);
values.put("name", "程序設計的藝術");
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

哈哈,作者有點懶,用了任玉剛的《Android開發藝術探索》 改的,被發現了

這裏寫圖片描述

Receiver 支持

官方解釋是將插件中靜態註冊的receiver重新註冊一遍。在代碼裏貌似沒找到相應的支持,Demo 裏也沒有,或許這部分還沒完成吧??

小結

本文重點在於分析插件的 Activity 啓動流程,其他包括主題、資源,並沒有詳細分析,因爲說細了內容還是有點多了,主要是讓大夥兒在閱讀代碼時,有個大致的方向。有疑問歡迎一起探討喲~~

感謝閱讀!

1
1

猜你在找
機器學習之概率與統計推斷
機器學習之數學基礎
機器學習之凸優化
機器學習之矩陣
響應式佈局全新探索
探究Linux的總線、設備、驅動模型
深度學習基礎與TensorFlow實踐
深度學習之神經網絡原理與實戰技巧
前端開發在線峯會
TensorFlow實戰進階:手把手教你做圖像識別應用
查看評論

  暫無評論

發表評論
  • 用 戶 名:
  • wzx104104104
      
* 以上用戶言論只代表其個人觀點,不代表CSDN網站的觀點或立場
    個人資料
    • 訪問:525366次
    • 積分:5120
    • 等級:
    • 排名:第5153名
    • 原創:71篇
    • 轉載:2篇
    • 譯文:4篇
    • 評論:342條
    GitHub
    Google最新hosts鏡像(自動在線更新)

    我的微博
    最新評論
您有1條新通知
收藏助手

保存代碼片

整理和分享保存的代碼片,請訪問代碼筆記
  • *標題
  • *描述
  •  標籤
    VirtualAPKxAndroidx插件化x滴滴x代理x

提問

您的問題將會被髮布在“技術問答”頻道×
該問題已存在,請勿重複提問
||||||
  
 
 
000:0
推薦標籤:
我要懸賞
取消發佈
可能存在類似的問題:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章