Android動態加載學習總結(二):資源訪問

參考資料:《Android開發藝術探索 --任玉剛》 Android中插件開發篇之----應用換膚原理解析

前言:動態加載要解決的三個問題,分別是資源訪問,Activity生命周期管理,類加載器的管理。前面一片文章總結了類加載器的學習,里面介紹了一些東西,對于管理類加載器還沒有涉及。這篇是資源訪問相關的學習,里面有用到類加載器文章中的PathClassLoader,如果對此沒有什么了解的話,可以先去看看介紹Android動態加載學習總結(一):類加載器,本篇博文的Demo來自于博文 Android中插件開發篇之----應用換膚原理解析,對于初步接觸,為了更好的學習,我簡略了一些內容。并且由于DexClassLoader的一些問題,我將博主的類加載方式更改成了PathClassLoader,既然是PathClassLoader,我們知道,這個類加載器只能加載dex文件,和已安裝的apk文件,所以本篇博文的demo是訪問已安裝apk中的資源。

一、資源訪問的問題

動態加載一個插件,如何訪問它的資源?在我們宿主程序中,我們通過R文件訪問資源,但是去訪問插件的資源,明顯是行不通的,我們在宿主程序中并沒有插件的資源。如果只是去解決資源訪問的問題的話,我們的確有方法,比如提前在宿主程序中預置一份,那么我們就需要在一個插件發布的時候將資源復制到宿主程序。能解決資源訪問嗎?肯定可以,但是我們為什么要有插件化技術(動態加載)?為了減小宿主程序apk大小,為了降低宿主程序的更新頻率,那么去復制到宿主程序明顯違背了這項技術最初的目的。

那么我們的解決方案如下:
Context中有兩個與資源有關的抽象方法:

public abstract AssetManager getAssets();
public abstract Resources getResources();

我們需要實現這兩個方法,實現方式如下:

protected void loadResources(String dexPath) {
//關于/assets目錄下的文件,該目錄下的文件不生成ID,如果我們要使用插件中該目錄下的文件,我們需要指定文件的路徑和文件名
 try { 
AssetManager assetManager=AssetManager.class.newInstance();
//我們通過調用AssetManager中的addAssetPath方法,可以將一個apk中資源加載到Resources對象中,而addAssetPath是隱藏API,所以通過反射調用 
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
//我們將APK的路徑傳給這個方法,資源便加載到AssetManager中 

addAssetPath.invoke(assetManager, dexPath); 
mAssetManager = assetManager; 
} catch (Exception e) { 
e.printStackTrace(); 
}  
Resources superRes = super.getResources(); superRes.getDisplayMetrics(); 
superRes.getConfiguration();
//通過AssetManger創建一個新的Resources對象,通過這個對象去訪問插件資源 
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration()); 
mTheme = mResources.newTheme(); 
mTheme.setTo(super.getTheme()); 
} 
@Override 
public AssetManager getAssets() { 
return mAssetManager == null ? super.getAssets() : mAssetManager; 
} 
@Override 
public Resources getResources() { 
return mResources == null ? super.getResources() : mResources; 
} 
@Override public Resources.Theme getTheme() 
{ return mTheme == null ? super.getTheme() : mTheme; 
}
}

二、插件的設計

我們已經知道了解決方案,那么開始插件設計,新建一個工程,命名為ResourcesLoaderApk1。

  • MainActivity
public class MainActivity extends Activity { 
@Override 
protected void onCreate(Bundle savedInstanceState) { 
super.onCreate(savedInstanceState); 
setContentView(R.layout.activity_main);
 }
}
  • 主活動布局activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
xmlns:tools="http://schemas.android.com/tools" 
android:layout_width="match_parent" 
android:layout_height="match_parent" 
tools:context="com.example.gao.resourceloaderapk1.MainActivity" 
android:background="#998877" >
</RelativeLayout>
  • 類UIUtil
public class UIUtil {  
public static String getTextString(Context ctx){ 
//在宿主程序中通過反射調用該方法得到本插件程序中的strings.xml中定義的app_name資源 
return ctx.getResources().getString(R.string.app_name);
 }  
public static Drawable getImageDrawable(Context ctx){ 
//在宿主程序中通過反射調用該方法得到本插件程序中的icon圖片資源 
return ctx.getResources().getDrawable(R.drawable.icon);
 }
}
  • AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
package="com.example.gao.resourceloaderapk1"> 
<application android:allowBackup="true" 
android:label="@string/app_name" 
android:icon="@mipmap/ic_launcher" 
android:theme="@style/AppTheme"> 
<activity 
android:name=".MainActivity"> 
<intent-filter> 
<action android:name="android.intent.action.MAIN"/> 
<category android:name="android.intent.category.LAUNCHER"/> 
</intent-filter> 
</activity> 
</application>
</manifest>

OK,插件設計完成,在模擬器中運行,也就相當于安裝到了模擬器中,我們在Android動態加載學習總結(一):類加載器中已經知道,PathClassLoader可以加載dex文件和已安裝的apk文件(其實是因為已安裝的在cache中有緩存的dex文件),我們進入adb shell ,看看生成的apk的名字是什么(如果多次運行的話,這個名字會變的,建議修改的話,去看看名字變沒有,我們要根據這個名字在宿主程序中進行加載)

  • 進入adb shell,并進入data/app目錄
    這里寫圖片描述
  • ls查看這個目錄下的所有apk,并找到我們插件程序的apk名字
    這里寫圖片描述

    也就是說我們宿主程序中PathClassLoader要加載的apk的路徑是"/data/app/com.example.gao.resourceloaderapk1-1.apk"這個在下面的宿主程序MainActivity的類加載部分中會用到,注意一下。

三、宿主程序的設計

  • BaseActivity(含有資源訪問的解決方案,和上面的代碼一樣)
public class BaseActivity extends Activity { 
protected AssetManager mAssetManager;
//資源管理器 
protected Resources mResources;
//資源 protected Resources.Theme mTheme;
//主題 
@Override 
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 } 
protected void loadResources(String dexPath) {
 try { 
AssetManager assetManager = AssetManager.class.newInstance(); 
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); 
addAssetPath.invoke(assetManager, dexPath); 
mAssetManager = assetManager; 
} catch (Exception e) { 
e.printStackTrace(); 
} 
Resources superRes = super.getResources(); 
superRes.getDisplayMetrics(); 
superRes.getConfiguration(); 
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration()); 
mTheme = mResources.newTheme(); 
mTheme.setTo(super.getTheme()); 
} 
@Override 
public AssetManager getAssets() { 
return mAssetManager == null ? super.getAssets() : mAssetManager; } 
@Override 
public Resources getResources() { 
return mResources == null ? super.getResources() : mResources; 
} 
@Override 
public Resources.Theme getTheme() { 
return mTheme == null ? super.getTheme() : mTheme;
 }
}
  • MainActivity(其中PathClassLoader加載的代碼和第一篇博客差不多,更改了需要加載的apk路徑還有intent,注意AndroidManifext.xml中的action需要跟intent一致)
public class MainActivity extends BaseActivity { 
/** 需要替換主題的控件 
*TextView,ImageView 
*/ 
private TextView textV; 
private ImageView imgV; 
// 更換控件的按鈕 
private Button btnChange; 
//類加載器 
protected PathClassLoader pc1 = null; 
@Override 
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 textV = (TextView)findViewById(R.id.text); 
imgV = (ImageView)findViewById(R.id.imageview); 
btnChange = (Button)findViewById(R.id.btn1); 
//通過點擊按鈕,更新TextView和ImageView兩個控件 
btnChange.setOnClickListener(new View.OnClickListener(){ 
@Override 
public void onClick(View arg0) { 
/**使用PathClassLoader方法加載類*/ 
/**創建一個意圖,用來找到指定的apk:這里的"com.example.gao.resourceloaderapk1"是指定apk中在AndroidMainfest.xml文件中定義的<action name="com.example.gao.resourceloaderapk1"/> */
Intent intent = new Intent("com.example.gao.resourceloaderapk1", null); 
//獲得包管理器 
PackageManager pm = getPackageManager();
List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0);
//獲得指定的activity的信息 
ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;  
//獲得apk的目錄,這個目錄在第二部分插件程序的設計末尾已得到 
final String apkPath = "/data/app/com.example.gao.resourceloaderapk1-1.apk"; 
//native代碼的目錄 
String libPath = actInfo.applicationInfo.nativeLibraryDir; 
//創建類加載器,把dex加載到虛擬機中 
//第一個參數:是指定apk安裝的路徑,這個路徑要注意只能是通過actInfo.applicationInfo.sourceDir來獲取 
//第二個參數:是C/C++依賴的本地庫文件目錄,可以為null 
//第三個參數:是上一級的類加載器 pc1 = new PathClassLoader(apkPath,libPath, MainActivity.this.getClassLoader()); //調用父類的loadResources()方法 
loadResources(apkPath); 
setContent(); 
} 
});
}
private void setContent(){ 
try{
 Class clazz = pc1.loadClass("com.example.gao.resourceloaderapk1.UIUtil"); 
/** * 通過反射調用插件中UIUtil類中的getTextString方法 */ 
Method method = clazz.getMethod("getTextString", Context.class); 
String str = (String)method.invoke(null, this); 
//更改宿主程序的TextView控件內容 textV.setText(str);
 /**通過反射調用插件中UIUtil類中的getImageDrawable方法 */ 
method = clazz.getMethod("getImageDrawable", Context.class); 
Drawable drawable = (Drawable)method.invoke(null, this); 
//更改宿主程序的ImageView內容 imgV.setImageDrawable(drawable); }catch(Exception e){ 
e.printStackTrace(); 
} 
}
}
  • activity_main.xml
<RelativeLayout 
xmlns:android="http://schemas.android.com/apk/res/android" 
xmlns:tools="http://schemas.android.com/tools" 
android:layout_width="match_parent" 
android:layout_height="match_parent"
tools:context="com.example.resourceloader.MainActivity" > 
<LinearLayout  
android:layout_width="match_parent" 
android:layout_height="wrap_content" 
android:orientation="vertical">  
//更改TextView和ImageView的按鈕 
<Button  android:id="@+id/btn1" 
android:layout_width="wrap_content" 
android:layout_height="wrap_content" 
android:layout_marginRight="10dp"
 android:text="主題1"/> 
//要更改的TextView,原顯示“demo” 
<TextView  android:id="@+id/text" 
android:layout_width="wrap_content" 
android:layout_height="wrap_content" 
android:text="demo"/> 
//要更改的ImageView,原顯示"ic_launcher圖片" 
<ImageView  
android:id="@+id/imageview" 
android:layout_width="wrap_content"
 android:layout_height="wrap_content" 
android:layout_marginTop="20dp" 
android:src="@drawable/ic_launcher"/>  
</LinearLayout>
</RelativeLayout>
  • AndroidManifest.xml
<manifest 
xmlns:android="http://schemas.android.com/apk/res/android" 
package="com.example.gao.resourceloading"> 
<application 
android:allowBackup="true" 
android:label="@string/app_name" 
android:icon="@mipmap/ic_launcher" 
android:theme="@style/AppTheme"> 
<activity android:name=".MainActivity"> 
<intent-filter> 
<action android:name="android.intent.action.MAIN"/> 
//注意這個action,MainActivity中Intent那行代碼與這個action保持一致 
<action android:name="com.example.gao.resourceloaderapk1"/>
 <category android:name="android.intent.category.LAUNCHER"/>
 </intent-filter> 
</activity> 
</application>
</manifest>

運行宿主程序,運行結果如下:
這里寫圖片描述

我們點擊一下主題一的按鈕,結果如下:
這里寫圖片描述

結果顯示我們成功訪問了插件程序的資源,TextView的內容由demo更改為了插件程序的app_name,ImageView的圖片由宿主程序的ic_launcher圖片改為了插件程序的icon圖片。在這之中,loadResources(String dexPath)方法起到了訪問插件程序資源的作用,如果我們把MainActivity中按鈕點擊的代碼中的loadResources(apkPath)注釋掉呢?相當于我們只是通過動態加載和反射調用了插件程序的UIUtil類的以下兩個方法
public static String getTextString(Context ctx){ 
return ctx.getResources().getString(R.string.app_name); 
}  
public static Drawable getImageDrawable(Context ctx){ 
return ctx.getResources().getDrawable(R.drawable.icon);
 }

我們并沒有訪問到插件程序的資源。那肯定用宿主程序的資源了,那么app_name,我們宿主程序也有,但是icon這個圖片我們宿主程序并沒有,所以把loadResources(apkPath)方法注釋掉后,我們點擊按鈕,只會更改TextView這個內容,改成我們宿主程序的app_name這個字符串的內容,即ResourcesLoading,而圖片并不會變。結果如下:
這里寫圖片描述

四、總結:

在本文中通過PathClassLoader去動態加載已安裝的apk,有一點限制是需要安裝插件后才可以加載這個apk,當然,我們也可以通過DexClassLoader去加載這個apk,這樣我們可以在插件未安裝的情況下就去訪問它的資源,那么根據這個,我們做一些更換主題,皮膚等等就有了解決方法。

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

推薦閱讀更多精彩內容