Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> Android插件實例——360 DroidPlugin詳解

Android插件實例——360 DroidPlugin詳解

編輯:關於android開發

Android插件實例——360 DroidPlugin詳解


在中國找到錢不難,但你的一個點子不意味著是一個創業。你談一個再好的想法,比如我今天談一個創意說,新浪為什麼不收購GOOGLE呢?這個創意很好。新浪一收購GOOGLE,是不是新浪就變成老大了?你從哪兒弄來錢?怎麼去整合GOOGLE呢;

\

之前寫過有關於Android 插件方向的文章,解析了一下Android的插件原理與運行方式。很多小伙伴都問我,為什麼不把我制作的插件放到Github上,讓大家共享一下。

我只能說,大哥啊,這個插件是我在公司研發的時候制作的,商業機密,不能開源啊。

剛好,最近逛github的時候,看到了奇虎360手機助手團隊的一個Android插件開源項目。今天,我們就具體的分析一下它的原理與實現邏輯。讓大家更清楚的了解,一個Android插件的構造。


360 Android 插件項目 DroidPlugin

這個框架是奇虎360手機助手團隊,最近在github上開源出來的Android插件框架。這種精神是很值得鼓勵的。

github地址為:https://github.com/Qihoo360/DroidPlugin

好,一下純屬引入了官方的說明:

說明

DroidPlugin是360手機助手在Android系統上實現了一種新的插件機制:
它可以在無需安裝、修改的情況下運行APK文件,此機制對改進大型APP的架構,實現多團隊協作開發具有一定的好處。

特點

  1. 支持Androd 2.3以上系統

  2. 插件APK完全不需做任何修改,可以獨立安裝運行、也可以做插件運行。要以插件模式運行某個APK,你無需重新編譯、無需知道其源碼。

  3. 插件的四大組件完全不需要在Host程序中注冊,支持Service、Activity、BroadcastReceiver、ContentProvider四大組件

  4. 插件之間、Host程序與插件之間會互相認為對方已經”安裝”在系統上了。

  5. API低侵入性:極少的API。HOST程序只是需要一行代碼即可集成Droid Plugin

  6. 超強隔離:插件之間、插件與Host之間完全的代碼級別的隔離:不能互相調用對方的代碼。通訊只能使用Android系統級別的通訊方法。

  7. 支持所有系統API 資源完全隔離:插件之間、與Host之間實現了資源完全隔離,不會出現資源竄用的情況。

  8. 實現了進程管理,插件的空進程會被及時回收,占用內存低。

  9. 插件的靜態廣播會被當作動態處理,如果插件沒有運行(即沒有插件進程運行),其靜態廣播也永遠不回被觸發。

那麼這個插件框架,和我們之前所說的插件框架,有什麼特點?它的實現方式是什麼樣的?這裡我們從源碼來分析一下。


插件原理——DexClassLoader

之前,我們也討論過,一個Android的apk插件是個什麼形式。主要是三點,我總結如下:
- DexLoader動態的加載dex文件進入vm
- AndroidManifest.xml中預注冊一些將要使用的四大組件(方法很low,其實有更好的)
- 針對四大組件,分別進行抽象代理,proxy
- Apk安裝還需要單獨處理native的,so文件。
- 宿主需要處理好Resource,保證R文件的正確使用。

360這個Android插件系統是否和我們之前討論的類似呢?這裡我們從IPluginManagerImpl.java文件中,我們能夠看到其真正的安裝apk插件的方法——installPackage。具體方法詳解如下所示:

    /**
      * @param filepath 插件Apk的路徑
      * @param flags Flag
      * 
      */
    @Override
    public int installPackage(String filepath, int flags) throws RemoteException {
        //install plugin
        String apkfile = null;
        try {
            // 驗證合法性
            PackageManager pm = mContext.getPackageManager();
            PackageInfo info = pm.getPackageArchiveInfo(filepath, 0);
            if (info == null) {
                return PackageManagerCompat.INSTALL_FAILED_INVALID_APK;
            }

            apkfile = PluginDirHelper.getPluginApkFile(mContext, info.packageName);

            if ((flags & PackageManagerCompat.INSTALL_REPLACE_EXISTING) != 0) {
                // 新安裝的插件

                // 停止,刪除插件的緩存
                forceStopPackage(info.packageName);
                if (mPluginCache.containsKey(info.packageName)) {
                    deleteApplicationCacheFiles(info.packageName, null);
                }
                new File(apkfile).delete();
                Utils.copyFile(filepath, apkfile);
                PluginPackageParser parser = new PluginPackageParser(mContext, new File(apkfile));
                parser.collectCertificates(0);
                PackageInfo pkgInfo = parser.getPackageInfo(PackageManager.GET_PERMISSIONS | PackageManager.GET_SIGNATURES);

                // 驗證申請的permission,宿主中是否存在,不存在不讓安裝。
                if (pkgInfo != null && pkgInfo.requestedPermissions != null && pkgInfo.requestedPermissions.length > 0) {
                    for (String requestedPermission : pkgInfo.requestedPermissions) {
                        boolean b = false;
                        try {
                            b = pm.getPermissionInfo(requestedPermission, 0) != null;
                        } catch (NameNotFoundException e) {
                        }
                        if (!mHostRequestedPermission.contains(requestedPermission) && b) {
                            Log.e(TAG, "No Permission %s", requestedPermission);
                            new File(apkfile).delete();
                            return PluginManager.INSTALL_FAILED_NO_REQUESTEDPERMISSION;
                        }
                    }
                }

                // 保存簽名
                saveSignatures(pkgInfo);
//                if (pkgInfo.reqFeatures != null && pkgInfo.reqFeatures.length > 0) {
//                    for (FeatureInfo reqFeature : pkgInfo.reqFeatures) {
//                        Log.e(TAG, "reqFeature name=%s,flags=%s,glesVersion=%s", reqFeature.name, reqFeature.flags, reqFeature.getGlEsVersion());
//                    }
//                }

                // 拷貝插件中的native庫文件。
                copyNativeLibs(mContext, apkfile, parser.getApplicationInfo(0));
                // opt Dex文件,並使用DexClassLoader動態的載入類代碼。
                dexOpt(mContext, apkfile, parser);
                mPluginCache.put(parser.getPackageName(), parser);

                // 回調函數,通知插件安裝
                mActivityManagerService.onPkgInstalled(mPluginCache, parser, parser.getPackageName());
                sendInstalledBroadcast(info.packageName);
                return PackageManagerCompat.INSTALL_SUCCEEDED;
            } else {
                // 以下是插件,覆蓋安裝的過程。即更新的過程。與新安裝類似。不做解釋了。

                if (mPluginCache.containsKey(info.packageName)) {
                    return PackageManagerCompat.INSTALL_FAILED_ALREADY_EXISTS;
                } else {
                    forceStopPackage(info.packageName);
                    new File(apkfile).delete();
                    Utils.copyFile(filepath, apkfile);
                    PluginPackageParser parser = new PluginPackageParser(mContext, new File(apkfile));
                    parser.collectCertificates(0);
                    PackageInfo pkgInfo = parser.getPackageInfo(PackageManager.GET_PERMISSIONS | PackageManager.GET_SIGNATURES);
                    if (pkgInfo != null && pkgInfo.requestedPermissions != null && pkgInfo.requestedPermissions.length > 0) {
                        for (String requestedPermission : pkgInfo.requestedPermissions) {
                            boolean b = false;
                            try {
                                b = pm.getPermissionInfo(requestedPermission, 0) != null;
                            } catch (NameNotFoundException e) {
                            }
                            if (!mHostRequestedPermission.contains(requestedPermission) && b) {
                                Log.e(TAG, "No Permission %s", requestedPermission);
                                new File(apkfile).delete();
                                return PluginManager.INSTALL_FAILED_NO_REQUESTEDPERMISSION;
                            }
                        }
                    }
                    saveSignatures(pkgInfo);
//                    if (pkgInfo.reqFeatures != null && pkgInfo.reqFeatures.length > 0) {
//                        for (FeatureInfo reqFeature : pkgInfo.reqFeatures) {
//                            Log.e(TAG, "reqFeature name=%s,flags=%s,glesVersion=%s", reqFeature.name, reqFeature.flags, reqFeature.getGlEsVersion());
//                        }
//                    }

                    copyNativeLibs(mContext, apkfile, parser.getApplicationInfo(0));
                    dexOpt(mContext, apkfile, parser);
                    mPluginCache.put(parser.getPackageName(), parser);
                    mActivityManagerService.onPkgInstalled(mPluginCache, parser, parser.getPackageName());
                    sendInstalledBroadcast(info.packageName);
                    return PackageManagerCompat.INSTALL_SUCCEEDED;
                }
            }
        } catch (Exception e) {
            if (apkfile != null) {
                new File(apkfile).delete();
            }
            handleException(e);
            return PackageManagerCompat.INSTALL_FAILED_INTERNAL_ERROR;
        }
    }
  • 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
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116

