通过前面的基础内容,做一个Android 资源更新的插件应该没有问题,读者只要将插件的apk当做资源包就可以了,需要更新的资源全部打包到插件包中.
在正式开篇之前,可能很多人在网上查找,主Host APK和plugin之间还需要设置相同的sharedUserId,但是我下面没有做这个要求,因为设置了sharedUserId即代表主Host APK和plugin在同一个进程,这样可以”辨识对方”,主要是方便主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);
可以查一下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提供