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,這樣我們可以在插件未安裝的情況下就去訪問它的資源,那么根據這個,我們做一些更換主題,皮膚等等就有了解決方法。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容