精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

我的Android重構(gòu)之旅:插件化篇

移動(dòng)開發(fā) Android
本文是“我的Android重構(gòu)之旅之插件化篇”,是讓我最為頭疼的一篇,在本文中,我將會(huì)和大家聊一聊“插件化”的概念,以及我們?cè)凇安寮笨蚣苌系倪x擇與碰到的一些問(wèn)題。

隨著項(xiàng)目的不斷成長(zhǎng),即便項(xiàng)目采用了 MVP 或是 MVVM 這類優(yōu)秀的架構(gòu),也很難跟得上迭代的腳步,當(dāng) APP 端功能越來(lái)越龐大、繁瑣,人員不斷加入后,牽一發(fā)而動(dòng)全局的事情時(shí)常發(fā)生,后續(xù)人員如同如履薄冰似的維護(hù)項(xiàng)目,為此我們必須考慮團(tuán)隊(duì)壯大后的開發(fā)模式,提前對(duì)業(yè)務(wù)進(jìn)行隔離,同時(shí)總結(jié)出插件化開發(fā)的流程,完善 Android 端基礎(chǔ)框架。

本文是“我的Android重構(gòu)之旅”的第三篇,也是讓我最為頭疼的一篇,在本文中,我將會(huì)和大家聊一聊“插件化”的概念,以及我們?cè)?ldquo;插件化”框架上的選擇與碰到的一些問(wèn)題。

[[236813]]

Plug-in Hello World

  • 插件化是指將 APK 分為宿主和插件的部分,在 APP 運(yùn)行時(shí),我們可以動(dòng)態(tài)的載入或者替換插件部分。宿主: 就是當(dāng)前運(yùn)行的APP。插件: 相對(duì)于插件化技術(shù)來(lái)說(shuō),就是要加載運(yùn)行的apk類文件。

插件化分為倆種形態(tài),一種插件與宿主 APP 無(wú)交互例如微信與微信小程序,一種插件與宿主極度耦合例如滴滴出行,滴滴出行將用戶信息作為獨(dú)立的模塊,需要與其他模塊進(jìn)行數(shù)據(jù)的交互,由于使用場(chǎng)景不一致,本文只針對(duì)插件與宿主有頻繁數(shù)據(jù)交互的情況。

在我們開發(fā)的過(guò)程中,往往會(huì)碰到多人協(xié)作進(jìn)行模塊化的開發(fā),我們期望能夠獨(dú)立運(yùn)行自己的模塊而又不受其他人模塊的影響,還有一個(gè)更為常見的需求,我們?cè)诳焖俚漠a(chǎn)品迭代過(guò)程中,我們往往希望能無(wú)縫銜接新的功能至用戶手機(jī)上,過(guò)于頻繁的產(chǎn)品迭代或過(guò)長(zhǎng)的開發(fā)周期,這會(huì)使得我們?cè)谂c竟品競(jìng)爭(zhēng)時(shí)失去先機(jī)。

我的Android重構(gòu)之旅:插件化篇

上圖是一款人臉識(shí)別產(chǎn)品的迭代記錄,由于上線的各個(gè)城市都有細(xì)微的邏輯差別,導(dǎo)致每次核心業(yè)務(wù)出現(xiàn) BUG 同事要一個(gè)個(gè) Push 至各各版本,然后通知各個(gè)城市的推廣商下載,這時(shí)候我就在想,能不能把我們的應(yīng)用做成插件的形式動(dòng)態(tài)下發(fā)呢,這樣就避免了每次都需要的版本升級(jí),在某次 Push 版本的深夜,我決定不能這樣下去了,我一定要用上插件化。

插件化框架的選擇

下圖是主流的插件化、組件化框架

 

我的Android重構(gòu)之旅:插件化篇

最終反復(fù)推敲決定使用滴滴出行的 VirtualAPK 作為我們的插件化框架,它有以下幾個(gè)優(yōu)點(diǎn):

  • 可與宿主工程通信
  • 兼容性強(qiáng)
  • 使用簡(jiǎn)單
  • 編譯插件方便
  • 經(jīng)過(guò)大規(guī)模使用

如果你要加載一個(gè)插件,并且這個(gè)插件無(wú)需和宿主有任何耦合,也無(wú)需和宿主進(jìn)行通信,并且你也不想對(duì)這個(gè)插件重新打包,那么推薦選擇DroidPlugin。

 

我的Android重構(gòu)之旅:插件化篇

 

