移動開發整體涼涼的背景下,究竟還剩哪些 Android熱門前沿知識

1. Android架構設計模式

  • MVC架構設計模式:MVC全名是Model View Controller,是模型(model)-視圖(view)-控制器(controller)的縮寫。
  • MVP架構設計模式:MVC全名是Model View Persenter,MVP由MVC演變而來,是現在主流的開發模式。
  • MVVM架構設計模式:MVVM全名是Model-View-ViewModel,它本質上就是MVC的改進版。

各種模型的主要目的都是是分離視圖(View)和模型(Model),即將UI界面顯示和業務邏輯進行分離。

架構設計模式-MVC

移動開發整體涼涼的背景下,究竟還剩哪些 Android熱門前沿知識

MVC模式

(1) 定義:在android開發過程中,比較流行的開發框架曾經採用的是MVC框架模式。

  • M(Model)層:實體模型,處理業務邏輯。如:數據庫操作,網絡操作,I/O操作,複雜操作和耗時任務等。
  • V(View)層:處理數據顯示。在Android開發中,它一般對應著xml佈局文件。
  • C(Controller)層:處理用戶交互。在Android開發中,它一般對應著Activity/Feagment。android中主要通過activity處理用戶交互和業務邏輯,接受用戶的輸入並調用Model和View去完成用戶的需求。

(2) 特點

  • 低耦合
  • 可重用易拓展
  • 模塊職責劃分明確

(3) 實例

android本身的設計結構符合 MVC 模式。

(4) MVC優缺點

  • MVC的優點:MVC模式通過Controller來掌控全局,同時將View展示和Model的變化分離開
  • MVC也有侷限性:View層對應xml佈局文件能做的事情非常有限,所以需要把大部分View相關的操作移到Controller層的activity中。導致activity相當於充當了2個角色(View層和Controller層),不僅要處理業務邏輯,還要操作UI。一旦一個頁面的業務繁多複雜的話,activity的代碼就會越來越臃腫和複雜。

架構設計模式-MVP

移動開發整體涼涼的背景下,究竟還剩哪些 Android熱門前沿知識

MVP

MVP是從經典的MVC模式演變而來,它們的基本思想有相通的地方:Controller/Presenter負責邏輯的處理,Model提供數據,View負責顯示。在Android開發中,MVP的具體實現流程是當Presenter接收到View的請求,便從Model層獲取數據,將數據進行處理。處理好的數據再通過View層的接口回調給Activity或Fragment。這樣MVP能夠讓Activity或Fragment成為真正的View,只做與UI相關的事而不處理其他業務流程。

(1) 定義

  • M(Model)層:實體模型,處理業務邏輯。如:數據庫操作,網絡操作,I/O操作,複雜操作和耗時任務等。
  • V(View)層:負責View的繪製以及與用戶交互。在Android開發中,它一般對應著xml佈局文件和Activity/Fragment。
  • P(Presenter)層:負責完成Model層和View層間的數據交互和業務邏輯。

(2) 實例

(3) MVC和MVP的區別

MVP中的View並不直接使用Model,它們之間的通信是通過Presenter來進行的,所有的交互都發生在Presenter內部,而在MVC中View會直接從Model中讀取數據而不通過Controller

  • MVC和MVP的最大區別:MVC的Model層和View層能夠直接交互;MVP的Model層和View層不能直接交互,需通過Presenter層來進行交互。
  • Activity職責不同:Activity在MVC中屬於Controller層,在MVP中屬於View層,這是MVC和MVP很主要的一個區別。可以說Android從MVC轉向MVP開發也主要是優化Activity的代碼,避免Activity的代碼臃腫龐大。
  • View層不同:MVC的View層指的是XML佈局文件(或用Java自定義的View);MVP的View層是Activity(或Fragment)
  • 控制層不同:MVC的控制層是Activity(或Fragment);MVP的控制層是Presenter,裡面沒有很多的實際東西,主要負責Model層和View層的交互。

(4) MVP優缺點

  • MVP的優點如下:

模型與視圖完全分離,我們可以修改視圖而不影響模型;項目代碼結構清晰,一看就知道什麼類幹什麼事情;我們可以將一個Presenter用於多個視圖,而不需要改變Presenter的邏輯,這個特性非常的有用,因為視圖的變化總是比模型的變化更頻繁 ;協同工作(例如在設計師沒出圖之前可以先寫一些業務邏輯代碼)

  • MVP也有不足之處:

接口過多,一定程度影響了編碼效率。一定程度上導致Presenter的代碼量過大。為了降低Presenter中業務繁多的問題,Google又推出了MVVM,試圖通過數據驅動來減少Presenter的代碼量。

