安卓平臺中的動態加載技術分析

安卓平臺的動態加載原理,本質其實仍是利用java相關知識實現。然而java語言中,開發人員能經過程序進行動態操做class的,主要是字節碼生成和類加載器這兩部分的功能。本文中也主要是圍繞這兩方面的技術,展開在安卓平臺上的應用分析。html

閱讀本文,一塊兒宏觀理解安卓插件化,熱修復,模塊化,AOP,Java類加載等知識。java

動態加載技術分析

1、Java基礎知識

一、虛擬機類的加載剖析

Java虛擬機把描述類的數據從Class文件加載到內存,對數據進行校驗、轉化解析和初始化,最終造成被虛擬機直接使用的Java類型,完成了類的加載機制。android

類型的加載、連接和初始化都是在程序運行時期間完成的,雖然會在初次加載耗必定性能,可是正是這個機制爲Java提供高度的動態擴展靈活性。如能夠編寫一個面向接口的應用程序,運行時再決定實際的實現類,實現類能夠從本地或是網絡動態加載。git

類從被加載到虛擬機內存到從內存中卸載,完整的生命週期包括:加載(loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、和卸載(UnLoading)七個階段,其中驗證、準備、解析統稱爲鏈接(Linking)。 這裏主要分析下加載階段:github

*‘加載’‘類加載’*過程當中的一個階段,主要完成三件事:1)經過類的全限定名來獲取定義此類的二進制字節流。2)將這個字節流所表明的靜態儲存結構轉化爲方法區的運行時數據結構。3)在內存中生成此類相應Class對象,做爲方法區這個類的各類數據的訪問入口。數據庫

虛擬機規定加載一個類在加載階段須要能「經過一個類的全限定名獲取此類二進制字節流」,可是並無規定要從哪裏、怎樣獲取,例如:編程

  • 從ZIP中讀取,如:jar,dex等。
  • 網絡流中獲取,如Applet。
  • 運行時計算生成,如動態代理。
  • 有其餘文件生成、數據庫中讀取等。
  • ...

這樣,開發人員就能夠經過自定義類加載器(ClassLoader),或是hook系統類加載器來控制字節流的獲取方式。bootstrap

類的顯式加載(Class.forName)和隱式加載(如:new)兩種加載方式,最後都會調用類加載器的 loadClass 方法來完成類的實際加載工做。安全

雙親委託模型

類的加載器具備雙親委託模型,該特性保證了Java程序的安全性,可是並非虛擬機強制規定的,咱們也能夠自定義類加載機制,來破壞這種機制。如安卓插件加載,熱修及OSGI模塊化動態加載中的類加載技術應用。bash

二、面向切面編程(AOP)

Java面對對象的語言,平時用的最多的就是OOP(面對對象編程),而後在不少技術研發中(如安卓動態加載,性能監控,日誌等)慢慢的接觸到一個新的名次**「AOP」**,Google事後,才發現這是一種很特別、很強大、很高級實用的編程方式。百度百科解釋以下:

AOP爲Aspect Oriented Programming的縮寫,意爲:面向切面編程,經過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。利用AOP能夠對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度下降,提升程序的可重用性,同時提升了開發的效率。

其實AOP編程模式已經存在不少年類,而且在Android項目開發中也有不少地方的應用,有不少優秀且流行的工具如APT,AspectJ, Javassist等,爲咱們AOP開發提供了便利實現。

不一樣的工具庫做用在不一樣的階段,這裏引用網絡上一張圖,能夠說明三者間的區別。

AOP-引用自網絡

APT

APT(Annotation Processing Too)應該都聽過而且用過,概念比較好理解,主要做用在編譯期,也是比較流行且常見的技術。

代碼編譯期解析註解後,結合square公司的開源項目javapoet項目,生成自定邏輯的java源碼。有不少開源庫都在用如:ButterKnife、EventBus等。

AspectJ

AspectJ支持編譯期和加載時代碼注入, 有一個專門的編譯器用來生成遵照Java字節編碼規範的Class文件。更多細節能夠看這裏

Javassist

Javassist是一個開源的分析、編輯和建立Java字節碼的類庫。容許開發者自由的在一個已經編譯好的類中添加新的方法,或者是修改已有的方法,容許開發者忽略被修改的類自己的細節和結構。

360開源插件項目RePlugin中,爲了減小對安卓系統的Hook點,又但願解耦開發層代碼邏輯,其中就用到了Javassist技術。

2、插件化技術分析