插件化原理

 

  • VirtualAPK 對(duì)插件沒(méi)有額外的約束,原生的apk即可作為插件。插件工程編譯生成 Apk 后,即可通過(guò)宿主 App 加載,每個(gè)插件apk被加載后,都會(huì)在宿主中創(chuàng)建一個(gè)單獨(dú)的 LoadedPlugin 對(duì)象。如下圖所示,通過(guò)這些 LoadedPlugin 對(duì)象,VirtualAPK 就可以管理插件并賦予插件新的意義,使其可以像手機(jī)中安裝過(guò)的 App 一樣運(yùn)行。

我們?cè)谝胍豢羁蚣艿臅r(shí)候往往不能只單純的了解如何使用,應(yīng)去深入的了解它是如何工作的,特別是插件化這種熱門的技術(shù),十分感謝開源項(xiàng)目給了我們一把探尋 Android 世界的金鑰匙,下面將和大家簡(jiǎn)易的分析下 VirtualAPK 的原理。

我的Android重構(gòu)之旅:插件化篇

四大組件對(duì)于安卓人員都是再熟悉不過(guò)了,我們都清楚四大組建都是需要在 AndroidManifest 中注冊(cè)的,而對(duì)于 VirtualAPK 來(lái)說(shuō)是不可能預(yù)先知曉名字,提前注冊(cè)在宿主 Apk 中的,所以現(xiàn)在基本都采用 hack 方案解決,VirtualAPK 大致方案如下:

Activity:在宿主 Apk 中提前占坑,然后通過(guò) Hook Activity 的啟動(dòng)過(guò)程,“欺上瞞下”啟動(dòng)插件 Apk 中的 Activity,因?yàn)? Activity 存在不同的 LaunchMode 以及一些特殊的熟悉,所以需要多個(gè)占坑的“李鬼” Activity。

  • Service:通過(guò)代理 Service 的方式去分發(fā);主進(jìn)程和其他進(jìn)程,VirtualAPK 使用了兩個(gè)代理Service。
  • BroadcastReceiver:靜態(tài)轉(zhuǎn)動(dòng)態(tài)。
  • ContentProvider:通過(guò)一個(gè)代理Provider進(jìn)行分發(fā)。

在本文,我們主要分析 Activity 的占坑過(guò)程,如果需要更深入的了解 VirtualAPK 請(qǐng)點(diǎn)我

Activity 流程

我們?nèi)绻獑⒂?VirtualAPK 的話,需要先調(diào)用pluginManager.loadPlugin(apk),進(jìn)行加載插件,然后我們繼續(xù)向下調(diào)用

 

  1. // 調(diào)用 LoadedPlugin 加載插件 Activity 信息 
  2.  LoadedPlugin plugin = LoadedPlugin.create(this, this.mContext, apk); 
  3.  // 加載插件的 Application 
  4.  plugin.invokeApplication(); 

我們可以發(fā)現(xiàn)插件 Activity 的解析是交由LoadedPlugin.create 去完成的,完成之后保存至 mPlugins 這個(gè) Map 當(dāng)中方便下次調(diào)用與解綁插件,我們繼續(xù)往下探索

 

  1. // 拷貝Resources 
  2.        this.mResources = createResources(context, apk); 
  3.        // 使用DexClassLoader加載插件并與現(xiàn)在的Dex進(jìn)行合并 
  4.        this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader()); 
  5.        // 如果已經(jīng)初始化不解析 
  6.        if (pluginManager.getLoadedPlugin(mPackageInfo.packageName) != null) { 
  7.            throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName); 
  8.        } 
  9.        // 解析APK 
  10.        this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK); 
  11.        // 拷貝插件中的So 
  12.        tryToCopyNativeLib(apk); 
  13.        // 保存插件中的 Activity 參數(shù) 
  14.        Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>(); 
  15.        for (PackageParser.Activity activity : this.mPackage.activities) { 
  16.            activityInfos.put(activity.getComponentName(), activity.info); 
  17.        } 
  18.        this.mActivityInfos = Collections.unmodifiableMap(activityInfos); 
  19.        this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]); 

LoadedPlugin 中將我們插件中的資源合并進(jìn)了宿主 App 中,至此插件 App 的加載過(guò)程就已經(jīng)完成了,這里大家肯定會(huì)有疑惑,該Activity必然沒(méi)有在Manifest中注冊(cè),這么啟動(dòng)不會(huì)報(bào)錯(cuò)嗎?

這就要涉及到 Activity 的啟動(dòng)流程了,我們?cè)趕tartActivity之后系統(tǒng)最終會(huì)調(diào)用 Instrumentation 的 execStartActivity 方法,然后再通過(guò) ActivityManagerProxy 與 AMS 進(jìn)行交互。

