RePlugin 關于插件管理

RePlugin GitHub 主頁

RePlugin Wiki 主頁

RePlugin Wiki 插件的管理

RePlugin 原理剖析

全面插件化:RePlugin 的使命


大致目錄:

* 前言

* 外置插件

  * 安裝插件

  * 升級插件

  * 卸載插件

* 內置插件

  * 添加內置插件

  * 刪除內置插件

  * 使用內置插件的時機

  * 內置插件的升級

* 預加載插件

* 插件的運行

* 安全與簽名校驗

* 插件管理進程

* 插件的目錄結構

一、前言

無論是插件還是主程序,都可以對自己和其它插件做相應的插件管理工作。但需要理解的是:不是所有的 APK 都能作為 RePlugin 的插件并安裝進來的。必須要嚴格按照《插件接入指南》中所述完成接入,其編譯出的 APK 才能成為插件,且這個 APK 同時也可以被安裝到設備中。

二、外置插件

外置插件是指可通過“下載”“放入SD卡”等方式來安裝并運行的插件。以下是外置插件的管理方案:

2.1 安裝插件

要安裝一個插件,只需使用 RePlugin.install() 方法,傳遞一個 APK 路徑即可。

RePlugin.install("/sdcard/exam.apk");
注意
  • 無論安裝還是升級,都會將源文件移動(而非復制)到插件的安裝路徑(如 app_p_a)上,這樣可大幅度節省安裝和升級時間,但顯然的,源文件也就會消失。

    • 若想改變這個行為,你可以參考 RePluginConfig 中的 setMoveFileWhenInstalling() 方法

    • 升級插件和此等同,故不再贅述

2.1.1 最佳實踐

以下為 360 手機衛士或其它合作 App 采用的設計,可供你參考:

  • 除非是基礎和核心功能插件,否則請盡量減少“靜默安裝”(指的是用戶無感知的情況下,偷偷在后臺安裝)插件的情況,以減少內部存儲空間的消耗,降低對用戶的影響。

  • 若插件需要下載,則請覆寫 RePluginCallbacks.onPluginNotExistsForActivity() 方法,并在此打開你的下載頁面并控制其邏輯

  • 下載插件前建議告知用戶其插件大小,尤其針對運營商網絡的情況

有關“插件下載”的處理,以及針對插件安裝失敗原因做進一步的操作,請閱讀《自定義您的 RePlugin》“在插件不存在時,提示下載”一節。

2.1.2 安裝或升級失敗?

安裝或升級失敗(返回值為 Null)的原因有如下幾種:

  • 是否開啟了“簽名校驗”功能且簽名不在“白名單”之中?—— 通常在 Logcat 中會出現 “verifySignature: invalid cert: ”。如是,則請參考安全與簽名校驗一節,了解如何將簽名加白,或關閉簽名校驗功能(默認為關閉)

  • 是否將 replugin-host-lib 升級到 2.1.4 及以上?—— 在 2.1.3 及之前版本,若沒有填寫 “meta-data”,則可能導致安裝失敗,返回值為 null。官方在 2.1.4 版本中已經修復了此問題(衛士和其它 App 的所有插件都填寫了 meta-data,所以問題沒出現)

  • APK 安裝包是否有問題?—— 請將插件 APK 直接安裝到設備上(而非作為插件)試試。如果在設備中安裝失敗,則插件安裝也一定是失敗的。

  • 是否沒有 SD 卡的讀寫權限?—— 如果你的插件 APK 放到了 SD 卡上,則請務必確保主程序中擁有 SD 卡權限(主程序 Manifest 要聲明,且 ROM 允許),否則會出現權限問題,當然,放入應用的 files 目錄則不受影響。

  • 設備內部存儲空間是否不足?—— 通常出現此問題時其 Logcat 會出現 “copyOrMoveApk: Copy/Move Failed” 的警告。如是,則需要告知用戶去清理手機。

2.2 升級插件

為了簡化操作,升級插件的做法和安裝是一樣的,仍可以直接調用 RePlugin.install() 方法。

RePlugin.install("/sdcard/exam_new.apk");
注意
  • 如果插件正在運行,則不會立即升級,而是“緩存”起來。直到所有“正在使用插件”的進程結束并重啟后才會生效

  • 升級可能會占用內部存儲空間(因為要釋放新的 APK)

  • 不支持“插件降級”,但可以“同版本覆蓋”(在 RePlugin 2.1.5 版本中開始支持)

出于穩定性和實際需求考慮,RePlugin 暫時沒有計劃支持“熱修復”方案。

2.2.1 最佳實踐

以下為 360 手機衛士或其它合作 App 采用的設計,可供你參考:

  • 大部分情況下,應盡可能“靜默升級”,以減少對用戶的打擾

  • 針對升級而言,可在后臺線程做一次“預加載”,提前釋放 Dex。具體做法:

PluginInfo pi = RePlugin.install("/sdcard/exam_new.apk");
if (pi != null) {
    RePlugin.preload(pi);
}
  • 若插件正在運行,則會有兩種場景,需分別對待:

    • 若是遇到嚴重問題,需要“強制升級”,則應立即提示用戶,待同意后則重啟進程

    • 通常情況下,建議在“鎖定屏幕”后重啟進程,讓其在后臺生效

  • 若插件沒有運行,則可直接升級

2.3 卸載插件

要卸載插件,則需要使用 RePlugin.uninstall() 方法。只需傳遞一個“插件名”即可。

RePlugin.uninstall("exam");
注意
  • 如果插件正在運行,則不會立即卸載插件,而是將卸載訴求記錄下來。直到所有“正在使用插件”的進程結束并重啟后才會生效

  • 由于內置插件是捆在主程序包內的,故無法卸載“內置插件”(此處有待官方優化)。

出于穩定性和實際需求考慮,RePlugin 暫時沒有計劃支持“熱卸載”方案。

2.3.1 最佳實踐

以下為 360 手機衛士或其它合作 App 采用的設計,可供你參考:

  • 在卸載時彈出對話框,提示用戶“是否同意卸載”

  • 若插件在運行時需要被卸載,則有兩種做法:

    • 提示用戶“需要重新啟動應用才能生效”

    • 在“鎖定屏幕”后重新啟動進程,讓其在后臺生效

  • 若插件沒有運行,則可以直接卸載,無需提示用戶

三、內置插件

內置插件是指可以“隨著主程序發版”而下發的插件,通常這個插件會放到主程序的 Assets 目錄下。

針對內置插件而言,開發者無需調用安裝方法,由 RePlugin來“按需安裝”。

“內置插件”是可以被“升級”的。升級后的插件等同于“外置插件”。

3.1 添加內置插件

添加一個內置插件是非常簡單的,甚至可以無需任何 Java 代碼。只需兩步即可:

  • 將 APK 改名為:[插件名].jar[ ] 在實際上不需要添加)

  • 放入主程序的 assets/plugins 目錄

這樣,當編譯主程序時,RePlugin 的“動態編譯方案”會自動在 assets 目錄下生成一個名叫 “plugins-builtin.json” 文件,記錄了其內置插件的主要信息,方便運行時直接獲取。

必須改成 “[插件名].jar” 后,才能被 RePlugin-Host-Gradle 識別,進而成為“內置插件”。

[插件名] 可以是“包名”,也可以是“插件別名”。有關這方面的說明,請閱讀《插件的信息》“插件命名”一節。

3.2 刪除內置插件

刪除內置插件非常簡單,直接移除相應的 Jar 文件,其余均交給 RePlugin 來自動化完成。

注意:若用戶已使用了內置插件,則即便用戶升級主程序,其包內已不帶這個內置插件,但用戶仍可繼續使用它

這樣可防止出現“用戶升級主程序后,發現內置插件突然用不了”的情況。

3.3 使用內置插件的時機

不同于“外置插件”需要先調用 RePlugin.install 方法后才能使用,內置插件可無需調用此方法。而一旦插件被使用,則 RePlugin 會在觸發相應邏輯前,為你做下列操作:

  • 將內置插件釋放到數據目錄下(近似于調用 install() 方法)

  • 若需要加載 Dex,則還會釋放優化后的 Dex 到數據目錄下,這可能會需要一些時間

這樣做的好處是,不會占用太多的“內部存儲空間”,畢竟不是所有內置插件,都一定會被用到。

3.4 內置插件的升級

