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