偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

面試官:你對插件化有什么了解?

開發(fā) 前端
對于一個(gè)完整功能的App,我們可以將其劃分成為很多模塊,每個(gè)模塊都可以將其劃分成為一個(gè)Apk。然后將基礎(chǔ)功能的Apk提交給應(yīng)用市場上架,后續(xù)我們可以通過基礎(chǔ)的Apk,下載其他模塊的Apk,從而完成功能的擴(kuò)展。

背景

插件化的誕生是為了解決什么問題?

我們不妨好好思考一下,作為客戶端開發(fā),平時(shí)工作中是否為這樣的情況發(fā)愁:

  • 核心業(yè)務(wù)功能迭代的時(shí)候,千小心,萬小心,又是做AB,又是做灰度,最后線上還是出問題了,這個(gè)時(shí)候只能靠發(fā)版解決問題,奈何客戶端的發(fā)版周期長,并且只有用戶升級以后才能解決問題
  • 有些業(yè)務(wù)上線以來,用的人不多,占用的包體積還不小,這些功能是否可以動(dòng)態(tài)加載
  • 開發(fā)一個(gè)功能,必須提交到應(yīng)用商店以后,用戶才能更新(涉及到監(jiān)管)

所以說,插件化設(shè)計(jì)之初就是為了不安裝新Apk,從而完成應(yīng)用的更新迭代。

我之前所在的團(tuán)隊(duì)也做了插件化,主要的原因還是包體積的訴求,原因有兩個(gè):

  • 廠商預(yù)裝的時(shí)候包體積的強(qiáng)制訴求:如果不做插件化,就需要每年預(yù)裝階段持續(xù)投入人力優(yōu)化包體積,成本比較高
  • 對外投放的時(shí)候,小包有利于提高用戶的轉(zhuǎn)化

轉(zhuǎn)換率轉(zhuǎn)換率

上圖是2018年谷歌IO披露的包體積與下載轉(zhuǎn)化率之間的關(guān)系,時(shí)至今日,即使我們的網(wǎng)絡(luò)狀況已經(jīng)有了很好的提升,但是優(yōu)化包體積仍然是我們的目標(biāo),比如說:

  • 線下推廣的時(shí)候用戶的網(wǎng)絡(luò)是不穩(wěn)定的
  • 對于線上廣告投放,用戶不可能一直在wifi下,小的包體積可以讓用戶更快的進(jìn)入應(yīng)用內(nèi),避免勸退用戶

所以我們可以看到,pdd這一方面做的很出色,僅有25m。

一、插件化難點(diǎn)

講插件化之前,我們先科普一下其中的概念。

對于一個(gè)完整功能的App,我們可以將其劃分成為很多模塊,每個(gè)模塊都可以將其劃分成為一個(gè)Apk。然后將基礎(chǔ)功能的Apk提交給應(yīng)用市場上架,后續(xù)我們可以通過基礎(chǔ)的Apk,下載其他模塊的Apk,從而完成功能的擴(kuò)展。

基礎(chǔ)分包基礎(chǔ)分包

在整個(gè)過程中,我們稱提交給應(yīng)用市場的Apk為宿主,其他模塊的Apk稱之為插件。

相信沒接觸過插件化的同學(xué)可能會有一些疑問,我們平時(shí)打包的時(shí)候不是都是一個(gè)完整的Apk,為什么可以加載一個(gè)單獨(dú)的Apk?好了,這就是插件化的第一個(gè)難點(diǎn)。

二、加載插件Apk

作為一個(gè)Android開發(fā),我們都知道Android里面的Davilk和Art虛擬機(jī)和Java虛擬機(jī)不是同一套,所以他們也有著不同的類加載結(jié)構(gòu)。

我們先回顧一下。

1. Java類加載

Jvm中加載的文件是class文件,再由Jvm翻譯成特定平臺的機(jī)器碼。使用的類加載器如下:

  • 啟動(dòng)類加載器:由C++語言實(shí)現(xiàn),負(fù)責(zé)加載Java中的核心類。
  • 擴(kuò)展類加載器:負(fù)責(zé)加載Java擴(kuò)展的核心類之外的類。
  • 應(yīng)用程序類加載器:負(fù)責(zé)加載用戶類路徑上指定的類庫。

