1. 程式人生 > >安卓之外掛化開發使用DexClassLoader&AssetManager來更換面板

安卓之外掛化開發使用DexClassLoader&AssetManager來更換面板

這篇文章主要使用DexClassLoader來實現外掛化更換面板,即將面板獨立出來做成一個面板外掛apk,當用戶想使用該面板時需下載(不需要安裝)對應的面板外掛apk

效果圖

這裡寫圖片描述

【為方便測試,主要通過改變背景圖來簡單地展示面板更換】

一、DexClassLoader

如果使用DexClassLoader來實現外掛化面板更換,我們需要去下載(不需安裝)我們的面板外掛apk:

  1. DexClassLoader 可以載入外部的 apk、jar 或 dex檔案,並且會在指定的 outpath 路徑存放其 dex 檔案。

  2. 建構函式:

    DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)

    dexPath:需被解壓的apk路徑,不能為空。

    optimizedDirectory:解壓後的.dex檔案的儲存路徑,不能為空。這個路徑強烈建議使用應用程式的私有路徑,不要放到sdcard上,否則程式碼容易被注入攻擊。

    libraryPath:c/c++庫的路徑,可以為null,若有相關庫,須填寫。

    parent:父親載入器,一般為context.getClassLoader(),使用當前上下文的類載入器。

  3. 下面為什麼要使用到擴充套件DexClassLoader?:

    這裡使用DexClassLoader是為了載入 外掛apk 中的dex檔案,載入dex檔案後系統就可以在dex中找到我們要使用的class類R.java,在R.java中包含著資源等的id,通過id我們可以獲取到資源。

二、主應用apk的邏輯

  1. 為了方便測試,我們將外掛apk存放在SD卡中,主應用apk再去獲取。所以在主應用中需要讀寫SD卡內容的許可權:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    
  2. 使用SharedPreferences來記錄面板的改變:

    SharedPreferences skinType; 
    
    skinType = getPreferences(Context.MODE_PRIVATE);
    String skin = skinType.getString("skin",null);
    
    if(skin!=null) installSkin(skin);
    
  3. 按鈕點選事件的響應:

    public void changeSkin1(View view) {
        installSkin("dog");
    }
    
    public void changeSkin2(View view) {
        installSkin("girl");
    }
    
  4. 所以我們更好面板的重點在installSkin函式中:

    public void installSkin(String skinName) {
    
        // 通過面板名字查詢面板apk是否存在,這裡需要注意面板apk的命名
        // 存在則返回路徑,否則返回null
        String apkPath = findPlugins(skinName);
    
        if (apkPath == null) {
            // 面板不存在時(可以靜默下載面板)
            Toast.makeText(this, "請先安裝面板", Toast.LENGTH_SHORT).show();
    
            // 面板外掛被刪除的情況,清空儲存
            if (skinType.getString("skin", skinName).equals(skinName))
                skinType.edit().clear().commit();
    
        } else {
             // 面板apk存在,獲取其包名。注意包名與面板名的關係
             String apkPackageName = "com.cxmscb.cxm."+skinName;
    
            /**
            *通常我們獲取Resoueces物件使用的是context.getResources
            *但我們無法獲取面板apk的context(因為面板apk沒有安裝)
            *在這裡通過獲取載入外掛apk的AssetManager來獲取外掛apk的Reources物件
            */
            Resources resources = getSkinApkResource(this,apkPath);
    
            // 獲取背景圖片的id
            int bgId = getSkinBackgroundId(apkPath,skinName,apkPackageName);
    
            //通過外掛的Resources物件和id獲取背景圖片
            rl.setBackgroundDrawable(resources.getDrawable(bgId));
    
            //儲存記錄
            skinType.edit().putString("skin",skinName).commit();
    
        }
    }
    
  5. 上面我們是通過findPlugins(String plugName)來查詢面板外掛apk是否存在:

    private String findPlugins(String plugName) {
    
        String apkPath = null;
    
        // 獲取apk的路徑 (為方便測試:將apk存放在SD卡的根目錄下)
        apkPath = Environment.getExternalStorageDirectory()+"/"+ plugName+".apk";
    
        //面板apk存在時,才返回路徑
        File file = new File(apkPath);
    
        if (file.exists()) {
    
            return apkPath;
        }
    
    
        return null;
    }
    
  6. 上面我們是通過getSkinApkResource(this,apkPath)來獲取外掛apk的Resources物件的。接下來是對獲取Resources物件的原始碼追蹤:

    a. 通常獲取資源時使用getResource獲得Resource物件,通過這個物件我們可以訪問相關資源。通過跟蹤原始碼發現,其實 getResource 方法是Context的一個抽象方法。

    /** Return a Resources instance for your application's package. */
    public abstract Resources getResources();
    

    b. 而getResource的具體實現是在ContextImpl類(Context的實現類)中實現的,獲取的Resource物件是應用的全域性變數mResource。

    public Resources getResources(){
        return mResources;
    }
    

    c. 然後繼續跟蹤ContextImpl類中的全域性變數mResource如何實現,發現 mResources 由一個LoadApk物件packageInfo來建立。

    Resources resources = packageInfo.getResources(mainThread);
    

    接著繼續跟蹤LoadApk這個類中的getResources方法:

    public Resources getResources(ActivityThread mainThread){
    
        if(mResources==null){
                mResources = mainThread.getTopLevelResources(mResDir,mSplitResDirs....)
        }
    
        return mResources;
    }
    

    d. 接著繼續跟蹤ActivityThread這個類中的getTopLevelResources方法發現呼叫的是ResourcesManager類的getTopLevelResources方法。於是繼續追蹤該方法:在這個方法中,有一個Resources物件的弱引用,當弱引用物件被釋放掉時會重新呼叫r = new Resources(assets, dm, config); 來建立Resources物件再放入虛引用中。

    其中AssetManager物件 assets引數 載入了應用的apk路徑:assets.addAssetPath(resDir) ,其中resDir為apk的路徑。dm, config引數可以分別為手機的螢幕資訊和手機的配置資訊。

    為此我們可以通過 new Resources(assets, dm, config)來建立載入面板外掛apk資源的Resources

    //ResourcesManager
    
    public Resources getTopLevelResources(String resDir, int displayId,  
            Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {  
        final float scale = compatInfo.applicationScale;  
        ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale,  
                token);  
        Resources r;  
        synchronized (this) {  
            // Resources is app scale dependent.  
            if (false) {  
                Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);  
            }  
            WeakReference<Resources> wr = mActiveResources.get(key);  
            r = wr != null ? wr.get() : null;  
            //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());  
            if (r != null && r.getAssets().isUpToDate()) {  
                if (false) {  
                    Slog.w(TAG, "Returning cached resources " + r + " " + resDir  
                            + ": appScale=" + r.getCompatibilityInfo().applicationScale);  
                }  
                return r;  
            }  
        }  
    
        //if (r != null) {  
        //    Slog.w(TAG, "Throwing away out-of-date resources!!!! "  
        //            + r + " " + resDir);  
        //}  
    
        AssetManager assets = new AssetManager();  
        if (assets.addAssetPath(resDir) == 0) {  
            return null;  
        }  
    
        //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);  
        DisplayMetrics dm = getDisplayMetricsLocked(displayId);  
        Configuration config;  
        boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);  
        final boolean hasOverrideConfig = key.hasOverrideConfiguration();  
        if (!isDefaultDisplay || hasOverrideConfig) {  
            config = new Configuration(getConfiguration());  
            if (!isDefaultDisplay) {  
                applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);  
            }  
            if (hasOverrideConfig) {  
                config.updateFrom(key.mOverrideConfiguration);  
            }  
        } else {  
            config = getConfiguration();  
        }  
        r = new Resources(assets, dm, config);  
        if (false) {  
            Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "  
                    + r.getConfiguration() + " appScale="  
                    + r.getCompatibilityInfo().applicationScale);  
        }  
    
        synchronized (this) {  
            WeakReference<Resources> wr = mActiveResources.get(key);  
            Resources existing = wr != null ? wr.get() : null;  
            if (existing != null && existing.getAssets().isUpToDate()) {  
                // Someone else already created the resources while we were  
                // unlocked; go ahead and use theirs.  
                r.getAssets().close();  
                return existing;  
            }  
    
            // XXX need to remove entries when weak references go away  
            mActiveResources.put(key, new WeakReference<Resources>(r));  
            return r;  
        }  
    }  
    

    e. 因此我們可以通過 new Resources(assets, dm, config)來建立載入面板外掛apk資源的Resources

    private Resources getSkinApkResource(Context context, String apkPath) {
    
        // 獲取載入外掛apk的AssetManager
        AssetManager assetManager = createSkinApkAssetManager(apkPath);
    
        // 建立建立外掛apk資源的Resources物件
        return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
    
    }
    

    f. 建立外掛面板apk資源的Resources需要AssetManager物件,而AssetManager物件無法通過AssetManager類的構造方法來建立,於是採用反射機制來建立,並呼叫addAssetPath載入面板外掛apk路徑:

    private AssetManager createSkinApkAssetManager(String apkPath) {
        AssetManager assetManager = null;
        try {
            assetManager = AssetManager.class.newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        try {
            AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(
                    assetManager, apkPath);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return assetManager;
    }
    
  7. 獲取載入面板外掛apk資源的Resources物件後,我們還需要獲取面板背景圖的id

    private int getSkinBackgroundId(String apkPath, String skinName,String apkPackageName) {
    
        int id = 0;
        try {
    
            /**使用DexClassLoader可以載入未安裝的apk中的dex
            * 構造方法的引數可參考文章前面的介紹
            */
            DexClassLoader dexClassLoader = new DexClassLoader(apkPath,this.getDir(skinName,Context.MODE_PRIVATE).getAbsolutePath(),null,this.getClassLoader());
    
            // 運用反射:在面板外掛R檔案的drawable類中尋找外掛資源的id 
            Class<?> forName = dexClassLoader.loadClass(apkPackageName+".R$drawable");
    
            // 獲取成員變數的值
            for (Field field : forName.getDeclaredFields()) {
    
                // 獲取包含“main_bg"名的資源id
                if (field.getName().contains("main_bg")) {
                    id = field.getInt(R.drawable.class);
                    return id;
                }
    
            }
    
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return id;
    }
    

三、面板外掛apk的邏輯

  1. 注意面板外掛的包名的設定,要與主應用的邏輯一致,可通過面板名獲取到包名。例:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.cxmscb.cxm.girl"
    >   
    
  2. 面板外掛不需要啟動Activity:可以清除Activity、其佈局檔案及其註冊。

  3. 在子程式的drawable中新增面板資原始檔(注意檔名的設定與主應用的邏輯一致)。例:

    這裡寫圖片描述

後續問題:

1.在apk打包後可能會對面板外掛進行混淆,混淆後的資源id會被更換,這樣會導致資源無法被主應用反射到。

2.上述主應用的邏輯並未完整,為了方便演示省去了面板外掛的下載(不需要安裝)

  1. 面板外掛apk最好存放在較私密的地方

參考:

相關推薦

外掛開發使用PathClassLoader動態更換面板

這篇文章主要使用PathClassLoader來實現外掛化更換面板 (將面板獨立出來做成一個面板外掛apk,當用戶想使用該面板時需下載對應的面板外掛) 效果圖: 【主要通過改變

外掛開發使用DexClassLoader&AssetManager更換面板

這篇文章主要使用DexClassLoader來實現外掛化更換面板,即將面板獨立出來做成一個面板外掛apk,當用戶想使用該面板時需下載(不需要安裝)對應的面板外掛apk 效果圖 【為方便測試,主要通過改變背景圖來簡單地展示面板更換】 一、

MP3播放器開發實例(3)進度條和歌詞更新的實現

tac run detail datetime style mem poll() arc call 上一次談了音樂播放的實現,這次說下最復雜的進度條和歌詞更新。因為須要在播放的Activity和播放的Service間進行交互,所以就涉及了Activi

Android外掛開發AMS與應用程式(客戶端ActivityThread、Instrumentation、Activity)通訊模型分析

今天主要分析下ActivityManagerService(服務端) 與應用程式(客戶端)之間的通訊模型,在介紹這個通訊模型的基礎上,再    簡單介紹實現這個模型所需要資料型別。         本文所介紹內容基於android2.2版本。由於Android版本的不同

外掛開發系列三---Android外掛從入門到放棄-最強合集

本人最近研究外掛化, 偶然發現此合集, 按照部分連結的文章實際簡單寫了些demo,受益良多, 覺得確實不錯,特轉載過來,給需要的人。 外掛化涉及的東西很多,所以我們需要多個維度去學習。大概分為5個部分:預備知識、入門、進階、系列、類庫。一步一步深入瞭解外掛的原理。 基礎1.Java 類載入器 類載入

與html混合開發原生與js相互呼叫

原生和html的優缺點就不多說了,有些特定條件下用html頁面可以很方便,也很容易更新和維護,那麼這就涉及到html與安卓原生的互動和通訊。 接下來我要分享的是html呼叫原生的彈窗和位置資訊,安卓原生呼叫JS中的方法。 xml很簡單: <?xml version=

外掛開發---Hook動態代理方式

今天自己來了解下Hook原理,以及在安卓開發中佔有的意義,我們先來理解下什麼是hook呢? hook就是對安卓原始碼、其他apk原始碼,在相應位置找hook點,然後通過反射等操作,來執行自己程式碼,進而達到需要的功能 以下2個截圖就是之前我公司進行的微信

開發app插件有能力的

thread 插件 app插件 1-1 能力 www app player .com %E6%9D%A5%E8%87%AARockPlayer%E8%81%94%E5%90%88%E5%88%9B%E5%A7%8B%E4%BA%BA%E6%9D%A8%E6%AD%A6%E8

[編譯] 6、開源兩個簡單且有用的APP命令行開發工具和nRF51822命令行開發工具

android 關註 eabi ref 文件 不存在 alt stdin vim 星期四, 27. 九月 2018 12:00上午 - BEAUTIFULZZZZ 一、前言 前幾天給大家介紹了如何手動搭建安卓APP命令行開發環境和nRF51822命令行開發環境,中秋這

為何程式用Java開發

因為android的UI層是用java的類封裝的,而底層是用c/c++。所以開發UI層(也就是軟體的介面層)時要用java開發,而你要用C++來提高軟體效率的話,需要使用jni,通過jni,在java中可以去呼叫c++程式。  選擇Java肯定是google經過深思熟慮的抉擇,

[Songqw.Net 基礎]WPF實現簡單的外掛開發

原文: [Songqw.Net 基礎]WPF實現簡單的外掛化開發 接著上一篇部落格, 那裡實現了簡單的控制檯載入外掛,在這裡通過WPF實現,做個備份. WPF控制元件空間經常會與WinFrom混淆,要記得WPF控制元件是引用 using System.Windows.Co

明日後手遊版今日10點上線!啊,一起來擼狗啊

等了大半年,從T恤等到棉襖,從蘋果等到安卓,明日之後今天上午10點終於上線啦。這款和絕地求生:刺激戰場、王者榮耀都不同型別的末日生存手游到底怎麼樣?畢竟型別和方舟:生存進化差不多,方舟在國外也是大火,還是非常期待的。不管怎麼樣,先牽著你家的狗狗,來跟我一起擼擼,看看各位玩家對明日之後的評價,是否值得

Android.mk編寫

generated sin efault print avi out ram https 個人 題記:編譯環境可以參考https://www.cnblogs.com/ywjfx/p/9960817.html 不管是寫C還是java,我想所有的程序員都經歷過HelloWorl

Android.mk多檔案以及動態庫編譯

1、多檔案編譯 多檔案編譯共有兩種方式:   (1) 在Android.mk中一一新增       LOCAL_PATH:= $(call my-dir) #定義當前模組的相對路徑       include $(CLEAR_VARS)      #清空當前環境變數       LOCAL_MO

Android.mk多文件以及動態庫編譯

pat 靜態 include 環境 一個 path table and uil 1、多文件編譯 多文件編譯共有兩種方式:   (1) 在Android.mk中一一添加       LOCAL_PATH:= $(call my-dir) #定義當前模塊的相對路徑     

[編譯] 6、開源兩個簡單且有用的APP命令列開發工具和nRF51822命令列開發工具

星期四, 27. 九月 2018 12:00上午 - BEAUTIFULZZZZ 一、前言 前幾天給大家介紹瞭如何手動搭建安卓APP命令列開發環境和nRF51822命令列開發環境,中秋這幾天我把上面篇文章的操作流程全部做成了shell指令碼,使得可以讓其他人簡單執行下指令碼、就能夠直接建立綠色開發環境,豈

筆記元件開發和元件管理工具composer

(1)元件化開發 一個元件可以釋出供別人使用,也可以使用別人釋出的元件快速構建專案,更換元件而不需修改系統其他部分的程式碼。 laravel底層使用了很多symfony框架的元件。 (2)如何實現元件化開發 composer,元件管理工具 (3)composer compos

入門系列-01開發工具Android Studio的安裝

谷歌在早幾年就關閉了第三方支援,現在官方主推的開發工具就是Android Studio,所以我的安卓開發也是從as開始的。 1.下載IDE 像安卓這類開發,它不同於其他的程式設計開發,一個好的工具是必須的。Android Studio經過幾年的逐步發展,如今已經是比較好用

佈局總結

 Adroid佈局 有人形象地比喻,Android開發中的佈局就相當於一棟建築的外觀架構。佈局用得好,這棟建築的外觀才美觀高大上。   Android佈局管理器 Android佈局管理器本身是一個介面控制元件,所有的佈局管理器都是ViewGroup類的子類,都是可以當做容器類來使用

外掛開發---DroidPlugin對廣播的管理

回想一下我們日常開發的時候是如何使用BroadcastReceiver的:註冊, 傳送和接收;因此,要實現BroadcastReceiver的外掛化就這三種操作提供支援;接下來我們將一步步完成這個過程。 我們可以註冊一個BroadcastReceiver然後接