以下是dexOpt方法的具體內容,簡單的貼一下,不做說明了。

    private void dexOpt(Context hostContext, String apkfile, PluginPackageParser parser) throws Exception {
        String packageName = parser.getPackageName();
        String optimizedDirectory = PluginDirHelper.getPluginDalvikCacheDir(hostContext, packageName);
        String libraryPath = PluginDirHelper.getPluginNativeLibraryDir(hostContext, packageName);
        ClassLoader classloader = new PluginClassLoader(apkfile, optimizedDirectory, libraryPath, ClassLoader.getSystemClassLoader());
//        DexFile dexFile = DexFile.loadDex(apkfile, PluginDirHelper.getPluginDalvikCacheFile(mContext, parser.getPackageName()), 0);
//        Log.e(TAG, "dexFile=%s,1=%s,2=%s", dexFile, DexFile.isDexOptNeeded(apkfile), DexFile.isDexOptNeeded(PluginDirHelper.getPluginDalvikCacheFile(mContext, parser.getPackageName())));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

占坑——預注冊四大組件

\

360 Android插件DroidPlugin中,主要的四大組件代理方式還是占坑。這裡所說的占坑就是指,在AndroidManifest.xml中預注冊以下可能會用到的信息。

ps.這個方式不是特別好。如果大家對為什麼要預注冊這個問題不是很了解,或者不了解為什麼不好。可以看一下我的博客,我會持續更新。http://blog.csdn.net/yzzst/article/details/45582315

OK,下面是我們從DroidPlugin的Test項目中,截取出來的AndroidManifest.xml代碼,我們看看什麼叫占坑,怎麼占坑。代碼如下所示:

 <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MyActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
             intent-filter>
         activity>
        <activity
            android:name=".ServiceTest1"
            android:label="服務測試" />
        <activity
            android:name=".ContentProviderTest"
            android:label="ContentProvider測試" />
        <activity
            android:name=".BroadcastReceiverTest"
            android:label="廣播測試" />
        <activity
            android:name=".NotificationTest"
            android:label="通知測試" />
        <activity
            android:name=".ServiceTest2"
            android:label="多進程服務測試" />
        <activity
            android:name=".ContentProviderTest2"
            android:label="多進程ContentProvider測試" />

         
        <service android:name=".Service1" />
        <service android:name=".Service2" />
        <service
            android:name=".Service3"
            android:process=":Process2" />
        <service
            android:name=".Service4"
            android:process=":Process2" />

         
        <provider
            android:name=".MyContentProvider1"
            android:authorities="com.example.ApiTest.MyContentProvider1" />
        <provider
            android:name=".MyContentProvider2"
            android:authorities="com.example.ApiTest.MyContentProvider2"
            android:process=":Process2" />

        <receiver android:name=".StaticBroadcastReceiver" >
            <intent-filter>
                <action android:name="com.example.ApiTest.StaticBroadcastReceiver" />
             intent-filter>
         receiver>

         
        <activity
            android:name=".StandardActivity"
            android:label="@string/title_activity_standard" />
        <activity
            android:name=".SingleTopActivity"
            android:label="@string/title_activity_single_top"
            android:launchMode="singleTop" />
        <activity
            android:name=".SingleTaskActivity"
            android:label="@string/title_activity_single_task"
            android:launchMode="singleTask" />
        <activity
            android:name=".SingleInstanceActivity"
            android:label="@string/title_activity_single_instance"
            android:launchMode="singleInstance" />
        <activity
            android:name=".ActivityTestActivity"
            android:label="@string/title_activity_activity_test" >
         activity>
 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
  • 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

大家都看的出來,雖然簡單的插件已經完全夠用了。但是這種方式確實不是很好。


Hook 系統API方法

\

DroidPlugin中最重要的就是,為了讓插件中的方法能夠正確的調用。DroidPlugin把所有的,方法都在內部Hook並替換掉了。

等等,為什麼不能夠正確的調用?
因為,這個是一個插件系統,Activity是虛擬Proxy出來的。四大組件,並不是真正的,即Context拿到不一定是真的。系統也不會有此類的回調。所以,很多單例都需要Context才能使用。
比如:InputMethodManager.getInstance(Context context);

這時,插件中的代碼將不能夠直接運行。當然,還有其他原因。

具體的替換和操作方法,我們能夠在 HookFactory.java文件中installHook方法中,看到整個DroidPlugin的具體操作如下所示:

public final void installHook(Context context, ClassLoader classLoader) throws Throwable {
        installHook(new IClipboardBinderHook(context), classLoader);
        //for INotificationManager
        installHook(new INotificationManagerBinderHook(context), classLoader);
        installHook(new IMountServiceBinder(context), classLoader);
        installHook(new IAudioServiceBinderHook(context), classLoader);
        installHook(new IContentServiceBinderHook(context), classLoader);
        installHook(new IWindowManagerBinderHook(context), classLoader);
        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
            installHook(new IGraphicsStatsBinderHook(context), classLoader);
        }
        if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
            installHook(new IMediaRouterServiceBinderHook(context), classLoader);
        }
        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
            installHook(new ISessionManagerBinderHook(context), classLoader);
        }
        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
            installHook(new IWifiManagerBinderHook(context), classLoader);
        }

        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
            installHook(new IInputMethodManagerBinderHook(context), classLoader);
        }

        installHook(new IPackageManagerHook(context), classLoader);
        installHook(new IActivityManagerHook(context), classLoader);
        installHook(new PluginCallbackHook(context), classLoader);
        installHook(new InstrumentationHook(context), classLoader);
        installHook(new LibCoreHook(context), classLoader);

        installHook(new SQLiteDatabaseHook(context), classLoader);
    }
  • 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

限制和缺陷

Ok,說到最後,還是得稱贊一下360。國內大公司開源的使用框架原來越少了。他們的這一舉動確實是造福了目前的Android開發者。

但是,對於DroidPlugin來說,目前還存在一下幾種不足和缺點:

  1. 無法在插件中發送具有自定義資源的Notification,例如: a. 帶自定義RemoteLayout的Notification b.
    圖標通過R.drawable.XXX指定的通知(插件系統會自動將其轉化為Bitmap)

  2. 無法在插件中注冊一些具有特殊Intent
    Filter的Service、Activity、BroadcastReceiver、ContentProvider等組件以供Android系統、已經安裝的其他APP調用。

  3. 對Activity的LaunchMode支持不夠好,Activity
    Stack管理存在一定缺陷。Activity的onNewIntent函數可能不會被觸發。 (此為BUG,未來會修復)

  4. 缺乏對Native層的Hook,對某些帶native代碼的apk支持不好,可能無法運行。比如一部分游戲(cocos2d、unity3d開發的游戲估計都用不了)無法當作插件運行。


    當然,看我說的再多,也只是我的個人理解,有興趣的同學,直接去github clone一份代碼閱讀閱讀吧。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved