1. 程式人生 > >Android 通過反射綜合應用-獲取外掛Plugin資源

Android 通過反射綜合應用-獲取外掛Plugin資源

通過前面的基礎內容,做一個Android 資源更新的外掛應該沒有問題,讀者只要將外掛的apk當做資源包就可以了,需要更新的資源全部打包到外掛包中.

在正式開篇之前,可能很多人在網上查詢,主Host APKplugin之間還需要設定相同的sharedUserId,但是我下面沒有做這個要求,因為設定了sharedUserId即代表主Host APKplugin在同一個程序,這樣可以辨識對方”,主要是方便主Host更加準確的查詢到自己的plugin,其實個人認為可以有必要設,但是單純從技術角度,這個不會有影響,因為在處理時,無論什麼apk(或者dex)是相同的.

另外,很多網友看到類似的文章

,很多部落格把下面可以獲取資源就認為可以更換APK的面板外觀了,但是我覺得還差的遠,不過下面的確提供了入門的思路.

下面我們通過具體的例項看看如何實現.

<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);

可以查一下getIdentifierAPI使用,這個方法將第二個引數改為”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 即外掛apkR類中drawable資源,這裡drawable相當於R的類中類,對於類種類的訪問,反射通過用”$”符號將其連線.這裡可以一次類推,如果是獲取String資源,那麼com.oneplus.oneplusplugin.R$drawable改為com.oneplus.oneplusplugin.R$String,其他型別同理,當然在後面呼叫時是字串不是圖片了.


我在程式碼中也會把獲取String和其他資源的方式儘量新增完全,但是根據上面的,個人覺得只要學會舉一反三,其他資源也不是難題.

為了以防萬一,我在這裡就不列出String,Color等資訊獲取,但是在我的測試demo中,我還是給出瞭如何獲取等操作:


整個工程原始碼在後續會通過github提供