通过前面的基础内容,做一个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提供