架構設計模式-MVVM

移動開發整體涼涼的背景下,究竟還剩哪些 Android熱門前沿知識

MVVM

(1) 定義

  • M(Model)層:仍然是實體模型(但是不同於之前定義的Model層),主要負責數據獲取、存儲和變化,提供數據接口供 ViewModel 層調用。
  • V(View)層:對應Activity/Feagment 和xml佈局文件 ,負責View的繪製以及與用戶交互 說明:View層僅能操作UI(數據綁定來實現 UI 更新);不能做任何和業務邏輯有關的數據操作
  • VM(ViewModel)層:負責完成Model層和View層間的數據交互和業務邏輯 說明:ViewModel層僅能做和業務邏輯有關的數據操作;不能做UI相關的操作。

2. 熱修復

什麼是熱修復

熱修復:讓應用能夠在無需重新安裝的情況實現更新,幫助應用快速建立動態修復能力。

早期遇到Bug我們一般會緊急發佈了一個版本。然而這個Bug可能就是簡簡單單的一行代碼,為了這一行代碼,進行全量或者增量更新迭代一個版本,未免有點大材小用了。而且新版本的普及需要時間,以Android用戶的升級習慣,即使是相對活躍的微信也需要10天以上的時間去覆蓋50%的用戶。使用熱修復技術,能做到1天覆蓋70%以上。這也是基於補丁體積較小,可以直接使用移動網絡下載更新。

熱修復開發流程

移動開發整體涼涼的背景下,究竟還剩哪些 Android熱門前沿知識

熱修復開發流程


目前Android業內,熱修復技術百花齊放,各大廠都推出了自己的熱修復方案,使用的技術方案也各有所異。其中QZone超級補丁基於的是dex分包方案,而dex分包是基於Java的類加載機制 ClassLoader。

ClassLoader介紹

任何一個 Java 程序都是由一個或多個 class 文件組成,在程序運行時,需要將 class 文件加載到虛擬機 中才可以使用,負責加載這些 class 文件的就是 Java 的類加載機制。 ClassLoader 的作用簡單來說就是加載 class 文件,提供給程序運行時使用。每個 Class 對象的內部都有一個 classLoader字段來標識自己是由哪個 ClassLoader 加載的。

<code>class Class {  ... private transient ClassLoader classLoader;  ...}/<code>

ClassLoader是一個抽象類,而它的主要實現類主要有:

  • BootClassLoader用於加載Android Framework層class文件。
  • PathClassLoader用於Android應用程序類加載器。可以加載指定的dex,以及jar、zip、apk中的classes.dex
  • DexClassLoader用於加載指定的dex,以及jar、zip、apk中的classes.dex

很多博客裡說 PathClassLoader只能加載已安裝的apk的dex,但是實際上 PathClassLoader和DexClassLoader一樣都能夠加載sdcard中的dex。

<code>Log.e(TAG, "Activity.class 由:" + Activity.class.getClassLoader() +" 加載");Log.e(TAG, "MainActivity.class 由:" + MainActivity.class.getClassLoader() +" 加載");//輸出:Activity.class 由:java.lang.BootClassLoader@d3052a9 加載MainActivity.class 由:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.enjoyfix-1/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.enjoyfix-1/lib/x86, /system/lib, /vendor/lib]]] 加載/<code>

它們之間的關係如下:

移動開發整體涼涼的背景下,究竟還剩哪些 Android熱門前沿知識

PathClassLoader與 DexClassLoader的共同父類是 BaseDexClassLoader。

<code>public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);    }}public class PathClassLoader extends BaseDexClassLoader { public PathClassLoader(String dexPath, ClassLoader parent) { super(dexPath, null, null, parent);    } public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent){ super(dexPath, null, librarySearchPath, parent);    }}/<code>

可以看到兩者唯一的區別在於:創建 DexClassLoader需要傳遞一個 optimizedDirectory參數,並且會將其創建為 File對象傳給 super,而 PathClassLoader則直接給到null。因此兩者都可以加載指定的dex,以及jar、zip、apk中的classes.dex

<code>PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());File dexOutputDir = context.getCodeCacheDir();DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex",dexOutputDir.getAbsolutePath(), null,getClassLoader());/<code>

optimizedDirectory參數為odex的目錄。實際上Android中的ClassLoader在加載dex時,會首先經過dexopt對dex執行優化,產生odex文件。optimizedDirectory為null時的默認路徑為:

/data/dalvik-cache。並且處於安全考慮,此目錄需要使用app私有目錄,如:getCodeCacheDir()在API 26源碼中,將DexClassLoader的optimizedDirectory標記為了 deprecated 棄用,實現也變為了:javapublicDexClassLoader(StringdexPath,StringoptimizedDirectory,StringlibrarySearchPath,ClassLoaderparent){super(dexPath,null,librarySearchPath,parent);}和PathClassLoader一摸一樣了!

雙親委託機制

創建 ClassLoader需要接收一個 ClassLoaderparent參數。這個 parent為父類加載。即:某個類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。這就是雙親委託機制

<code>protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException{ // 檢查class是否有被加載 Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //如果parent不為null,則調用parent的loadClass進行加載                c = parent.loadClass(name, false);            } else { //parent為null,則調用BootClassLoader進行加載                c = findBootstrapClassOrNull(name);            }        } catch (ClassNotFoundException e) {        } if (c == null) { // 如果都找不到就自己查找 long t1 = System.nanoTime();            c = findClass(name);        }    } return c;}/<code>

因此我們自己創建的ClassLoader: newPathClassLoader("/sdcard/xx.dex",getClassLoader());並不僅僅只能獲得 xx.dex中的Class,還能夠獲得其父ClassLoader中加載的Class。

findClass

在所有父ClassLoader無法加載Class時,則會調用自己的 findClass方法。findClass在ClassLoader中的定義為:

<code>protected Class> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name);}/<code>

其實任何ClassLoader子類,都可以重寫 loadClass與 findClass。一般如果你不想使用雙親委託,則重寫 loadClass修改其實現。而重寫 findClass則表示在雙親委託下,父ClassLoader都找不到Class的情況下,定義自己如何去查找一個Class。而我們的 PathClassLoader會自己負責加載 MainActivity這樣的程序中自己編寫的類,利用雙親委託父ClassLoader加載Framework中的 Activity。說明 PathClassLoader並沒有重寫 loadClass,因此我們可以來看看PathClassLoader中的 findClass 是如何實現的。