Activity 是否注冊(cè)在 Manifest 的校驗(yàn)是由 AMS 進(jìn)行的,所以我們?cè)谟?AMS 交互前,提前將 ActivityManagerProxy 提交給 AMS 的 ComponentName替換為我們占坑的名字即可。通常我們可以選擇 Hook Instrumentation 或者 Hook ActivityManagerProxy 都可以達(dá)到目標(biāo),VirtualAPK 選擇了 Hook Instrumentation 。

 

  1. private void hookInstrumentationAndHandler() { 
  2.        try { 
  3.            Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext); 
  4.            if (baseInstrumentation.getClass().getName().contains("lbe")) { 
  5.                // reject executing in paralell spacefor example, lbe. 
  6.                System.exit(0); 
  7.            } 
  8.            // 用于處理替換 Activity 的名稱 
  9.            final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation); 
  10.            Object activityThread = ReflectUtil.getActivityThread(this.mContext); 
  11.            // Hook Instrumentation 替換 Activity 名稱 
  12.            ReflectUtil.setInstrumentation(activityThread, instrumentation); 
  13.            // Hook handleLaunchActivity 
  14.            ReflectUtil.setHandlerCallback(this.mContext, instrumentation); 
  15.            this.mInstrumentation = instrumentation; 
  16.        } catch (Exception e) { 
  17.            e.printStackTrace(); 
  18.        } 
  19.    } 

上面我們已經(jīng)成功的 Hook 了 Instrumentation ,接下來(lái)就是需要我們的李鬼上場(chǎng)了

 

  1. public ActivityResult execStartActivity( 
  2.            Context who, IBinder contextThread, IBinder token, Activity target, 
  3.            Intent intent, int requestCode, Bundle options) { 
  4.        mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent); 
  5.        // 只有是插件中的Activity 才進(jìn)行替換 
  6.        if (intent.getComponent() != null) { 
  7.            Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(), 
  8.                    intent.getComponent().getClassName())); 
  9.            // 使用"李鬼"進(jìn)行替換 
  10.            this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent); 
  11.        } 
  12.        ActivityResult result = realExecStartActivity(who, contextThread, token, target, 
  13.                    intent, requestCode, options); 
  14.        return result; 
  15.    } 

我們來(lái)看一看 markIntentIfNeeded(intent); 到底做了什么

 

  1. public void markIntentIfNeeded(Intent intent) { 
  2.         if (intent.getComponent() == null) { 
  3.             return
  4.         } 
  5.         String targetPackageName = intent.getComponent().getPackageName(); 
  6.         String targetClassName = intent.getComponent().getClassName(); 
  7.         // 保存我們?cè)袛?shù)據(jù) 
  8.         if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) { 
  9.             intent.putExtra(Constants.KEY_IS_PLUGIN, true); 
  10.             intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName); 
  11.             intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName); 
  12.             dispatchStubActivity(intent); 
  13.         } 
  14.     } 
  15.  
  16.     private void dispatchStubActivity(Intent intent) { 
  17.         ComponentName component = intent.getComponent(); 
  18.         String targetClassName = intent.getComponent().getClassName(); 
  19.         LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent); 
  20.         ActivityInfo info = loadedPlugin.getActivityInfo(component); 
  21.         // 判斷是否是插件中的Activity 
  22.         if (info == null) { 
  23.             throw new RuntimeException("can not find " + component); 
  24.         } 
  25.         int launchMode = info.launchMode; 
  26.         // 并入主題 
  27.         Resources.Theme themeObj = loadedPlugin.getResources().newTheme(); 
  28.         themeObj.applyStyle(info.theme, true); 
  29.         // 將插件中的 Activity 替換為占坑的 Activity 
  30.         String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj); 
  31.         Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity)); 
  32.         intent.setClassName(mContext, stubActivity); 
  33.     } 