安卓項目發展早起,可能最初只是爲了解決65535問題及包大小,各家公司都在研究安卓平臺動態加載,也就是如今所謂的插件化解決方案。

從14年的Dynamic-load-apk開源項目,到如今(17年)的RePluginVirtualAPK,分析下實現原理,我的理解,其實並非很難,無非就是利用靜態代理、動態代理,反射,HOOK及ClassLoader(dex合併,或雙親委託破壞)等相關技術對安卓四大組件的沙盒實現。

固然真正的難點在於對千萬安卓機型及Rom的適配,及完整安卓功能的兼容實現,不能否認插件化大牛在安卓行業發展的巨大貢獻(感謝各位開源大佬🙏)。

既然已經有不少優秀的開源庫,那麼咱們是否還有必要學習它呢?答案是否認的。

我的認爲,學習插件化是一名安卓開發,接觸系統源碼,牢固安卓知識,提高開發技能得良好途徑。推薦能夠好好學習下15年最先開源DroidPlugin項目,雖然和今年開源的RePluginVirtualAPK比,有必定的複雜及兼容差,可是分析後者其實仍是會有前者實現思想,基本上萬變不離其綜(如activity的插件實現,都是佔坑思想;都是在startActivity後,利用必定技術手段在必定點替換targeActivity,等AMS回調回來,在找一合適點,換回原activity)。我的理解後者是從不一樣角度對DroidPlugin的升級及優化。

3、熱修復

安卓技術發展中期,有不少大型APP,須要緊急解決線上問題,業界開始研究線上熱修復技術。

期間,Google官方爲了解決65535問題,發佈了Multidex多dex方案,利用Application類加載及初始化的過程,從中attachContext時候,動態加載多個dex。並在Android Studio 2.0發佈了Intant Run快速代碼部署方案(熱插拔)。

這不但給插件化給出類借鑑方案,也給熱修改提供了Java層的可行方案(此文不展開Native層的實現分析)。具備表明性的項目如美團的熱更新方案Robust

這裏簡單分析下Google intant Run實現原理

經過閱讀官方文檔及源碼,可看出官方代碼快速部分定義了三個概念:

  • 熱拔插:代碼改變被應用、投射到APP上,不須要重啓應用,不須要重建當前activity。場景:適用於多數的簡單改變(包括一些方法實現的修改,或者變量值修改)
  • 溫拔插:activity須要被重啓才能看到所需更改。場景:典型的狀況是代碼修改涉及到了資源文件,即res。
  • 冷拔插:app須要被重啓(可是仍然不須要從新安裝)場景:任何涉及結構性變化的,好比:修改了繼承規則、修改了方法簽名等。

搜下Intant Run Server源碼會發現代碼更新方式邏輯以下:

private int handlePatches(List<ApplicationPatch> changes,
            boolean hasResources, int updateMode) {
        if (hasResources) {
            FileManager.startUpdate();
        }
        for (ApplicationPatch change : changes) {
            String path = change.getPath();
            if (path.endsWith(".dex")) {
                handleColdSwapPatch(change);//冷拔插

                boolean canHotSwap = false;
                for (ApplicationPatch c : changes) {
                    if (c.getPath().equals("classes.dex.3")) {
                        canHotSwap = true;
                        break;
                    }
                }
                if (!canHotSwap) {
                    updateMode = 3;
                }
            } else if (path.equals("classes.dex.3")) {
                updateMode = handleHotSwapPatch(updateMode, change);//熱拔插
            } else if (isResourcePath(path)) {
                updateMode = handleResourcePatch(updateMode, change, path);//溫拔插
            }
        }
        if (hasResources) {
            FileManager.finishUpdate(true);
        }
        return updateMode;
    }
複製代碼

主要分析下涉及到代碼更新的熱插拔和冷插拔。分析代碼過程主要是反編經過Intant Run爲咱們生成的Apk,而後順藤摸瓜。

首先看這個殼子APK的Application都幹了什麼,分析下殼子APK是怎麼啓動咱們真正的App的。