內置插件的升級分為兩種情況:主程序隨包升級、通過 install() 方法升級

  • 主程序隨包升級:當用戶升級了帶“新版本內置插件”的主程序時,則 RePlugin 會在使用插件前先做升級

  • 通過 install() 方法升級:若通過 RePlugin.install() 方法做的升級(大多為用戶從服務器上下載并更新),則 RePlugin 在調用 install() 方法時開始做升級。當然,其規則仍遵循安裝插件的規則,例如“插件運行時先不覆蓋”等。

值得注意的是,無論采用何種方式,均“不支持降級”,但支持“同版本覆蓋”升級,也即:

  • 內置插件:只要 APK 的時間戳和大小發生變化就升級,若兩者均無變化,則不會升級。(在 RePlugin 2.1.5 版本中開始支持)

  • 外置插件:只要調用 RePlugin.install() 方法即可將“內置插件”轉化為“外置插件”。同樣的,需遵循安裝插件規則。

3.5 最佳實踐

以下為 360 手機衛士或其它合作 App 采用的設計,可供你參考:

  • 需控制“內置插件”的數量,因為會占用主程序 APK 的大小

  • 比較適合成為“內置插件”的有:

    • 核心業務插件:沒有它就等于“核心功能缺失”。比如 360 手機衛士的“首頁體檢”、“清理”插件等

    • 基礎插件:各插件都需要用到,且為必須的。比如“安全 WebView”、“下載”插件等

    • 啟動時必備插件:明確要在啟動時要用到的功能。比如 360 手機衛士的 “Push”、“常駐服務管理”等

  • 可將一些啟動時必須要加載的,以及經常要用到的內置插件做一次“預加載”。具體做法:

RePlugin.preload("exam");

四、預加載插件

什么是預加載?一言以蔽之,就是將插件的 Dex “提前做釋放”,并將 Dex 緩存到內存中,這樣在下次啟動插件時,可無需走 dex2oat 過程,速度會快很多。

預加載不會做下列事情:
  • 不會“啟動插件”

  • 不會加載其 Application 對象

  • 不會打開 Activity 和其它組件等。

換言之,預加載的目的非常單純,就是提前釋放 Dex,僅此而已。

4.1 預加載的用法

如之前所述,預加載有兩種做法:

  • 預加載當前安裝的插件
    此為絕大多數用到的場景。直接預加載當前安裝的插件即可,如果當前正在運行這個插件,則調用此方法則是無效的,畢竟當前插件已經早就被使用過了。

可使用 RePlugin.preload(pluginName),例如:

RePlugin.preload("exam");
  • 預加載新安裝的插件
    此場景主要用于“后臺升級某個插件”。如果此插件“正在被使用”,則必須借助 RePlugin.install() 方法的返回值(新插件的信息)來做預加載。

可使用 RePlugin.preload(PluginInfo),例如:

PluginInfo pi = RePlugin.install("/sdcard/exam_new.apk");
if (pi != null) {
    RePlugin.preload(pi);
}

4.2 最佳實踐

以下為 360 手機衛士或其它合作 App 采用的設計,可供你參考:

  • 建議將 RePlugin.preload() 方法的調用放到“工作線程”中進行。由于此方法是“同步”的,所以直接在 UI 線程中調用時,可能會卡住,甚至導致 ANR 問題。

  • 如果正在 preload 某插件,則無論在哪個進程和線程,在過程中加載這個插件時,可能會出現卡頓,這和為了安全起見,做了進程鎖有關。建議在 preload 做完后再打開此插件。

五、插件的運行

插件運行的場景有很多,包括:

  • 打開插件的四大組件

  • 獲取插件的 PackageInfo/Context/ClassLoader

  • 預加載(preload)

  • 使用插件 Binder

如果想判斷插件是否在運行,可使用 RePlugin.isPluginRunning() 方法。

六、安全與簽名校驗

作為一家安全公司旗下的開源項目,其“安全性”是作為其重點之一來考慮的。曾經有幾個 App 在使用動態加載 Dex 方案(非 RePlugin)時,被爆出有可能攜帶“病毒”,經追查發現是由于沒有對外來的 Dex 和 Apk 做“校驗”導致。所以說,一旦不做校驗,則不排除惡意人會劫持 DNS 或網絡,并通過網絡來下發惡意插件,對你的應用造成很不好的影響。

若開啟此開關,則一旦簽名校驗失敗,則會在 Logcat 中提示 “verifySignature: invalid cert”,且 install() 方法返回 null

此外,出于性能考慮,內置插件無需做“簽名校驗”,僅“外置插件”會做。