可以看到上面將我們?cè)镜男畔⒈4嬷?Intent 中,然后調(diào)用了 getStubActivity(targetClassName, launchMode, themeObj); 進(jìn)行了替換

 

  1. public static final String STUB_ACTIVITY_STANDARD = "%s.A$%d"
  2.    public static final String STUB_ACTIVITY_SINGLETOP = "%s.B$%d"
  3.    public static final String STUB_ACTIVITY_SINGLETASK = "%s.C$%d"
  4.    public static final String STUB_ACTIVITY_SINGLEINSTANCE = "%s.D$%d"
  5.  
  6.    public String getStubActivity(String className, int launchMode, Theme theme) { 
  7.        String stubActivity= mCachedStubActivity.get(className); 
  8.        if (stubActivity != null) { 
  9.            return stubActivity; 
  10.        } 
  11.  
  12.        TypedArray array = theme.obtainStyledAttributes(new int[]{ 
  13.                android.R.attr.windowIsTranslucent, 
  14.                android.R.attr.windowBackground 
  15.        }); 
  16.        boolean windowIsTranslucent = array.getBoolean(0, false); 
  17.        array.recycle(); 
  18.        if (Constants.DEBUG) { 
  19.            Log.d("StubActivityInfo""getStubActivity, is transparent theme ? " + windowIsTranslucent); 
  20.        } 
  21.        stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity); 
  22.        switch (launchMode) { 
  23.            case ActivityInfo.LAUNCH_MULTIPLE: { 
  24.                stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity); 
  25.                if (windowIsTranslucent) { 
  26.                    stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2); 
  27.                } 
  28.                break; 
  29.            } 
  30.            case ActivityInfo.LAUNCH_SINGLE_TOP: { 
  31.                usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1; 
  32.                stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity); 
  33.                break; 
  34.            } 
  35.            case ActivityInfo.LAUNCH_SINGLE_TASK: { 
  36.                usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1; 
  37.                stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity); 
  38.                break; 
  39.            } 
  40.            case ActivityInfo.LAUNCH_SINGLE_INSTANCE: { 
  41.                usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1; 
  42.                stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity); 
  43.                break; 
  44.            } 
  45.  
  46.            default:break; 
  47.        } 
  48.  
  49.        mCachedStubActivity.put(className, stubActivity); 
  50.        return stubActivity; 
  51.    } 

 

  1. <!-- Stub Activities --> 
  2.      <activity android:name=".B$1" android:launchMode="singleTop"/> 
  3.      <activity android:name=".C$1" android:launchMode="singleTask"/> 
  4.      <activity android:name=".D$1" android:launchMode="singleInstance"/> 
  5.       其余略···· 

StubActivityInfo 根據(jù)同的 launchMode 啟動(dòng)相應(yīng)的“李鬼” Activity 至此,我們已經(jīng)成功的 欺騙了 AMS ,啟動(dòng)了我們占坑的 Activity 但是只成功了一半,為什么這么說(shuō)呢?因?yàn)槠垓_過(guò)了 AMS,AMS 執(zhí)行完成后,最終要啟動(dòng)的并非是占坑的 Activity ,所以我們還要能正確的啟動(dòng)目標(biāo)Activity。

我們?cè)?Hook Instrumentation 的同時(shí)一并 Hook 了 handleLaunchActivity,所以我們之間到 Instrumentation 的 newActivity 方法查看啟動(dòng) Activity 的流程。

 

  1. @Override 
  2.    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { 
  3.        try { 
  4.            // 是否能直接加載,如果能就是宿主中的 Activity 
  5.            cl.loadClass(className); 
  6.        } catch (ClassNotFoundException e) { 
  7.            // 取得正確的 Activity 
  8.            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent); 
  9.            String targetClassName = PluginUtil.getTargetActivity(intent); 
  10.            Log.i(TAG, String.format("newActivity[%s : %s]", className, targetClassName)); 
  11.            // 判斷是否是 VirtualApk 啟動(dòng)的插件 Activity 
  12.            if (targetClassName != null) { 
  13.                Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent); 
  14.                // 啟動(dòng)插件 Activity 
  15.                activity.setIntent(intent); 
  16.                try { 
  17.                    // for 4.1+ 
  18.                    ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources()); 
  19.                } catch (Exception ignored) { 
  20.                    // ignored. 
  21.                } 
  22.                return activity; 
  23.            } 
  24.        } 
  25.        // 宿主的 Activity 直接啟動(dòng) 
  26.        return mBase.newActivity(cl, className, intent); 
  27.    } 

好了,到此Activity就可以正常啟動(dòng)了。

小結(jié)

VritualApk 整理思路很清晰,在這里我們只介紹了 Activity 的啟動(dòng)方式,感興趣的同學(xué)可以去網(wǎng)上了解下其余三大組建的代理方式。不論如何如果想使用插件化框架,一定要了解其中的實(shí)現(xiàn)原理,文檔上描述的并不是所有的細(xì)節(jié),很多一些屬性什么的,以及由于其實(shí)現(xiàn)的方式造成一些特性的不支持。

引入插件化之痛

由于項(xiàng)目的宿主與插件需要進(jìn)行較為緊密的交互,在插件化的同時(shí)需要對(duì)項(xiàng)目進(jìn)行模塊化,但是模塊化并不能一蹴而就,在模塊化的過(guò)程中經(jīng)常出現(xiàn),牽一發(fā)而動(dòng)全身的問(wèn)題,在經(jīng)歷過(guò)無(wú)數(shù)個(gè)通宵的夜晚后,我總結(jié)出了模塊化的幾項(xiàng)準(zhǔn)則。