protected void attachBaseContext(Context context) {
        if (!AppInfo.usingApkSplits) {
            String apkFile = context.getApplicationInfo().sourceDir;
            long apkModified = apkFile != null ? new File(apkFile)
                    .lastModified() : 0L;
            createResources(apkModified);
            setupClassLoaders(context, context.getCacheDir().getPath(),
                    apkModified);
        }
        createRealApplication();
        if (this.realApplication != null) {
            try {
                Method attachBaseContext = ContextWrapper.class
                        .getDeclaredMethod("attachBaseContext",
                                new Class[] { Context.class });
                attachBaseContext.setAccessible(true);
                attachBaseContext.invoke(this.realApplication,
                        new Object[] { context });
            } catch (Exception e) {
            }
        }
    }
    
    private void createResources(long apkModified) {
        File file = FileManager.getExternalResourceFile();
        this.externalResourcePath = (file != null ? file.getPath() : null);
       
        if (file != null) {
            try {
                long resourceModified = file.lastModified();
               
                if ((apkModified == 0L) || (resourceModified <= apkModified)) {
                    if (Log.isLoggable("InstantRun", 2)) {
                        Log.v("InstantRun",
                                "Ignoring resource file, older than APK");
                    }
                    this.externalResourcePath = null;
                }
            } catch (Throwable t) {
                Log.e("InstantRun", "Failed to check patch timestamps", t);
            }
        }
    }

    private static void setupClassLoaders(Context context, String codeCacheDir,
                                          long apkModified) {
        List dexList = FileManager.getDexList(context, apkModified);
        Class server = Server.class;
        Class patcher = MonkeyPatcher.class;
        if (!dexList.isEmpty()) {
            ClassLoader classLoader = BootstrapApplication.class
                    .getClassLoader();
            String nativeLibraryPath;
            try {
                nativeLibraryPath = (String) classLoader.getClass()
                        .getMethod("getLdLibraryPath", new Class[0])
                        .invoke(classLoader, new Object[0]);
                if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun", "Native library path: "
                            + nativeLibraryPath);
                }
            } catch (Throwable t) {
                nativeLibraryPath = FileManager.getNativeLibraryFolder()
                        .getPath();
            }
            IncrementalClassLoader.inject(classLoader, nativeLibraryPath,
                    codeCacheDir, dexList);
        }
    }
複製代碼

這裏能夠看到最後會有個IncrementalClassLoader,當前PathClassLoader委託IncrementalClassLoader加載dex。代碼冷部署方案中,當APK進程重啓時,IncrementalClassLoader會優先加載部署的變動代碼。 分析源碼看出它的結構圖以下:

引用網絡圖片

createRealApplication方法建立出真正的Application,啓動真正Dex方式和上面插件化思想有點相似,也是一個殼子動態加載代碼的很好例子。

private void createRealApplication() {
        if (AppInfo.applicationClass != null) {
            if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun",
                        "About to create real application of class name = "
                                + AppInfo.applicationClass);
            }
            try {
                Class realClass = (Class) Class
                        .forName(AppInfo.applicationClass);
                if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun",
                            "Created delegate app class successfully : "
                                    + realClass + " with class loader "
                                    + realClass.getClassLoader());
                }
                Constructor constructor = realClass
                        .getConstructor(new Class[0]);
                this.realApplication = ((Application) constructor
                        .newInstance(new Object[0]));
                if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun",
                            "Created real app instance successfully :"
                                    + this.realApplication);
                }
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        } else {
            this.realApplication = new Application();
        }
    }
複製代碼

真正的Application建立後,接下來再分析下變動的代碼是如何熱更新生效的?

public void onCreate() {
        if (!AppInfo.usingApkSplits) {
            MonkeyPatcher.monkeyPatchApplication(this, this,
                    this.realApplication, this.externalResourcePath);
            MonkeyPatcher.monkeyPatchExistingResources(this,
                    this.externalResourcePath, null);
        } else {
            MonkeyPatcher.monkeyPatchApplication(this, this,//反射 ActivityThread 替換Application
                    this.realApplication, null);
        }
        if (AppInfo.applicationId != null) {
            try {
                boolean foundPackage = false;
                int pid = Process.myPid();
                ActivityManager manager = (ActivityManager) getSystemService("activity");
                List processes = manager
                        .getRunningAppProcesses();
                boolean startServer = false;
                if ((processes != null) && (processes.size() > 1)) {
                    for (ActivityManager.RunningAppProcessInfo processInfo : processes) {
                        if (AppInfo.applicationId
                                .equals(processInfo.processName)) {
                            foundPackage = true;
                            if (processInfo.pid == pid) {
                                startServer = true;
                                break;
                            }
                        }
                    }
                    if ((!startServer) && (!foundPackage)) {
                        startServer = true;
                    }
                } else {
                    startServer = true;
                }
                if (startServer) {
                    Server.create(AppInfo.applicationId, this);
                }
            } catch (Throwable t) {
                Server.create(AppInfo.applicationId, this);
            }
        }
        if (this.realApplication != null) {
            this.realApplication.onCreate();//真正的application回調onCreate
        }
    }
    
     public static void monkeyPatchApplication(Context context,
                                              Application bootstrap, Application realApplication,
                                              String externalResourceFile) {
        try {
            Class activityThread = Class
                    .forName("android.app.ActivityThread");
            Object currentActivityThread = getActivityThread(context,
                    activityThread);
            Field mInitialApplication = activityThread
                    .getDeclaredField("mInitialApplication");
            mInitialApplication.setAccessible(true);
            Application initialApplication = (Application) mInitialApplication
                    .get(currentActivityThread);
            if ((realApplication != null) && (initialApplication == bootstrap)) {
                mInitialApplication.set(currentActivityThread, realApplication);
            }
            if (realApplication != null) {
                Field mAllApplications = activityThread
                        .getDeclaredField("mAllApplications");
                mAllApplications.setAccessible(true);
                List allApplications = (List) mAllApplications
                        .get(currentActivityThread);
                for (int i = 0; i < allApplications.size(); i++) {
                    if (allApplications.get(i) == bootstrap) {
                        allApplications.set(i, realApplication);//完成application的替換
                    }
                }
            }
            Class loadedApkClass;
            try {
                loadedApkClass = Class.forName("android.app.LoadedApk");
            } catch (ClassNotFoundException e) {
                loadedApkClass = Class
                        .forName("android.app.ActivityThread$PackageInfo");
            }
            Field mApplication = loadedApkClass
                    .getDeclaredField("mApplication");
            mApplication.setAccessible(true);
            Field mResDir = loadedApkClass.getDeclaredField("mResDir");
            mResDir.setAccessible(true);
            Field mLoadedApk = null;
            try {
                mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
            } catch (NoSuchFieldException e) {
            }
            for (String fieldName : new String[] { "mPackages",
                    "mResourcePackages" }) {
                Field field = activityThread.getDeclaredField(fieldName);
                field.setAccessible(true);
                Object value = field.get(currentActivityThread);
                for (Map.Entry> entry : ((Map>) value).entrySet()) {
                    Object loadedApk = ((WeakReference) entry.getValue()).get();
                    if (loadedApk != null) {
                        if (mApplication.get(loadedApk) == bootstrap) {
                            if (realApplication != null) {
                                mApplication.set(loadedApk, realApplication);
                            }
                            if (externalResourceFile != null) {
                                mResDir.set(loadedApk, externalResourceFile);//資源文件替換
                            }
                            if ((realApplication != null)
                                    && (mLoadedApk != null)) {
                                mLoadedApk.set(realApplication, loadedApk);
                            }
                        }
                    }
                }
            }
        } catch (Throwable e) {
            throw new IllegalStateException(e);
        }
    }
複製代碼

程序在調用realApplication前,替換調了本身進程中全部關於Application的實例,包括:

  • ActivityThread的mInitialApplication爲realApplication
  • mAllApplications 中全部的Application爲realApplication
  • ActivityThread的mPackages,mResourcePackages中的mLoaderApk中的application爲realApplication。

monkeyPatchExistingResources方法中替換資源問題,新建一個AssetManager對象newAssetManager,而後用newAssetManager對象替換全部當前Resource、Resource.Theme的mAssets成員變量。

到這裏APK完成了啓動操做,接下來重點分析下神奇的handleHotSwapPatch熱部署,不用重啓Activity,不用重啓進程,就能夠即時生效,這種方案對安卓的線上熱修復有很大的思路借鑑。

private int handleHotSwapPatch(int updateMode, ApplicationPatch patch) {
        try {
            String dexFile = FileManager.writeTempDexFile(patch.getBytes());
            if (dexFile == null) {
                return updateMode;//code update mode
            }

            String nativeLibraryPath = FileManager.getNativeLibraryFolder()
                    .getPath();
            DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
                    this.mApplication.getCacheDir().getPath(),
                    nativeLibraryPath, getClass().getClassLoader());//加載dex

            Class<?> aClass = Class.forName(
                    "com.android.tools.fd.runtime.AppPatchesLoaderImpl", true,
                    dexClassLoader);
            try {
                PatchesLoader loader = (PatchesLoader) aClass.newInstance();
                String[] getPatchedClasses = (String[]) aClass
                        .getDeclaredMethod("getPatchedClasses", new Class[0])
                        .invoke(loader, new Object[0]);
 
                if (!loader.load()) {
                    updateMode = 3;
                }
            } catch (Exception e) {
                Log.e("InstantRun", "Couldn't apply code changes", e);
                e.printStackTrace();
                updateMode = 3;
            }
        } catch (Throwable e) {
            Log.e("InstantRun", "Couldn't apply code changes", e);
            updateMode = 3;
        }
        return updateMode;
    }