打開簽名校驗也是非常簡單的。只需兩步:

第一步:打開開關

例如,若你繼承 RePluginApplication,則請在創建 RePluginConfig 時調用其 setVerifySign(true) 即可。

當然,更推薦的做法是傳遞 !BuildConfig.DEBUG 參數。這表示:若為 Debug 環境下則無需校驗簽名,只有 Release 才會校驗。以下是具體用法:

    @Override
    protected RePluginConfig createConfig() {
        RePluginConfig c = new RePluginConfig();
        c.setVerifySign(!BuildConfig.DEBUG);
        ...
        return c;
    }

如果你是“非繼承式”,則需要在調用 RePlugin.App.attachBaseContext() 的地方,傳遞RePluginConfig,并設置 setVerifySign 即可。以下是具體用法:

RePluginConfig c = new RePluginConfig();
c.setVerifySign(!BuildConfig.DEBUG);
...
RePlugin.App.attachBaseContext(context, c);

自 RePlugin 2.1.4 版本開始,默認將“關閉”簽名校驗,之前默認為“開啟”。

第二步:加入合法簽名

光是打開其開關還是不夠的,還應該將“合法的簽名”加入到 RePlugin 的“白名單”中,可調用 RePlugin.addCertSignature() 來完成。例如:

// Add signature to "White List"
RePlugin.addCertSignature("379C790B7B726B51AC58E8FCBCFEB586");

其中,其參數傳遞的是簽名證書的 MD5,且去掉“:”’。

請務必去掉“:”,且不要傳遞 SHA1 或其它非簽名 MD5 內容

獲取簽名的做法有很多,比較推薦的是使用 keytool 工具,可參見此文檔的介紹

出于性能考慮,RePlugin 不會自動將“主程序簽名”加入進來。如有需要,建議你自行加入。

6.1 最佳實踐

以下為 360 手機衛士或其它合作 App 采用的設計,可供你參考:

  • 強烈建議開啟安全和簽名校驗

  • 若在調用 install() 方法前就已對 APK 做了校驗(例如,手機衛士是云控加密 MD5 + V5 簽名校驗),則可關閉,以避免重復校驗

  • 請盡量不要使用和“主程序”一樣的簽名,而是單獨創建一個

七、插件管理進程

由于 RePlugin 支持獨特的“跨進程安全通訊”(見 IPC 類)以及復雜的插件管理機制,為保證插件能統一由“一個中心”來管理,提高每個進程的啟動、運行速度,官方團隊在設計 RePlugin 之初,就設計了一個“插件管理進程”,所有插件、進程等信息均在此進程中被記錄,各進程均從此中獲取、修改等,而無需像其它那樣,要求“每個進程各自初始化信息”。RePlugin 的這種做法有點像 AMS

7.1 目前我們有兩種進程可以作為“插件管理進程”:

7.1.1 以“常駐進程”作為“插件管理進程”(默認)

在 RePlugin 2.1.7 及以前版本,這是唯一的方式。RePlugin 默認的“常駐進程”名為“:GuardService”,通常在后臺運行,存活時間相對較久。這樣的最大好處是:應用“冷啟動”的概率被明顯的降低,大部分都變成了“熱啟動”,速度更快。

適合作為常駐進程的場景包括:
  • 以后臺服務為主要業務的應用,例如:手機安全類、健身和健康監控類、OS 內應用等

  • 需要有常駐通知欄的應用,例如:音樂類、清理類等

  • 需保持常連接(例如 Push 等)的應用,如:即時通訊類、泛社交類等

目前市面上多數應用都集成了推送功能(例如友盟、極光推送),常駐進程可以掛載在那里。

優點,這是結合“常駐進程”長期存活的特點而展開的:
  • 各進程啟動時,插件信息的獲取速度會更快(因直接通過 Binder 從常駐進程獲取)

  • 只要常駐進程不死,其它進程殺掉重啟后,仍能快速啟動(熱啟動,而非“冷啟動”)

如果做得好的話,甚至可以做到 “0 秒啟動”,如 360 手機衛士。

缺點:
  • 若應用為“冷啟動”(無任何進程時啟動),則需要同時拉起“常駐進程”,時間可能有所延長

  • 若應用對“進程”數量比較敏感,則此模式會無形中“多一個進程”