整個(gè)加載架構(gòu)如下:

Java類加載器結(jié)構(gòu)Java類加載器結(jié)構(gòu)

雙親委派機(jī)制保證了收到類加載請求的時(shí)候,優(yōu)先讓父類加載器去加載,父類加載器處理不了的時(shí)候,才會自己去加載,保證了類加載機(jī)制的穩(wěn)定性。

2. Android類加載

我們上面提過,由于CPU和功耗環(huán)境不一致,Android虛擬機(jī)和Jvm有著很大的不同,Android里面的虛擬機(jī)在4.4.4以后,就是ART虛擬機(jī)了。早期的時(shí)候,安裝的時(shí)候,會將dex文件直接編譯成.oat這樣的機(jī)器碼,不過這樣會有其他問題:

  • 提高安裝和升級應(yīng)用的時(shí)間

于是,在 Android 7.0 以后,第一次啟動(dòng)的時(shí)候,使用 Jit,針對dex,邊解釋邊執(zhí)行,然后在空閑的時(shí)候,將剩余的 dex 文件編譯成機(jī)器碼。

所以,我們可以注意到,每次應(yīng)用升級的一段時(shí)間內(nèi),我們的啟動(dòng)時(shí)長會出現(xiàn)波動(dòng),過了幾天以后,又會達(dá)到穩(wěn)定的狀態(tài)。因此,很多大廠,會針對這個(gè)過程優(yōu)化,如:

  • 如何更多的觸發(fā) dex2aot 過程
  • 對啟動(dòng)熱點(diǎn)代碼預(yù)先aot,比如谷歌的BaselineProfile方案,很多大廠也有自己的方案

我們再來看一下類加載機(jī)制,Android里面類加載的單位是dex,類加載器包括:

  • BootClassLoader:用來解決Android系統(tǒng)啟動(dòng)時(shí)的核心基礎(chǔ)類
  • PathClassLoader:Android中默認(rèn)的類加載器,主要用來加載應(yīng)用程序自身的類以及系統(tǒng)類庫之外的本地代碼
  • DexClassLoader:加載指定路徑下的Apk、Jar包的類
  • InMemoryDexClassLoader:Android 8.0 中可以用來加載內(nèi)存中的Dex文件

整個(gè)結(jié)構(gòu)是這樣的:

Android類加載器Android類加載器

如果是指定路徑下的Apk或者jar包,我們需要將 PathClassLoader 替換成 DexClassLoader。到這里,第一個(gè)問題的解決思路就很清晰了,我們可以通過 DexClassLoader 加載插件Apk。

3. 方案實(shí)現(xiàn)

DexClassLoader的原理主要是通過DexPathList管理DexFile列表信息,從而加載到具體的類。

DexClassLoaderDexClassLoader


基于DexClassLoader,通常有兩種方案:

  • 單個(gè)DexClassLoader方案
  • 多個(gè)DexClassLoader方案

單個(gè)DexClassLoader

單個(gè)DexClassLoader指的我們可能有多個(gè)插件Dex,多個(gè)插件Dex使用同一個(gè)DexClassLoader,如圖:

單ClassLoader結(jié)構(gòu)

將所有的插件中的類都由統(tǒng)一的DexClassLoader加載。

多個(gè)DexClassLoader

多個(gè)DexClassLoader指的是對于多個(gè)插件Dex,每一個(gè)Dex都會有自己的DexClassLoader,如圖:

多ClassLoader結(jié)構(gòu)多ClassLoader結(jié)構(gòu)

由各自DexClassLoader負(fù)責(zé)相關(guān)的插件的類加載。

看一下各自的優(yōu)缺點(diǎn):

分類

優(yōu)點(diǎn)

缺點(diǎn)

單DexClassLoader

類之間不隔離,可以互相調(diào)用

需要處理一些適配問題,比如不同插件加載了同一庫的不同版本,可能引發(fā)兼容性問題

多DexClassLoader

安全、穩(wěn)定

類之間隔離,需要處理互相調(diào)用的問題

對于我們安卓系統(tǒng)來講,僅僅能夠加載插件中的類顯然是不夠的,還要能夠啟動(dòng)插件中的的四大組件,并正確的執(zhí)行四大組件的生命周期,為什么不能夠執(zhí)行四大組件的生命周期呢?

這是因?yàn)椋挥性谖覀兯拗靼蠱anifest文件中注冊的四大組件才能夠啟動(dòng),如果沒有注冊,就會拋出異常,提醒你在Manifest中注冊。這也是我們遇到的第二個(gè)問題。

三、組件加載

Android 中四大組件包括Activity、Service、廣播和ContentProvider,我們主要介紹一下Activity。

1. Activity解決方法

如果我們想讓對應(yīng)的Activity啟動(dòng),一般有如下幾種方法:

  1. 宿主包提前聲明組件
  2. 占位組件 + 手動(dòng)調(diào)用組件
  3. 占位組件 + 欺騙系統(tǒng)

我們針對這幾種分別解釋一下。

1.1 宿主包提前聲明組件

將所有的四大組件在宿主包中都提前聲明,這是最簡單粗暴的方式。

但這種方式會丟失插件化的動(dòng)態(tài)性,也就是說,如果想在插件包中,加入宿主包沒有注冊的Activity,這就會有問題。

那這種方式的優(yōu)點(diǎn)呢?解決包體積的問題的同時(shí)不用處理復(fù)雜的組件加載以及伴隨的生命周期的問題。

1.2 占位組件 + 手動(dòng)調(diào)用組件

那如果想要保存插件的動(dòng)態(tài)化加載呢?也就是說我們想要在插件包中的 Manifest 文件中進(jìn)行注冊。

默認(rèn)情況下,如果我們啟動(dòng)一個(gè)沒有在插件 Manifest 中注冊的的 Activity,會發(fā)生 error,原因是啟動(dòng)過程中的 Instrumentation 中的 checkStartActivityResult 方法:

public class Instrumentation {
    public static void checkStartActivityResult(int res, Object intent) {
        if (!ActivityManager.isStartResultFatalError(res)) {
            return;
        }

        switch (res) {
            case ActivityManager.START_INTENT_NOT_RESOLVED:
            case ActivityManager.START_CLASS_NOT_FOUND:
                if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
                    throw new ActivityNotFoundException(
                            "Unable to find explicit activity class "
                            + ((Intent)intent).getComponent().toShortString()
                            + "; have you declared this activity in your AndroidManifest.xml?");
                throw new ActivityNotFoundException(
                        "No Activity found to handle " + intent);
                ...
        }
    }
}

所以我們傳入的Activity必須要在宿主包中注冊,這樣系統(tǒng)才能檢驗(yàn)通過,那怎么才能實(shí)現(xiàn)動(dòng)態(tài)化呢?

答案是使用占位Activity,這其實(shí)就是使用的代理模式。每次需要啟動(dòng)插件中的Activity的時(shí)候,先啟動(dòng)一個(gè)占位Activity實(shí)例,然后在占位Activity實(shí)例里面持有目標(biāo)Activity的實(shí)例對象,從而通過反射或者其他方法調(diào)用實(shí)例的生命周期。

生命周期處理生命周期處理

這種方法的問題主要如下:

  1. 代碼的入侵性比較強(qiáng),需要統(tǒng)一繼承PluginActvity。
  2. 對于Activity的啟動(dòng)模式,處理比較繁瑣。
  3. 改造已有的模塊比較繁瑣。

1.3 欺騙系統(tǒng)

第三種方法我們稱之為欺騙系統(tǒng),具體怎么個(gè)欺騙方法呢?

先看一下具體Activity的啟動(dòng)流程,默認(rèn)大家對Activity的啟動(dòng)流程比較了解了:

Activity啟動(dòng)流程Activity啟動(dòng)流程

我們在整個(gè)過程中,同樣也需要一個(gè)占位Activity。

使用步驟如下:

  1. 在途中的第一步,啟動(dòng)PluginActivity跳轉(zhuǎn)的時(shí)候,通過Instrument處理的時(shí)候,會將PluginActivity的Intent改成占位Activity的Intent,并存入原始Activity的信息。
  2. 在圖片中的第十三步,等系統(tǒng)驗(yàn)證完成回來創(chuàng)建占位Activity的實(shí)例對象,就替換成PluginActivity。

最終,系統(tǒng)以為自己調(diào)用的是占位Activity的對象,并且和實(shí)際上調(diào)用的是PluginActivity進(jìn)行綁定。

在最終使用之前,我們在插件中的Android資源文件并不能使用,比如說圖片、字符串、布局文件等,原因是插件的資源路徑并沒有被添加。

四、資源加載

Apk安裝以后,我們都是通過 Resource 對象去訪問資源,簡單看一下 Resouce 的構(gòu)造方法:

@Deprecated  
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {  
    this(null);  
    mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());  
}

可以看到,構(gòu)造函數(shù)中有一個(gè)參數(shù)是 AssetManager,我們可以通過在 AssetManager 中,加入我們插件的資源地址,就可以訪問到插件中的資源。

1 解決方法

現(xiàn)在可以訪問具體的資源了,和之前的類加載方式類似,也有兩種加載方式:

  • 合并式:插件和宿主資源可以互相訪問,要處理資源沖突
  • 獨(dú)立式:無需處理資源沖突,宿主和插件的資源訪問比較難處理

首先,我們想一下為什么會有資源沖突問題?其實(shí)是因?yàn)樗拗骱筒寮际仟?dú)立編譯的,所以打包的時(shí)候生成的資源Id會存在相同的情況,這個(gè)時(shí)候,訪問的的時(shí)候就存在資源沖突。

我們項(xiàng)目之前采用的 Qigsaw 方案,所以簡單介紹一下合并式的方案,資源id是8位16進(jìn)制數(shù)表示:

QigsawQigsaw

如上圖:

  • PP為Package Id,代表應(yīng)用類型:如系統(tǒng)應(yīng)用、第三方應(yīng)用、dynamic feature等。
  • TT為資源類型:如drawabl、layout和string。
  • EEEE為Entry:代表資源順序

所以我們對不同的插件包,進(jìn)行打包的時(shí)候,前面的PP字段,可以進(jìn)行依次遞減,可以避免資源沖突的問題。常用的方案有:

  1. 修改AAPT生成ResourceId,在編譯期間完成修改
  2. 修改resouce.arsc文件

Qigsaw使用的第一種方案。

總結(jié)

本文是一篇入門插件化的文章,主要回答了插件化是什么,有什么難點(diǎn),又是怎么解決的,其中沒有涉及到很多代碼,非常適合入門。

責(zé)任編輯:武曉燕 來源: 九心說
相關(guān)推薦

2025-04-01 00:00:00

項(xiàng)目CRUD單例模式

2022-07-26 08:40:42

Java并發(fā)工具類

2022-08-02 06:31:32

Java并發(fā)工具類

2022-07-11 10:47:46

容器JAVA

2021-11-25 10:18:42

RESTfulJava互聯(lián)網(wǎng)

2021-08-09 07:47:40

Git面試版本

2025-01-13 09:24:32

2019-12-25 11:22:19

負(fù)載均衡集群算法

2020-12-04 06:27:04

序列化面試官Java

2022-02-21 17:24:18

序列化對象存儲

2022-07-18 14:18:26

Babel代碼面試

2015-08-13 10:29:12

面試面試官

2020-02-28 15:42:26

AOPJDKCGLib

2020-12-01 08:47:36

Java異常開發(fā)

2025-02-21 15:25:54

虛擬線程輕量級

2024-09-27 15:43:52

零拷貝DMAIO

2020-06-12 15:50:56

options前端服務(wù)器

2022-03-21 09:05:18

volatileCPUJava

2025-03-21 00:00:05

Reactor設(shè)計(jì)模式I/O 機(jī)制

2024-10-24 16:14:43

數(shù)據(jù)傳輸CPU零拷貝
點(diǎn)贊
收藏

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