複製代碼

handleHotSwapPatch主要做用是反射調用AppPatchesLoaderImpl類的load方法,看下load方法具體實現:

public boolean load() {
        try {
            for (String className : getPatchedClasses()) {
                ClassLoader cl = getClass().getClassLoader();
                Class<?> aClass = cl.loadClass(className + "$override");
                Object o = aClass.newInstance();
                Class<?> originalClass = cl.loadClass(className);
                Field changeField = originalClass.getDeclaredField("$change");

                changeField.setAccessible(true);

                Object previous = changeField.get(null);
                if (previous != null) {
                    Field isObsolete = previous.getClass().getDeclaredField(
                            "$obsolete");
                    if (isObsolete != null) {
                        isObsolete.set(null, Boolean.valueOf(true));
                    }
                }
                changeField.set(null, o);
                if ((Log.logging != null)
                        && (Log.logging.isLoggable(Level.FINE))) {
                    Log.logging.log(Level.FINE, String.format("patched %s",
                            new Object[] { className }));
                }
            }
        } catch (Exception e) {
            if (Log.logging != null) {
                Log.logging.log(Level.SEVERE, String.format(
                        "Exception while patching %s",
                        new Object[] { "foo.bar" }), e);
            }
            return false;
        }
        return true;
    }
複製代碼

load方法主要加載了以*「Patch類名+override」*命名的一個類,並初次加載時把該類**change**中成員*$obsolete設置成true*,而後賦值給已經加載的原始類。

反編譯原class文件,會發現每一個類會多個*"IncrementalChange localIncrementalChange = $change"*成員,並會在每一個方法內添加如下代碼,已攔截patch執行邏輯。

public void onCreate(Bundle paramBundle) {
        IncrementalChange localIncrementalChange = $change;
        if (localIncrementalChange != null) {//如上文中加載步驟,會用patch類給予賦值,而後走新代碼邏輯
            localIncrementalChange.access$dispatch(
                    "onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
                            paramBundle });//經過IncrementalChange接口,分派到對應的新方法邏輯
            return;
        }
        super.onCreate(paramBundle);
        ...
    }
複製代碼

這樣,就能夠不用重啓,即時調用新類中的新方法邏輯。有興趣的同窗能夠進一步看下nuptboyzhb同窗反編譯的intant run源碼

4、模塊化

安卓技術發展到成熟器,安卓客戶度項目發展到必定規模,爲了便於各個業務線低耦合開發、加快編譯速度、按模塊業務升級及測試等方面的高效進行,各家都陸續提出了模塊化解決方案。

講到模塊化,還有個概念是「組件化」,網上有博客直接認爲是一個概念,我的認爲,二者仍是有本質的不一樣的。

我的理解,組件化目標對象是代碼,爲了解耦功能模塊代碼,加大複用力度,按照代碼功能模塊抽離出組件模塊,造成項目的「組件化」。而模塊化目標對象是開發人員,更多的是以業務線爲界限,解耦業務線間的代碼調用,使得一個業務線內高內聚,能夠獨立完成開發到上線各階段工做,而不受其餘模塊開發影響。也能夠說組件化只是模塊化項目中的一個子集概念。

模塊化過程當中,須要達到的開發目標要求,這引用阿里Atlas開源項目目標定義:

  • 在工程期,實現工程獨立開發,調試的功能,工程模塊能夠獨立。
  • 在運行期,實現完整的組件生命週期的映射,類隔離等機制。
  • 在運維期,提供快速增量的更新修復能力,快速升級。

然而,在Java世界裏,已經有了OSGI規範,是基於Java語言的動態模塊化規範。安卓模塊上,也能夠參考OSGI的解耦方式,定製ClassLoader,動態加載模塊代碼。

有興趣的能夠研究的阿里的手機淘寶研發團隊開源態組件化(Dynamic Bundle)框架項目Atlas

5、總結

本文主要以宏觀視角,從Java動態加載基礎知識,到最近比較流行的「模塊化」進行了基本概念及技術基礎的回顧,目的是對 有關Java動態加載技術在安卓平臺上的應用 等相關的技術樹的整理和梳理。但願可以對安卓平臺的插件化、及模塊化相關的動態加載技術有個整理宏觀瞭解,並針對必定方向有必定深刻研究。


歡迎轉載,請標明出處:常興E站 canking.win

相關文章
相關標籤/搜索