[[236815]]

VirtualAPK 本身的使用并不困難,困難的是需要逐步整理項(xiàng)目的模塊,在這期間問(wèn)題百出,因?yàn)樽陨頉](méi)有相關(guān)經(jīng)驗(yàn)在網(wǎng)上看了很多關(guān)于模塊化的文章,最終我找到有贊模塊化的文章,對(duì)他們總結(jié)出來(lái)的經(jīng)驗(yàn)深刻認(rèn)同。

在項(xiàng)目模塊化時(shí)應(yīng)該遵循以下幾個(gè)準(zhǔn)則

  • 確定業(yè)務(wù)邏輯邊界
  • 模塊的更改上保持克制
  • 公共資源及時(shí)抽取

確定業(yè)務(wù)邏輯邊界 在模塊化之前,我們先要詳細(xì)的分析業(yè)務(wù)邏輯,App 作為業(yè)務(wù)鏈的末端,由于角色所限,開發(fā)人員對(duì)業(yè)務(wù)的理解比后端要淺,所謂欲速則不達(dá),重構(gòu)不能急,理清楚業(yè)務(wù)邏輯之后再動(dòng)手。

我的Android重構(gòu)之旅:插件化篇

在模塊化進(jìn)行時(shí),我們需要將業(yè)務(wù)模塊進(jìn)行隔離,業(yè)務(wù)模塊之間不能互相依賴能存在數(shù)據(jù)傳輸,只能單向依賴宿主項(xiàng)目,為了達(dá)到這個(gè)效果 我們需要借用市面上的路由方案 ARouter ,由于篇幅原因,我在這里不做過(guò)多介紹,感興趣的同學(xué)可以自行搜索。

我的Android重構(gòu)之旅:插件化篇

項(xiàng)目改造后宿主只留下最簡(jiǎn)單的公共基礎(chǔ)邏輯,其他部分都由插件的形式裝載,這樣使得我們?cè)诎姹靖碌倪^(guò)程中自由度很高,從項(xiàng)目結(jié)構(gòu)上我們看起來(lái)很像所有插件都依賴了宿主 App 的代碼,但實(shí)際上在打包的過(guò)程中 VirtualAPK 會(huì)幫助我們剔除重復(fù)資源。

我的Android重構(gòu)之旅:插件化篇

模塊的更改上保持克制 在模塊化進(jìn)行時(shí),不要過(guò)分的追求***的目標(biāo),簡(jiǎn)單粗暴一點(diǎn),后續(xù)再逐漸改善,很多業(yè)務(wù)邏輯經(jīng)常會(huì)和其他業(yè)務(wù)邏輯產(chǎn)生牽連,它們倆會(huì)處于一個(gè)相對(duì)曖昧的關(guān)系,這種時(shí)候我們不要去強(qiáng)行的分割它們的業(yè)務(wù)邊界,過(guò)分的分割往往會(huì)因?yàn)榫幋a人員對(duì)于模塊的不清晰導(dǎo)致項(xiàng)目改造的全盤崩潰。

公共資源及時(shí)抽取 VirtualAPK 會(huì)幫助我們剔除重復(fù)資源,對(duì)于一些曖昧不清的資源我們可以索性將它放入宿主項(xiàng)目中,如果將過(guò)多的資源存于插件項(xiàng)目中,這樣會(huì)導(dǎo)致我們的插件失去應(yīng)有的靈活性和資源的復(fù)用性。

總結(jié)

最初在公司內(nèi)部推廣插件化的時(shí)候,同事們嘩然一片大多數(shù)都是對(duì)插件化的質(zhì)疑,在這里我要感謝我原來(lái)的領(lǐng)導(dǎo),在關(guān)鍵時(shí)刻給我的支持幫我頂住了大家質(zhì)疑的聲音,在十多個(gè)日日夜夜的修改重構(gòu)后,插件化后的***個(gè)上線的版本,插件化靈活的優(yōu)勢(shì)體現(xiàn)的***,每個(gè)插件只有60 KB 的大小,對(duì)服務(wù)端的帶寬幾乎沒(méi)有絲毫的壓力,幫助我們快速的進(jìn)行了產(chǎn)品的迭代 、Bug的修復(fù)。本文中,只是我自己在項(xiàng)目插件化的一些經(jīng)驗(yàn)與想法,并沒(méi)有深入的介紹如何使用 VirtualAPK 感興趣的同學(xué)可以讀一下 VirtualAPK 的 WiKi ,希望本文的設(shè)計(jì)思路能帶給你一些幫助。

責(zé)任編輯:未麗燕 來(lái)源: 簡(jiǎn)書
相關(guān)推薦

2018-07-10 10:00:15

Android架構(gòu)MVC

2011-07-29 09:56:23

2012-05-08 16:40:36

Android

2025-01-07 14:09:58

2015-07-14 09:45:09

虛擬化

2021-07-12 07:31:22

重構(gòu)軟件行業(yè)

2024-06-26 18:58:30

游戲MQ重構(gòu)

2016-05-24 10:40:32

NodeJS總結(jié)

2024-11-08 09:19:28

2024-09-27 12:04:48

2020-12-08 06:20:49

前端重構(gòu)Vue

2011-06-07 16:47:28

Android 重構(gòu)

2021-08-01 22:35:16

Vscode開發(fā)編輯器

2009-07-06 10:42:05

2011-10-31 10:32:14

OpenStack

2011-05-31 08:54:37

Android開發(fā) 架構(gòu)

2017-08-11 16:10:36

微信Android實(shí)踐

2017-08-08 16:07:57

Android 模塊化架構(gòu)

2023-03-08 10:24:05

智能自動(dòng)化數(shù)字策略

2020-11-02 12:49:16

重構(gòu)核心系統(tǒng)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

超碰人人在线| 国产精品suv一区二区三区| 97人人做人人爽香蕉精品| 国产视频一区二区三区在线观看| 国产精品人成电影| 成人免费视频国产免费观看| 超碰cao国产精品一区二区| 精品久久久久久久久久久久久| 日本不卡一区二区三区在线观看| 夜夜骚av一区二区三区| 国产精品theporn| 亚洲人成伊人成综合网久久久| 在线免费观看视频黄| 亚洲资源一区| 久久久91精品国产一区二区三区| 国产综合色香蕉精品| 九九热在线视频播放| av在线不卡顿| 亚洲大胆人体av| 奇米影音第四色| www.九色在线| 亚洲男人的天堂网| 国产精品美女久久久久av福利| 在线永久看片免费的视频| 欧美独立站高清久久| 亚洲国产另类 国产精品国产免费| 日本免费观看网站| 成人影院在线视频| 亚洲乱码国产乱码精品精可以看| 欧美韩国日本精品一区二区三区| a网站在线观看| 日韩va欧美va亚洲va久久| 欧美大片第1页| 免费看一级黄色| 色综合中文网| 亚洲精品99999| 日本少妇xxxx软件| www.久久久.com| 欧美日韩另类国产亚洲欧美一级| 精品少妇一区二区三区在线| 性xxxfreexxxx性欧美| 亚洲欧洲美洲综合色网| 欧洲精品久久| 欧美日韩国产亚洲沙发| 播五月开心婷婷综合| 99久久免费国| 亚洲第一色网站| 国产精品自在在线| 97se亚洲综合| 高潮一区二区三区乱码| 国产精品小仙女| 国产啪精品视频| 在线播放一级片| 蜜桃在线一区二区三区| 国产精品久久久久久久久影视 | 国产99在线|中文| 日韩经典在线观看| 99精品福利视频| 7777免费精品视频| 欧美日韩综合在线观看| 综合激情视频| 国产亚洲精品va在线观看| 懂色av粉嫩av蜜乳av| 白嫩白嫩国产精品| 欧美一级二级在线观看| 亚洲综合欧美在线| 激情久久一区二区| 欧美视频一区二区在线观看| 国产黄色特级片| 成人线上视频| 色综合久久天天| 爱福利视频一区二区| 日韩精品极品| 欧美午夜国产| 亚洲另类在线制服丝袜| 成人手机在线播放| 欧美成人性生活视频| 中文字幕av一区二区三区| 日韩精品大片| 9i精品一二三区| 中文字幕av一区二区三区高| 日韩国产精品一区二区| www 日韩| 日韩久久一区二区| 免费av手机在线观看| 97人澡人人添人人爽欧美| 亚洲h在线观看| 国产肥臀一区二区福利视频| 国产不卡人人| 欧美丝袜美女中出在线| 亚洲熟妇av一区二区三区| 国产精欧美一区二区三区蓝颜男同| 欧美日韩国内自拍| 亚洲精品一二三四五区| 国产精品无码久久久久| 7777精品伊人久久久大香线蕉 | 日本激情视频在线观看| 国产精品国产三级国产普通话99| 亚洲国产激情一区二区三区| 9色在线视频| 亚洲综合在线免费观看| 男女激情无遮挡| 欧美一级大片| 欧美久久婷婷综合色| 日本少妇一区二区三区| 卡一精品卡二卡三网站乱码| 亚洲精品一区二区网址| 久久久久久久久久久国产精品| 九九久久精品| 日韩在线观看成人| 久久精品第一页| 亚洲无线视频| 国产精品毛片a∨一区二区三区|国| 中文字幕一区二区三区四区免费看 | 在线一区亚洲| av午夜在线观看| 欧美视频一区二区在线观看| 色哟哟网站在线观看| 自拍偷拍欧美一区| 久久高清视频免费| 国产在线观看黄色| 国模一区二区三区白浆| 久久国产精品精品国产色婷婷| 3d成人动漫在线| 亚洲成人精品影院| 日本免费色视频| 国产精品手机在线播放| 欧美激情成人在线视频| 欧美激情一区二区三区免费观看| 国产麻豆精品在线观看| 日本午夜精品一区二区| 男女在线观看视频| 欧美午夜在线观看| 亚洲一区二区在线免费| 久久综合电影| 日韩av电影中文字幕| 亚洲av无码国产综合专区 | 久久久久久久久毛片| 久久九九国产| 国产无套精品一区二区| 国产网站在线免费观看| 一本久久综合亚洲鲁鲁五月天 | 黄色录像特级片| 在线成人av观看| 精品久久久久久久久久久久包黑料| 人人妻人人澡人人爽| 妖精视频成人观看www| 国产精品美女主播在线观看纯欲| 国产99999| 亚洲欧美中日韩| 亚洲精品久久久中文字幕| 曰本一区二区三区视频| 91精品国产乱码久久久久久蜜臀| 国产jzjzjz丝袜老师水多| 国产精品天天看| 国产熟女高潮视频| 小说区图片区色综合区| 午夜精品久久久久久久男人的天堂| www.日本在线观看| 伊人婷婷欧美激情| 亚洲国产综合av| 欧美一区二区三区免费看| 国产精品视频26uuu| 国产美女性感在线观看懂色av| 午夜激情久久久| 91人妻一区二区| 国产综合欧美| 97se亚洲综合在线| 亚洲丝袜一区| 欧美videos中文字幕| 18岁成人毛片| 国产成人在线看| 狠狠干视频网站| 欧美日韩黄网站| 九九九久久久久久| 亚洲黄色小说网| 亚洲一区二区三区四区在线 | 久久精品亚洲一区二区三区浴池| 国产日韩一区二区在线| 日韩大片在线免费观看| 欧美在线视频一区二区| 日本五码在线| 在线免费av一区| 国产又粗又猛又爽又黄的视频小说| 蜜臀av在线播放一区二区三区| 中文字幕中文字幕在线中心一区| 午夜激情成人网| 色噜噜国产精品视频一区二区| 一二三四区视频| 亚洲精品高清在线| 亚洲av人人澡人人爽人人夜夜| 夜久久久久久| 午夜免费电影一区在线观看| 深夜福利亚洲| 欧美另类老女人| 香蕉国产在线视频| 日本精品一区二区三区四区的功能| 久久精品在线观看视频| 成人性生交大片免费看视频在线| 欧美老熟妇喷水| 青草国产精品| 国产精品乱子乱xxxx| 蜜桃视频在线观看播放| 一区二区欧美亚洲| www日本在线| 亚洲成人av福利| ass极品国模人体欣赏| 国产伦精品一区二区三区视频青涩| 精品成在人线av无码免费看| 免费一区二区| 91免费版网站入口| 激情aⅴ欧美一区二区欲海潮| 夜夜嗨av一区二区三区四区| 国产青青草视频| 色综合天天综合网国产成人综合天| 亚洲人成人无码网www国产| 国产剧情在线观看一区二区| 伊人成色综合网| 国产高清一区| 久久日韩精品| 国产日韩中文在线中文字幕| 8090成年在线看片午夜| 黄色网址在线免费观看| 亚洲美女又黄又爽在线观看| 91国产精品一区| 精品国产乱码久久久久久婷婷| 青青青视频在线播放| aaa亚洲精品| 91视频福利网| 日韩av一级片| 久久免费视频3| 欧美一区久久| 一本久久a久久精品vr综合| 精品精品精品| 亚洲自拍偷拍福利| 色999久久久精品人人澡69| 欧美亚洲另类在线| 欧美男男video| 精品国产自在精品国产浪潮| 免费在线超碰| 亚洲激情在线观看| 天天摸天天干天天操| 欧美精品一二三| 超碰在线观看91| 欧美日韩午夜激情| 久久久久久久久久久久久久免费看 | 国产一二三在线视频| 99久久婷婷| 五月天亚洲综合小说网| 一道在线中文一区二区三区| 国产一区二区三区色淫影院 | 精品av久久久久电影| 日韩欧美一级在线| 一区二区三区在线电影| 亚洲一区二区精品在线| 欧美中文字幕一区二区| 日韩亚洲一区在线播放| 欧美极品中文字幕| 久久久久久久久久久一区| 九色丨蝌蚪丨成人| 亚洲专区中文字幕| 加勒比色老久久爱综合网| 国产精品一区二区三区在线观| 99国内精品久久久久| 国产一区二中文字幕在线看| 国产精品久久乐| 日本免费在线精品| 日本不良网站在线观看| 91精品国产乱码久久久久久蜜臀| av电影院在线看| 亚洲18私人小影院| 午夜影院一区| 国产精品国内视频| 精品久久久网| 91日韩在线播放| 99国产精品免费网站| 97久久精品午夜一区二区| xxxx日韩| 乱色588欧美| 亚洲人挤奶视频| 中文字幕一区二区三区四区五区| 婷婷综合伊人| 日本免费成人网| 一本色道精品久久一区二区三区| aⅴ在线免费观看| 毛片av一区二区| 91视频免费入口| 91在线免费播放| 欧美一个色资源| 黄色av中文字幕| 亚洲精品永久免费| a√在线中文网新版址在线| 久久黄色av网站| 亚洲国产精品www| 综合久久十次| 69堂免费视频| 久久国产精品99久久久久久老狼 | 久草在线资源站手机版| 人九九综合九九宗合| 色成人免费网站| 国产伦精品一区二区三毛| 亚洲美女久久| 国产精品亚洲天堂| 日韩午夜免费| 午夜剧场在线免费观看| 国产91综合一区在线观看| 中文字幕免费看| 国产精品国产三级国产a| 国产成人无码精品久在线观看 | 国产毛片在线视频| 亚洲国产欧美在线成人app| 成人精品一区二区三区免费 | 亚洲人做受高潮| 欧美日韩加勒比精品一区| 在线观看中文字幕2021| 精品成人佐山爱一区二区| 亚洲色偷精品一区二区三区| 欧美国产日韩精品| 69堂精品视频在线播放| 国产不卡一区二区在线观看| 欧美三级情趣内衣| 无码人妻少妇伦在线电影| 美女视频一区二区三区| 精品影片一区二区入口| 中文字幕在线播放不卡一区| 国产又黄又猛又粗又爽| 日韩午夜精品视频| www.在线播放| 欧美亚洲成人精品| 一区二区三区亚洲变态调教大结局 | 吴梦梦av在线| 影音国产精品| 成人一区二区三区仙踪林| 亚洲国产成人一区二区三区| 久久久久久久99| 91精品国产黑色紧身裤美女| 国产人成在线观看| 91精品国产高清久久久久久91| 欧美国产中文高清| 日韩久久久久久久| 久久亚洲二区| 亚洲调教欧美在线| 亚洲精品视频观看| 亚洲一区二区天堂| 国产一区二区av| 在线视频cao| 九9re精品视频在线观看re6| 国产日韩亚洲欧美精品| 97精品人人妻人人| 亚洲一区二区视频| 亚洲精品福利网站| 欧美高清电影在线看| 欧美视频二区欧美影视| 日韩久久久久久久久久久久久| 99视频在线精品国自产拍免费观看| 欧美一区二区三区影院| 18涩涩午夜精品.www| 一级片视频免费| 日韩中文字幕网站| 国产在线|日韩| 亚洲va久久久噜噜噜久久狠狠 | 日韩a级在线观看| 成人精品国产一区二区4080| 精品少妇一二三区| 欧美精品一区二区三区很污很色的 | 亚洲欧洲中文| 久久国产夜色精品鲁鲁99| 久久精品国产亚洲AV成人婷婷| 91久久国产综合久久| 四虎国产精品永远| 17婷婷久久www| 自拍偷拍精品| 日本人视频jizz页码69| 欧美激情一区二区三区不卡| 亚洲 小说区 图片区| 一本色道久久综合狠狠躁篇的优点 | 亚洲国产精品成人综合色在线婷婷| 亚洲黄网在线观看| 中文字幕av一区| av日韩久久| 性一交一乱一伧国产女士spa| 成人av在线观| 欧美另类高清videos的特点| 中文字幕无线精品亚洲乱码一区| 日韩在线激情| 欧美高清中文字幕| av中文字幕在线不卡| 无码人妻精品一区二区三区不卡| 伊人激情综合网| 99欧美精品| av一区二区三区免费观看| av中文一区二区三区| 手机av免费观看| 欧美夫妻性生活视频| 尤物tv在线精品| 亚洲色图欧美自拍| 午夜精品爽啪视频| www视频在线观看免费| 亚洲精品免费在线视频| 99视频一区|