安卓之外掛化開發使用DexClassLoader&AssetManager來更換面板
這篇文章主要使用DexClassLoader來實現外掛化更換面板,即將面板獨立出來做成一個面板外掛apk,當用戶想使用該面板時需下載(不需要安裝)對應的面板外掛apk
效果圖
【為方便測試,主要通過改變背景圖來簡單地展示面板更換】
一、DexClassLoader
如果使用DexClassLoader來實現外掛化面板更換,我們需要去下載(不需安裝)我們的面板外掛apk:
DexClassLoader 可以載入外部的 apk、jar 或 dex檔案,並且會在指定的 outpath 路徑存放其 dex 檔案。
建構函式:
DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
dexPath:需被解壓的apk路徑,不能為空。
optimizedDirectory:解壓後的.dex檔案的儲存路徑,不能為空。這個路徑強烈建議使用應用程式的私有路徑,不要放到sdcard上,否則程式碼容易被注入攻擊。
libraryPath:c/c++庫的路徑,可以為null,若有相關庫,須填寫。
parent:父親載入器,一般為context.getClassLoader(),使用當前上下文的類載入器。
下面為什麼要使用到擴充套件DexClassLoader?:
這裡使用DexClassLoader是為了載入 外掛apk 中的dex檔案,載入dex檔案後系統就可以在dex中找到我們要使用的class類R.java,在R.java中包含著資源等的id,通過id我們可以獲取到資源。
二、主應用apk的邏輯
為了方便測試,我們將外掛apk存放在SD卡中,主應用apk再去獲取。所以在主應用中需要讀寫SD卡內容的許可權:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
使用SharedPreferences來記錄面板的改變:
SharedPreferences skinType; skinType = getPreferences(Context.MODE_PRIVATE); String skin = skinType.getString("skin",null); if(skin!=null) installSkin(skin);
按鈕點選事件的響應:
public void changeSkin1(View view) { installSkin("dog"); } public void changeSkin2(View view) { installSkin("girl"); }
所以我們更好面板的重點在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(); } }
上面我們是通過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; }
上面我們是通過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; }
獲取載入面板外掛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的邏輯
注意面板外掛的包名的設定,要與主應用的邏輯一致,可通過面板名獲取到包名。例:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.cxmscb.cxm.girl" >
面板外掛不需要啟動Activity:可以清除Activity、其佈局檔案及其註冊。
在子程式的drawable中新增面板資原始檔(注意檔名的設定與主應用的邏輯一致)。例:
後續問題:
1.在apk打包後可能會對面板外掛進行混淆,混淆後的資源id會被更換,這樣會導致資源無法被主應用反射到。
2.上述主應用的邏輯並未完整,為了方便演示省去了面板外掛的下載(不需要安裝)
- 面板外掛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然後接