Android 通過反射綜合應用-獲取外掛Plugin資源
通過前面的基礎內容,做一個Android 資源更新的外掛應該沒有問題,讀者只要將外掛的apk當做資源包就可以了,需要更新的資源全部打包到外掛包中.
在正式開篇之前,可能很多人在網上查詢,主Host APK和plugin之間還需要設定相同的sharedUserId,但是我下面沒有做這個要求,因為設定了sharedUserId即代表主Host APK和plugin在同一個程序,這樣可以”辨識對方”,主要是方便主Host更加準確的查詢到自己的plugin,其實個人認為可以有必要設,但是單純從技術角度,這個不會有影響,因為在處理時,無論什麼apk(或者dex等)是相同的.
另外,很多網友看到類似的文章
下面我們通過具體的例項看看如何實現.
<1>: 新建一個工程,工程樹如下:
在工程中寫一個介面類.
<2> : 再新建外掛工程,工程樹如下:
<3> : 上面host工程和外掛工程的介面是完全一樣的.具體程式碼如下:
/** * */ package com.oneplus.plugin.interfaces; import android.content.Context; import android.graphics.drawable.Drawable; /** * @author zhibao.liu * @date 2015-11-20 * @company : oneplus.Inc */ public interface PluginInterface { void ConnectToPlugin(); }
現在裡面增加一個測試方法!
<4> : 在外掛工程中新增實現上面介面的類PluginInterfaceImpl.java,程式碼如下:
/** * */ package com.oneplus.oneplusplugin; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.util.Log; import com.oneplus.plugin.interfaces.PluginInterface; /** * @author zhibao.liu * @date 2015-11-20 * @company : oneplus.Inc */ public class PluginInterfaceImpl implements PluginInterface { private final static String TAG="oneplus"; @Override public void ConnectToPlugin() { // TODO Auto-generated method stub Log.i(TAG,"PluginInterfaceImpl from plugin project !"); } }
<5> : Host工程中添加布局如下oneplus_host.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"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".OneplusHostActivity" >
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/oneplus_connect"
android:text="@string/oneplus_checkplugin"/>
</RelativeLayout>
同時新增加一個oneplus_string.xml檔案:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="oneplus_checkplugin">check plugin</string>
</resources>
主工程類OneplusHostActivity.java新增如下:
private void OneplusLoaderPlugin(String intentname,String packagename,Context context){
Intent intent = new Intent(intentname, null);
// package manager
PackageManager pm = getPackageManager();
List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0);
// activity information
ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
// jar in apk direction
String apkPath = actInfo.applicationInfo.sourceDir;
// native code direction
String libPath = actInfo.applicationInfo.nativeLibraryDir;
PathClassLoader pcl = new PathClassLoader(apkPath, libPath,
this.getClassLoader());
try {
Class clazz=pcl.loadClass(packagename);
try {
Object obj=clazz.newInstance();
try {
Method method=clazz.getMethod("ConnectToPlugin", new Class[]{});
method.setAccessible(true);
try {
method.invoke(obj, new Object[]{});
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
其中PathClassLoader類在第一節就介紹了.後面Class clazz=pcl.loadClass(packagename);反射外掛包的類,然後呼叫反射類中的方法.在主工程增加了一個按鈕,新增按鈕事件如下:
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
int resid=v.getId();
switch(resid){
case R.id.oneplus_connect:
OneplusLoaderPlugin(ONEPLUS_PLUGIN_ACTION,ONEPLUS_PLUGIN_PACKAGE_NAME,OneplusHostActivity.this);
break;
default:
break;
}
}
上面兩個常量:
private final static String ONEPLUS_PLUGIN_ACTION="oneplus.action.plugin";
private final static String ONEPLUS_PLUGIN_PACKAGE_NAME="com.oneplus.oneplusplugin.PluginInterfaceImpl";
主Host完成以後,還需要對外掛的manifest檔案進行配置:
刪除application下面的:
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
以及activity標籤下的:
<category android:name="android.intent.category.LAUNCHER" />
因為外掛不需要執行和顯示在桌面上!!!
經過上面的整頓,首先先講plugin的apk安裝到手機裡面,然後將主工程Host執行,執行結果如下:
看到上面的結果,表明Host和Plugin可以”通訊”了,下面看看plugin如何傳遞資源資訊,首先介紹第一種:從外掛包中獲取一張圖片, screen_show_1.png放到plugin工程資源drawable資料夾下.下面是隨便截了一張圖片
<1> : 在介面類中繼續宣告一個方法:
Drawable getImageResource(Context context,String name, String packageName);
<2> : 外掛工程實現上面介面類如下:
@Override
public Drawable getImageResource(Context context, String name, String packageName) {
// TODO Auto-generated method stub
if(context==null){
return null;
}
PackageManager mPm=context.getPackageManager();
try {
Resources res=mPm.getResourcesForApplication(packageName);
int resid=res.getIdentifier(name, "drawable", packageName);
return res.getDrawable(resid);
} catch (NameNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
<3> : 接著在主Host工程中新增下面的方法來獲取圖片資源:
private void OneplusLoaderDrawablePlugin(String intentname,String packagename,Context context){
Intent intent = new Intent(intentname, null);
// package manager
PackageManager pm = getPackageManager();
List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0);
// activity information
ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
// jar in apk direction
String apkPath = actInfo.applicationInfo.sourceDir;
// native code direction
String libPath = actInfo.applicationInfo.nativeLibraryDir;
PathClassLoader pcl = new PathClassLoader(apkPath, libPath,
this.getClassLoader());
try {
Class clazz=pcl.loadClass(packagename);
try {
Object obj=clazz.newInstance();
PluginInterface plugin=(PluginInterface) clazz.newInstance();
Drawable draw=plugin.getImageResource(OneplusHostActivity.this, "screen_show_1", actInfo.packageName);
if(OneplusImage!=null){
OneplusImage.setImageDrawable(draw);
}
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
程式解釋:
int resid=res.getIdentifier(name, "drawable", packageName);
可以查一下getIdentifier的API使用,這個方法將第二個引數改為”value”就可以獲取string,color等值型別的資源,如果改為”layout”,當然就可以獲取佈局資源了,也可以獲取raw目錄下的資源.
執行結果:
上面有一個問題,如果我們的外掛根本沒有安裝,而僅僅放在移動某個目錄下,那麼需要操作才能夠獲取呢?下面講一個更加通用的方法,步驟如下:
<1> : 在主Host工程在增加一個按鈕UI,並且聽見點選事件.
並且增加一個恆量:
private final static String ONEPLUS_PLUGIN_RESOURCE="com.oneplus.oneplusplugin.R$drawable";
另外將OneplusAndroidPlugin.apk push到手機裡面:
因為要訪問sdcard路徑,所以主Host配置檔案中需要加許可權:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<2> : 關鍵程式如下 :
private void OneplusLoaderDrawableFlexPlugin(String resname,String packagename){
String dexPath = Environment.getExternalStorageDirectory().toString()
+ File.separator + "OneplusAndroidPlugin.apk";
File file=new File(dexPath);
if(!file.exists()){
return ;
}
final File optimizedDexOutputPath = getDir("outdex", 0);
DexClassLoader cl = new DexClassLoader(dexPath, optimizedDexOutputPath.getAbsolutePath(), null,
getClassLoader());
try {
Class clazz=cl.loadClass(packagename);
try {
Object obj=clazz.newInstance();
try {
Field field=clazz.getDeclaredField(resname);
Object ret=field.getInt(obj);
//following put plugin apk to resource path so that others can find it
AssetManager aMgr=AssetManager.class.newInstance();
try {
Method method=aMgr.getClass().getDeclaredMethod("addAssetPath", new Class[]{String.class});
try {
method.invoke(aMgr, new Object[]{dexPath});
Resources res=this.getResources();
Resources resouces=new Resources(aMgr,res.getDisplayMetrics(),res.getConfiguration());
int resid=Integer.parseInt(ret.toString());
if(OneplusImage!=null){
OneplusImage.setImageDrawable(resouces.getDrawable(resid));
}
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
程式解釋:
<1>:AssetManager aMgr=AssetManager.class.newInstance();
try {
Method method=aMgr.getClass().getDeclaredMethod("addAssetPath", new Class[]{String.class});
try {
method.invoke(aMgr, new Object[]{dexPath});
… …
由於外掛apk只是push到移動裝置任意目錄下,那麼首先工作是將其增加到系統資源路徑下,這樣可以讓其被其他APP呼叫,因為AssetManager裡面的addAssetPath方法不是對普通應用開發者公開的,所以通過反射將其呼叫,利用這個方法將資源包置於系統資源路徑下.
<2>:Resources res=this.getResources();
Resources resouces=new Resources(aMgr,res.getDisplayMetrics(),res.getConfiguration());
獲取系統資源Resources物件.
<3>:Field field=clazz.getDeclaredField(resname);
Object ret=field.getInt(obj);
這一段反射com.oneplus.oneplusplugin.R$drawable 即外掛apk中R類中drawable資源,這裡drawable相當於R的類中類,對於類種類的訪問,反射通過用”$”符號將其連線.這裡可以一次類推,如果是獲取String資源,那麼com.oneplus.oneplusplugin.R$drawable改為com.oneplus.oneplusplugin.R$String,其他型別同理,當然在後面呼叫時是字串不是圖片了.
我在程式碼中也會把獲取String和其他資源的方式儘量新增完全,但是根據上面的,個人覺得只要學會舉一反三,其他資源也不是難題.
為了以防萬一,我在這裡就不列出String,Color等資訊獲取,但是在我的測試demo中,我還是給出瞭如何獲取等操作:
整個工程原始碼在後續會通過github提供