1. 程式人生 > >【Android 熱修復與外掛化 一】帶你入門Android外掛化(附demo)

【Android 熱修復與外掛化 一】帶你入門Android外掛化(附demo)

本文為博主Colin原創文章,歡迎轉載。 https://blog.csdn.net/colinandroid/article/details/79431502

 

一. 背景
Android外掛化作為每個合格的Android程式設計師都必須會的技術,被各大廠廣泛使用。隨著各大廠對移動網際網路的壟斷,我們漸漸發現app整合的功能越來越多。比如如下幾個app(攜程、淘寶、支付寶): 
   
可以看到每一個app都被集成了無數的功能入口,就拿淘寶來說,“天貓”、“外賣”、“飛豬”、“拍賣”,這任何一個入口都其實是一個app,只不過被整合到“淘寶”這個入口裡了。如果沒有外掛化技術,很難想象淘寶app的size會有多大。很可能有幾個GB!!

再來看看支付寶,可以發現支付寶中提供了很多第三方app的入口,而點選這些入口跳轉的也都是native頁面。應用市場上的支付寶app一共只有二三十MB,而如果這些app都整合到支付寶中,那支付寶的size就不是二三十MB了,那就是二三十GB了!!

本篇blog的主題是介紹Android外掛化技術,並且會提供一個仿支付寶外掛化技術的demo,告訴你支付寶是如何把一個第三方app作為外掛整合到自己的app裡的。

二. 外掛化好處
宿主和外掛分開編譯 
編譯時只需要編譯宿主app,外掛app是在編譯好後下發到宿主app裡的。
併發開發 
宿主app什麼時候釋出版本跟外掛app什麼時候開發完沒有關係,宿主app只要開發完並且為外掛app提供一個入口就可以了。
動態更新外掛 
外掛app在開發完後下發到宿主app裡,點選相應的入口就可以跳轉到最新版的外掛app了。
按需下載模組
解決方法數或變數數爆棚(65536)
三. 隨便一個app都能整合到支付寶嗎?
答案是:不能! 
我們來思考,支付寶要跳轉到一個外掛的Activity,而外掛是沒有被安裝的,它沒有上下文,也就沒有生命週期,那麼外掛Activity的生命週期就要由宿主app來控制。為此,我們需要建立一套標準。

四. 外掛化程式結構
我們先來看下外掛化程式結構,瞭解下其大致框架,對程式有巨集觀感受。下面我們直接開始擼程式碼。 


五. 動態載入apk
1. 外掛app的activity沒有在宿主app中註冊,該怎麼辦?
插樁,一個空的Activity,專門用來載入外掛app中的activity,這個Activity叫ProxyActivity,後面我會具體去講這個空Activity該如何實現。我們只需要在宿主app裡註冊這個Activity就可以了。 


2. 載入外掛app中的Activity
實際場景中外掛apk肯定是由服務端下發後,儲存到SD卡的某個資料夾下。這裡將編譯好的外掛apk放到手機外接SD卡的根目錄中,我們來演示宿主app如何去載入外掛app中的Activity。 


六. 資源載入
接下來我們來看下FluginManager的loadPath方法如何實現。如果要實現這個功能,首先想到的肯定是用反射。 
 
可是你別忘記了,外掛app根本就沒有安裝,這裡是無法找到這個Class的。我們需要DexClassLoader來完成Activity類的載入。 
PluginManager的getDexClassLoader的實現如下: 


講完了如何載入Activity,我們來講下如何載入Activity中用到的資原始檔。我們在日常開發中需要資原始檔時,我們是通過getResources()來獲取。例如載入一個圖片:

getResources().getDrawable()
1
可現在我們需要獲取的是另外一個app的資源,所以這裡就需要自己實現一個getResources()方法。 
PluginManager的getResources方法實現如下: 


至此,一個外掛app的activity載入功能就實現完成了,下面我們來看如何跳轉。

七. 跳轉到外掛app中的Activity

由於我們需要讀取SD卡中的外掛apk,這裡別忘記加上SD卡的讀寫許可權 

這就可以實現跳轉了。下面我們來看下效果 
 
奇怪,為什麼我們跳轉到外掛app的activity是空白的?我們來看下外掛app的activity應該長什麼樣子。 
 
當然這裡我只在外掛app的主activity裡放了一張圖片,並沒有寫複雜的佈局。可是為什麼我們跳過來的是空白頁呢?我們再看下ProxyActivity的程式碼:

package com.ctrip.pluginapplication

import android.content.res.Resources
import android.os.Bundle
import android.support.v7.app.AppCompatActivity

/**
 * 殼!專門用來載入外掛Activity
 * @author Zhenhua on 2018/3/3.
 * @email [email protected] ^.^
 */
class ProxyActivity : AppCompatActivity() {

    /**
     * 要跳轉的activity的name
     */
    private var className = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        /**
         * step1:得到外掛app的activity的className
         */
        className = intent.getStringExtra("className")
        /**
         * step2:通過反射拿到class,
         * 但不能用以下方式,因為外掛app沒有被安裝!
         */
//        classLoader.loadClass(className)
//        Class.forName(className)


    }

    override fun getClassLoader(): ClassLoader {
        //不用系統的ClassLoader,用dexClassLoader載入
        return PluginManager.getInstance().getDexClassLoader() as? ClassLoader
                ?: super.getClassLoader()
    }

    override fun getResources(): Resources {
        //不用系統的resources,自己實現一個resources
        return PluginManager.getInstance().getResources() ?: super.getResources()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
我們發現,我們這裡還沒有在ProxyActivity裡寫邏輯啊,我們只是得到了外掛app的主activity的name,這時activity還沒有生命週期。?我們接著來實現。我們需要讓ProxyActivity控制外掛app的activity的生命週期,所以我們需要得到外掛app的activity的例項,然後去控制其生命週期:

package com.ctrip.pluginapplication

import android.content.res.Resources
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.ctrip.standard.AppInterface

/**
 * 殼!專門用來載入外掛Activity
 * @author Zhenhua on 2018/3/3.
 * @email [email protected] ^.^
 */
class ProxyActivity : AppCompatActivity() {

    /**
     * 要跳轉的activity的name
     */
    private var className = ""
    private var appInterface: AppInterface? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        /**
         * step1:得到外掛app的activity的className
         */
        className = intent.getStringExtra("className")
        /**
         * step2:通過反射拿到class,
         * 但不能用以下方式
         * classLoader.loadClass(className)
         * Class.forName(className)
         * 因為外掛app沒有被安裝!
         * 這裡我們呼叫我們重寫過多classLoader
         */
        var activityClass = classLoader.loadClass(className)
        var constructor = activityClass.getConstructor()
        var instance = constructor.newInstance()

        appInterface = instance as?AppInterface
        appInterface?.attach(this)
        var bundle = Bundle()
        appInterface?.onCreate(bundle)

    }

    override fun onStart() {
        super.onStart()
        appInterface?.onStart()
    }

    override fun onResume() {
        super.onResume()
        appInterface?.onResume()
    }

    override fun onDestroy() {
        super.onDestroy()
        appInterface?.onDestroy()
    }

    override fun getClassLoader(): ClassLoader {
        //不用系統的ClassLoader,用dexClassLoader載入
        return PluginManager.getInstance().getDexClassLoader() as? ClassLoader
                ?: super.getClassLoader()
    }

    override fun getResources(): Resources {
        //不用系統的resources,自己實現一個resources
        return PluginManager.getInstance().getResources() ?: super.getResources()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
這時我們就可以成功跳轉了。ok,外掛化實現完成。我們來看下效果。 
 
這裡附上demo(點選下載),如有任何疑問可留言提問,博主每天都會檢視。

~~~~華麗麗的分割線:在外掛app中實現更多功能~~~ 
之前我們的外掛app的activity其實就只是載入了一個imageView,我們現在來實現這樣一個功能:“點選ImageView,彈出一個toast”。 
程式碼如下: 

我們來看下效果。。 
然而,點選竟然crash了。我們來貼下錯誤日誌: 

原來我們在外掛Activity中不能用自己的上下文,我們應該用that!! 
 
程式碼已經更新到github上,歡迎下載體驗。
--------------------- 
作者:Colin_Mindset 
來源:CSDN 
原文:https://blog.csdn.net/colinandroid/article/details/79431502 
版權宣告:本文為博主原創文章,轉載請附上博文連結!