7.1.2 以“主進程”作為“插件管理進程”

和“常駐進程”不同的是,自 RePlugin 2.2.0 開始,主進程也可以作為“插件管理進程”。這樣做的最大好處是:應用啟動時,可以做到“只有一個進程”(注意,這不代表你不能開啟其它插件進程,這里只是說沒有“常駐進程”了而已)。當然,代價是享受不到“常駐進程”時的一些好處。

從適用場景上來看,只要是不符合上述“常駐進程”中所涉及到的場景的,本模式都適合。

優點:
  • 無需額外啟動任何進程,例如你的應用只有一個進程的話,那采用此模型后,也只有一個進程

  • 應用冷啟動(無任何進程時啟動)的時間會短一些,因為無需再拉起額外進程

缺點:

“冷啟動”的頻率會更高,更容易被系統回收,再次啟動的速度略慢于“熱啟動”

7.2 如何使用?

若不設置,則默認是以“常駐進程”作為“插件管理進程”。

如需切換到以“主進程”作為“插件管理進程”(也即不產生額外進程),則需要在宿主的 app/build.gradle 中添加下列內容,用以設置 persistentEnable 字段為 False

apply plugin: 'replugin-host-gradle'
repluginHostConfig {
    // ... 其它RePlugin參數

    // 設置為“不需要常駐進程”
    persistentEnable = false
}

八、插件的目錄結構

無論是內置插件,還是外置插件,為了保證穩定性,RePlugin 會把經過驗證的插件放到一個特殊的目錄下,以防止“源文件”被刪除后的一些問題。

由于歷史原因,內置插件和外置插件的存放路徑略有不同。以下將分別予以說明。以下為簡化起見,將 “/data/data/[你的主程序包名]” 統一簡化成“主程序路徑”:

外置插件(未來將只有這一種目錄):

  • APK 存放路徑:主程序路徑/app_p_a

  • Dex 存放路徑:主程序路徑/app_p_od

  • Native 存放路徑:主程序路徑/app_p_n

  • 插件數據存放路徑:主程序路徑/app_plugin_v3_data

內置插件 & 舊 P-N 插件(未來將等同于外置插件):

  • APK 存放路徑:主程序路徑/app_plugin_v3

  • Dex 存放路徑:主程序路徑/app_plugin_v3_odex

  • Native 存放路徑:主程序路徑/app_plugin_v3_libs

  • 插件數據存放路徑:主程序路徑/app_plugin_v3_data

8.1 文件的組織形式

  • 外置插件:為了方便使用,插件會有一個 JSON 文件,用來記錄所有已安裝插件的信息。目前位于 “主程序路徑/app_p_a/p.l” 中。有興趣的朋友可以自行打開此文件來閱覽其中內容。

  • 內置插件:不同于外置插件,內置插件 的 JSON 文件只存放于主程序 “assets/plugins-builtin.json” 文件下。每次會從那里獲取信息。

官方計劃將“內置插件”的管控做到和“外置插件”的一致。屆時兩者的管理將變得統一起來。

  • P-N 插件(即將廢棄):由于歷史原因,P-N 插件不采用記錄 Json 的形式,而是在 “主程序路徑/files” 下,檢索所有 “p-n-” 開頭,且末尾為 “.jar” 的文件,并讀取其內容頭,進而找到插件的信息,并記錄到內存中。由于該方案即將廢棄(雖然截止到 2017 年 7 月,衛士多數插件仍然在用,同樣穩定),故這里不再贅述。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內容

  • 接入比較簡單,只需按官方給出的文檔即可 宿主接入指南 插件接入指南 文檔都非常詳細,在這我要說的是一些文檔上面沒有...
    ctrun閱讀 2,112評論 0 0
  • RePlugin的開源地址:https://github.com/Qihoo360/RePlugin官方介紹:ht...
    JarryWell閱讀 10,882評論 6 32
  • 一、RePlugin簡介 RePlugin是一套完整的、穩定的、適合全面使用的,占坑類插件化方案。我們“逐>詞”拆...
    阿犇專用閱讀 3,969評論 1 17
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,686評論 25 708
  • 做了一個夢 ,像是孽緣久久不忘,就記下吧。 夢里時間、地點不知,背景只是一座基底很寬大的山。山勢高低起伏綿延千里,...
    大圣歸去來兮閱讀 195評論 0 0