天天看點

移動開發整體涼涼的背景下,究竟還剩哪些 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

(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

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

(1) 定義

  • 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

  • M(Model)層:仍然是實體模型(但是不同于之前定義的Model層),主要負責資料擷取、存儲和變化,提供資料接口供 ViewModel 層調用。
  • V(View)層:對應Activity/Feagment 和xml布局檔案 ,負責View的繪制以及與使用者互動 說明:View層僅能操作UI(資料綁定來實作 UI 更新);不能做任何和業務邏輯有關的資料操作
  • VM(ViewModel)層:負責完成Model層和View層間的資料互動和業務邏輯 說明:ViewModel層僅能做和業務邏輯有關的資料操作;不能做UI相關的操作。

Android開發熱門前沿知識下載下傳位址:

https://shimo.im/docs/hHWyVjj6TWGrVv9p

2. 熱修複

什麼是熱修複

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

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

熱修複開發流程

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

ClassLoader

ClassLoader介紹

任何一個 Java 程式都是由一個或多個 class 檔案組成,在程式運作時,需要将 class 檔案加載到虛拟機 中才可以使用,負責加載這些 class 檔案的就是 Java 的類加載機制。

ClassLoader

的作用簡單來說就是加載 class 檔案,提供給程式運作時使用。每個 Class 對象的内部都有一個

classLoader

字段來辨別自己是由哪個

ClassLoader

加載的。

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

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。
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]]] 加載           

它們之間的關系如下:

PathClassLoader

DexClassLoader

的共同父類是

BaseDexClassLoader

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);
    }
}           

可以看到兩者唯一的差別在于:建立

DexClassLoader

需要傳遞一個

optimizedDirectory

參數,并且會将其建立為

File

對象傳給

super

,而

PathClassLoader

則直接給到null。是以兩者都可以加載指定的dex,以及jar、zip、apk中的classes.dex

PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());

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

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

為父類加載。即:某個類加載器在接到加載類的請求時,首先将加載任務委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功傳回;隻有父類加載器無法完成此加載任務時,才自己去加載。這就是雙親委托機制!

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;
}           
是以我們自己建立的ClassLoader:

newPathClassLoader("/sdcard/xx.dex",getClassLoader());

并不僅僅隻能獲得 xx.dex中的Class,還能夠獲得其父ClassLoader中加載的Class。

findClass

在所有父ClassLoader無法加載Class時,則會調用自己的

findClass

方法。

findClass

在ClassLoader中的定義為:

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

其實任何ClassLoader子類,都可以重寫

loadClass

findClass

。一般如果你不想使用雙親委托,則重寫

loadClass

修改其實作。而重寫

findClass

則表示在雙親委托下,父ClassLoader都找不到Class的情況下,定義自己如何去查找一個Class。而我們的

PathClassLoader

會自己負責加載

MainActivity

這樣的程式中自己編寫的類,利用雙親委托父ClassLoader加載Framework中的

Activity

。說明

PathClassLoader

并沒有重寫

loadClass

,是以我們可以來看看PathClassLoader中的

findClass

是如何實作的。

public BaseDexClassLoader(String dexPath, File optimizedDirectory,String 
                        librarySearchPath, ClassLoader parent) {
 super(parent);
 this.pathList = new DexPathList(this, dexPath, librarySearchPath,
                                    optimizedDirectory);
}

@Override
protected 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;
}           

實作非常簡單,從

pathList

中查找class。繼續檢視

DexPathList

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;
}           

熱修複

PathClassLoader

中存在一個Element數組,Element類中存在一個

dexFile

成員表示dex檔案,即:APK中有X個dex,則Element數組就有X個元素。

而對于類的查找,由代碼

for(Elementelement:dexElements)

得知,會由數組從前往後進行查找。

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檔案。

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

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

類繼承關系如下圖:

我們先來看下 PathClassLoader 和 DexClassLoader。

// /libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
// optimizedDirectory 直接為 null
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
// optimizedDirectory 直接為 null
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
// API 小于等于 26/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
public 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.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
// DexPathList 的第四個參數是 optimizedDirectory,可以看到這兒為 null
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
}
// API 25/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}           

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

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

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

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

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

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

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

列印結果:

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@a26e88d
classLoader:java.lang.BootClassLoader@a26e88d           

通過列印結果可知,應用程式類是由 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版本對賬号同步機制做了改動,該方法可能不再生效。