<code>public BaseDexClassLoader(String dexPath, File optimizedDirectory,String                         librarySearchPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, librarySearchPath,                                    optimizedDirectory);}@Overrideprotected Class> findClass(String name) throws ClassNotFoundException { List<throwable> suppressedExceptions = new ArrayList<throwable>(); //查找指定的class Class c = pathList.findClass(name, suppressedExceptions); if (c == null) { ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" +                                                       name + "\" on path: " + pathList); for (Throwable t : suppressedExceptions) {            cnfe.addSuppressed(t);        } throw cnfe;    } return c;}/<throwable>/<throwable>/<code>

實現非常簡單,從 pathList中查找class。繼續查看 DexPathList

<code>public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) { //......... // splitDexPath 實現為返回 List<file>.add(dexPath) // makeDexElements 會去 List<file>.add(dexPath) 中使用DexFile加載dex文件返回 Element數組 this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,                                           suppressedExceptions, definingContext); //.........}public Class findClass(String name, List<throwable> suppressed) { //從element中獲得代表Dex的 DexFile for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { //查找class Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz;            }        }    } if (dexElementsSuppressedExceptions != null) {        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));    } return null;}/<throwable>/<file>/<file>/<code>

熱修復

PathClassLoader中存在一個Element數組,Element類中存在一個 dexFile成員表示dex文件,即:APK中有X個dex,則Element數組就有X個元素。

移動開發整體涼涼的背景下,究竟還剩哪些 Android熱門前沿知識

而對於類的查找,由代碼 for(Elementelement:dexElements)得知,會由數組從前往後進行查找。

移動開發整體涼涼的背景下,究竟還剩哪些 Android熱門前沿知識

在 PathClassLoader中的Element數組為:[patch.dex , classes.dex , classes2.dex]。如果存在Key.class位於patch.dex與classes2.dex中都存在一份,當進行類查找時,循環獲得 dexElements中的 DexFile,查找到了Key.class則立即返回,不會再管後續的element中的 DexFile是否能加載到Key.class

了。

因此,可以將出現Bug的class單獨的製作一份patch.dex文件(補丁包),然後在程序啟動時,從服務器下載patch.dex保存到某個路徑,再通過patch.dex的文件路徑,用其創建 Element對象,然後將這個 Element對象插入到我們程序的類加載器 PathClassLoader的 pathList中的 dexElements數組頭部。這樣在加載出現Bug的class時會優先加載patch.dex中的修復類,從而解決Bug。QQ空間熱修復的原理就是這樣,利用反射Hook了PathClassLoader中pathList的dexElements數組。

3.插件化

前言

插件化技術最初源於免安裝運行 apk 的想法,這個免安裝的 apk 就可以理解為插件,而支持插件的 app 我們一般

叫宿主。宿主可以在運行時加載和運行插件,這樣便可以將 app 中一些不常用的功能模塊做成插件,一方面減小

了安裝包的大小,另一方面可以實現 app 功能的動態擴展。插件化的實現

我們如何去實現一個插件化呢?

首先我們要知道,插件apk是沒有安裝的,那我們怎麼加載它呢?不知道。。。

沒關係,這兒我們還可以細分下,一個 apk 主要就是由代碼和資源組成,所以上面的問題我們可以變為:如何加載

插件的類?如何加載插件的資源?這樣的話是不是就有眉目了。

然後我們還需要解決類的調用的問題,這個地方主要是四大組件的調用問題。我們都知道,四大組件是需要註冊

的,而插件的四大組件顯然沒有註冊,那我們怎麼去調用呢?

所以我們接下來就是解決這三個問題,從而實現插件化

1. 如何加載插件的類?

2. 如何加載插件的資源?

3. 如何調用插件類?

類加載(ClassLoader)

我們在學 java 的時候知道,java 源碼文件編譯後會生成一個 class 文件,而在 Android 中,將代碼編譯後會生成

一個 apk 文件,將 apk 文件解壓後就可以看到其中有一個或多個 classes.dex 文件,它就是安卓把所有 class 文件

進行合併,優化後生成的。

java 中 JVM 加載的是 class 文件,而安卓中 DVM 和 ART 加載的是 dex 文件,雖然二者都是用的 ClassLoader 加

載的,但因為加載的文件類型不同,還是有些區別的,所以接下來我們主要介紹安卓的 ClassLoader 是如何加載

dex 文件的。

ClassLoader的實現類

ClassLoader是一個抽象類,實現類主要分為兩種類型:系統類加載器和自定義加載器。

其中系統類加載器主要包括三種:

BootClassLoader

用於加載Android Framework層class文件。

PathClassLoader

用於Android應用程序類加載器。可以加載指定的dex,以及jar、zip、apk中的classes.dex

DexClassLoader

用於加載指定的dex,以及jar、zip、apk中的classes.dex

類繼承關係如下圖:

移動開發整體涼涼的背景下,究竟還剩哪些 Android熱門前沿知識

我們先來看下 PathClassLoader 和 DexClassLoader。

<code>// /libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.javapublic class PathClassLoader extends BaseDexClassLoader {// optimizedDirectory 直接為 nullpublic PathClassLoader(String dexPath, ClassLoader parent) {super(dexPath, null, null, parent);}// optimizedDirectory 直接為 nullpublic PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {super(dexPath, null, librarySearchPath, parent);}}// API 小於等於 26/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.javapublic class DexClassLoader extends BaseDexClassLoader {public DexClassLoader(String dexPath, String optimizedDirectory,String librarySearchPath, ClassLoader parent) {// 26開始,super裡面改變了,看下面兩個構造方法super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);}}// API 26/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.javapublic BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath, ClassLoader parent) {super(parent);// DexPathList 的第四個參數是 optimizedDirectory,可以看到這兒為 nullthis.pathList = new DexPathList(this, dexPath, librarySearchPath, null);}// API 25/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.javapublic BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath, ClassLoader parent) {super(parent);this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);}/<code>

根據源碼瞭解到,PathClassLoader 和 DexClassLoader 都是繼承自 BaseDexClassLoader,且類中只有構造方

法,它們的類加載邏輯完全寫在 BaseDexClassLoader 中。

其中我們值的注意的是,在8.0之前,它們二者的唯一區別是第二個參數 optimizedDirectory,這個參數的意思是

生成的 odex(優化的dex)存放的路徑,PathClassLoader 直接為null,而 DexClassLoader 是使用用戶傳進來的

路徑,而在8.0之後,二者就完全一樣了。

下面我們再來了解下 BootClassLoader 和 PathClassLoader 之間的關係。

<code>// 在 onCreate 中執行下面代碼ClassLoader classLoader = getClassLoader();while (classLoader != null) {Log.e("leo", "classLoader:" + classLoader);classLoader = classLoader.getParent();}Log.e("leo", "classLoader:" + Activity.class.getClassLoader());/<code>

打印結果:

<code>classLoader:dalvik.system.PathClassLoader[DexPathList[[zip file"/data/user/0/com.enjoy.pluginactivity/cache/plugin-debug.apk", zip file"/data/app/com.enjoy.pluginactivity-T4YwTh-8gHWWDDS19IkHRg==/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.pluginactivity-T4YwTh-8gHWWDDS19IkHRg==/lib/x86_64, /system/lib64, /vendor/lib64]]]classLoader:java.lang.BootClassLoader@a26e88dclassLoader:java.lang.BootClassLoader@a26e88d/<code>

通過打印結果可知,應用程序類是由 PathClassLoader 加載的,Activity 類是 BootClassLoader 加載的,並且

BootClassLoader 是 PathClassLoader 的 parent,這裡要注意 parent 與父類的區別。這個打印結果我們下面還會提到。

4. Android進程保活

(1) 進程保活概念

進程保活:讓進程在內存中永遠存在且無法殺死,就算被殺死也能保活。進程被殺死的原因:人為地調用kill;被第三方安全軟件殺死。

進程保活並非是一種流氓手段,在很多場景下我們需要一個常駐進程來為用戶提供服務,如:

  • 接收屏幕開關的系統廣播:因為廣播接收者不支持靜態註冊,必須在進程中動態註冊廣播接收者來接收,如果沒有常駐進程,那麼鎖屏應用無法為用戶正常提供服務。
  • 定位服務:需要在後臺維護一個長連接,以便及時地將信息(推送的信息/定位信息等)傳達給用戶。

缺點:進程保活在內存,不管如何優化,或多或少都會增加性能的開銷。所以需在進程保活和內存消耗之間尋找平衡點來為用戶進程保活。

(2) android進程優先級和回收策略

android進程優先級:前臺進程 > 可見進程 > 服務進程 > 後臺進程 > 空進程

android進程的回收策略:主要依靠LMK ( Low Memory Killer )機制來完成。LMK機制通過 oom_adj 這個閥值來判斷進程的優先級,oom_adj 的值越高,優先級越低,越容易被殺死。

拓展:LMK ( Low Memory Killer ) 機制基於Linux的OOM(Out Of Memery)機制,通過一些比較複雜的評分機制,對進程進行打分,將分數高的進程判定為bad進程,殺死並釋放內存。LMS機制和OOM機制的不同之處在於:OOM只有當系統內存不足時才會啟動檢查,而LMS機制是定時進行檢查。

(3) android進程保活方案

  • 利用系統廣播拉活 在發生系統事件時,系統會發出相對響應的廣播(常用的廣播事件如:開機、網絡狀態變化、文件或sd卡的卸載等),我們可以在mainfest.xml文件中靜態註冊廣播監聽器

缺點(無法拉活的情形):廣播接收者被管理軟件或系統軟件通過自啟動管理等功能禁用的場景下是無法接受廣播的,從而無法自啟動進行系統拉活;系統廣播事件是不可控制的,只有在發生事件時才能進行拉活,無法保證進程被殺死後立即被拉活。

  • 利用系統Service機制拉活 將Service中的onStartCommand()回調方法的返回值設為START_STICKY,就可以利用系統機制在Service掛掉後自動拉活。

拓展:onStartCommand()的返回值表明當Service由於系統內存不足而被系統殺掉之後,在未來的某個時間段內當系統內存足夠的情況下,系統會嘗試創建這個Service,一旦創建成功就又會回調onStartCommand()方法。

缺點(無法拉活的情形):Service第一次被異常殺死後會在5s內重啟,第二次會在10s內重啟,第三次會在20s內重啟,若Service在短時間內被殺死的次數超過3次以上系統就會不驚醒拉活;進程被取得root權限的管理工具或系統工具通過強制stop時,通過Service機制無法重啟進程。

  • 利用Native進程拉活 思想:利用Linux中的fork機制創建一個Native進程,在Native進程可以監控主進程的存活,當主進程掛掉之後,Native進程可以立即對主進程進行拉活。

在Native進程中如何監聽主進程被殺死:可在Native進程中通過死循環或定時器,輪詢地判斷主進程被殺死,但是此方案會耗時耗資源;在主線程中創建一個監控文件,並且在主進程中持有文件鎖,在拉活進程啟動後申請文件鎖將會被阻塞,一旦成功獲取到鎖說明主進程掛掉了。

如何在Native進程中拉活主進程:主要通過一個am命令即可拉活。說明:android5.0後系統對Native進程加強了管理,利用Native進程拉活的方式已失效。

  • 利用JobScheduler機制拉活

說明:android在5.0後提供了JobScheduler接口,這個接口能夠監聽主進程的存活,然後拉活進程。

  • 利用賬號同步機制拉活(已失效)

說明:android系統的賬號同步機制會定期同步賬號信息,這個方案主要是利用賬號同步機制進行進程拉活。不過最新的android版本對賬號同步機制做了改動,該方法可能不再生效。


分享到:


相關文章: