插件化二(Android)

插件化二(Android)

上一篇文章《插件化一(android》里大概构思了下插件加载与校验的流程和一些大体设计,这次就具体展开,在《动态加载与插件化》里提到以apk形式开发带res资源的插件,这里也介绍下具体的实现方式。

插件信息规划

那我们就开始进入正题,在实现插件化的时候,我们都需要考虑,对于插件的描述信息(如插件名,插件版本等等),我们应该放在哪里。比如弄一个文件储存插件信息再和插件一起打个包,或如jar形式的可以直接在jar里添加插件信息,相信大家都能想到多种是实现方法。

    在这里我则是将插件信息写在zip(即jar或apk)的末尾的comment里,即在不修改zip文件的结构的前提下,在zip结构中添加信息。为了后续插件开发方便我为写入插件信息的工作编写了个cmd 工具,也方便后续用ANT与Eclipse集成简化开发流程,以下是部分代码。

  1. int main(int argc, char* argv[])
  2. {
  3.    if (argc < 3)
  4.    {
  5.       printf("params count error");
  6.       return -1;
  7.    }
  8.    char* path = argv[1];
  9.    char* data = argv[2];
  10.  
  11.    FileInfo* file = open(path, "r+");//打开zip文件
  12.    if (file == NULL)
  13.    {
  14.       printf("open file error");
  15.       return -1;
  16.    }
  17.  
  18.    if (!isVaildZip(file))//校验是否是合法zip文件
  19.    {
  20.       printf("error zip file");
  21.       return -1;
  22.    }
  23.  
  24.    if (!writeComment(file, data, strlen(data)))//写入信息到zip文件中
  25.       addComment(file, data, strlen(data));
  26.  
  27.  
  28.    EndRecord* end = readZipEndRecord(file);
  29.  
  30.    if (end == NULL)
  31.    {
  32.       printf("read end Record error");
  33.       return -1;
  34.    }
  35.  
  36.    printf(" comment is %s", end->Comment);
  37.    return 0;
  38. }

插件的加载

讲完插件信息的规划,我们来看下插件化至关重要的一步插件的加载,在《动态加载与插件化》里介绍过Android动态加载的实现方式,对于纯代码的android插件加载,相对来说还是比较简单的一个DexClassLoader就搞定了,相信DexClassLoader网上很多介绍的文章,这里就不具体介绍了,直接上代码如下

  1. @Override
  2.    public void loadPluginPackage(Context context, PluginInfo info, PluginContextLoadCallBack callBack)
  3.    {
  4. //创建dexopt的目录DexClassLoader需要的
  5.        File file = new File(context.getFilesDir(), "dexopt");
  6.        if (!file.exists())
  7.       file.mkdirs();
  8.  
  9.        File pluginFile = new File(info.getPath());
  10.        File temp;
  11.        try
  12.        {
  13.       temp = StorageManager.create(context).createFile(info.getName()+".jar",StorageManager.Cache);
  14.       Logger.I("move plugin to run path");
  15.       IOManager.moveTo(pluginFile, temp);//把插件移到加载目录
  16.        }
  17.        catch (IOException e)
  18.        {
  19.       ExceptionUtils.handle(e);
  20.       if(callBack != null)
  21.           callBack.onError(ErrorInfo.Plugin_Load_Error, "move "+pluginFile.getPath()+" to temp path fail");
  22.       return;
  23.        }
  24.  
  25.        if (file != null)
  26.        {
  27. //加载插件,就这么一句别的你都可以忽略
  28.       _loader = new DexClassLoader(temp.getPath(), file.getPath(), null, context.getClassLoader());
  29.       _app = context;
  30.  
  31.       if (callBack != null)
  32.       {
  33.           callBack.onLoad(this);
  34.           return;
  35.       }
  36.        }
  37.  
  38.    }

过完纯代码的插件加载方式,那我们再来看下如何加载带res资源的apk形式的插件(PS:以下方式使用到多个android内部的api,兼容性未大规模测试过),在《动态加载与插件化》介绍到要使用Apk里的res那就必须要拿到这个APK对应的Resources对象,那里我介绍了两种获取Resources对象的方法,这里我着重讲下第二种(第一种以后再介绍^_^)。

那如何获得插件APK对应的Context呢,如果去研究AndroiddActivity的启动过程不难发现,Application(就是Context^_^)是由一个叫LoadedApk的对象创建的,LoadedApk有一个makeApplication方法有两个参数,booleanInstrumentation,第一个参数是指定是否使用创建默认的Application,第二个参数是是一个环境对象,用于跟踪android个组件的创建,在android的测试框架中可能会接触到它。调用如下(某些版本里LoadedApk是个不公开的内部类,所以以反射方式调用,也建议以下所有访问内部api的都用反射方式调用,这样可以做多版本的兼容)

  1. // 创建apk的application
  2.  application = ReflectHelper.invoke(loadedApk, "makeApplication", new Class<?>[] { boolean.class, Instrumentation.class }, false, new Instrumentation());

现在我们知道Application是由LoadedApk创建,那LoadedApk对象我们又从哪里获得呢,查看Android的源码顺藤摸瓜,最终找到了ActivityThread. getPackageInfoNoCheck这个方法,这个方法在4.0以上的系统和4.0以下的系统,参数是不一样的.

4.0以上有两个参数,第一个是ApplicationInfo 就是对应APK的Application信息这个大家应该熟悉的,我们可以通过PackageManager.getPackageArchiveInfo这个方法传入apk路径和Flag参数获得PackageInfo,从PackageInfo里就能获得APK的Applicationinfo,然后用应用的Applicationinfo的参数替换一下如uid,datadir等.第二个是CompatibilityInfo, CompatibilityInfo是4.0以上才有的(4.0以下没有这个参数),包含和屏幕分辩率有关的信息,我们可以直接通过Resources对象获得Resources.getCompatibilityInfo()这样我们就凑齐参数了,可以放大了^_^,调用代码

  1. private void RelpacePluginInfo(PackageInfo info, Context context, String path)
  2.    {
  3.   info.applicationInfo.dataDir = context.getApplicationInfo().dataDir;
  4.   info.applicationInfo.publicSourceDir = path;
  5.   info.applicationInfo.sourceDir = path;
  6.   info.applicationInfo.uid = context.getApplicationInfo().uid;
  7.   info.applicationInfo.metaData = context.getApplicationInfo().metaData;
  8.   info.applicationInfo.nativeLibraryDir = context.getApplicationInfo().nativeLibraryDir;
  9.    }
  1. private void getPackageInfoNoCheck(final Context context, final ApplicationInfo info, final ResultCallBack<Object> callBack)
  2.    {
  3. //注意ActivityThread. getPackageInfoNoCheck必须在主线程调用
  4.   AsyncManager.postUI(new Runnable()
  5.   {
  6.       @Override
  7.       public void run()
  8.       {
  9.      final Object value = getPackageInfoNoCheck(context, info);
  10.      if (callBack != null)
  11.      {
  12.          if (value != null)
  13.         callBack.onCompleted(value);
  14.          else callBack.onError(CallBack.Error, "get loadedapk fail");
  15.      }
  16.       }
  17.   });
  18.    }
  19.  
  20.    private Object getPackageInfoNoCheck(Context context, ApplicationInfo info)
  21.    {
  22.   ActivityThread thread = ActivityThread.currentActivityThread();
  23.   Object value;
  24.   try
  25.   {
  26.       value = ReflectHelper.invoke(ActivityThread.class, "getPackageInfoNoCheck", new Class<?>[] { ApplicationInfo.class }, thread, info);
  27.       return value;
  28.   }
  29.   catch (Exception e)
  30.   {
  31.       ExceptionUtils.handle(e);
  32.       return thread.getPackageInfoNoCheck(info, context.getResources().getCompatibilityInfo());//如果调用一个参数的getPackageInfoNoCheck失败,就尝试访问两个参数的
  33.   }
  34.    }

现在万事OK不,但是当我们执行makeApplication时,Logcat无情的抛了个异常给我们,查看异常我们很容易发现原因,是某个目录没有访问的权限,是由于getPackageInfoNoCheck中创建LoadedApk时需要用DexClassLoader去加载Apk的代码,指定的路径无法访问。难道是死胡同一条,那接着去查看makeApplication的逻辑,可以发现如果LoadedApk里的mClassLoader这个字段如果不为null,LoadedApk就不会去重新创建,这样就给了我们机会,我们可以自己用DexClassLoader去加载Apk再通过反射设置给LoadedApk就骗过它了。

  1. ClassLoader loader = getApkClassLoader(currentApp, path);
  2.      if (!ReflectHelper.setValue(loadedApk, ClassLoader, loader))
  3.      {
  4.     Logger.E("set LoadedApk ClassLoader fail");
  5.     return null;
  6.      }

这样之后我们就能顺利调用LoadedApk的makeApplication方法创建Apk对应的Application,获得Application后我们是不是就可以随便访问Apk里的资源了呢,实际上不是这么容易在的,在《动态加载与插件化》里提到,如果直接用ApplicationLayoutInflater去创建View资源我们是可以顺利拿到View也可以创建,但是会有潜在问题,由于这个Context所对应的Apk没有安装,如果View里使用到系统服务(如剪切板),系统服务如果去按报名检索这个apk时,是无法找到的,那时候就直接GameOver,所以我们还要对LayoutInflater做一定的处理(其实就是保证具体的Context是安装了的那个),这个有两种方法,一种是替换LayoutInflater的反射mContext字段,还有种是通过LayoutInflater(LayoutInflater,Context)这个构造创建个新的LayoutInflater。在这之前我们要先构造个特殊的Context,一个包含宿主APK信息和插件资源信息的Context

如下

  1. public Context getPluginContext()
  2.     {
  3.    return new ContextWrapper(getAppContext())
  4.    {
  5.        @Override
  6.        public Resources getResources()
  7.        {
  8.       return getPluginAppContext().getResources();
  9.        }
  10.  
  11.        @Override
  12.        public Theme getTheme()
  13.        {
  14.       return getPluginAppContext().getTheme();
  15.        }
  16.  
  17.        @Override
  18.        public ClassLoader getClassLoader()
  19.        {
  20.            return getPluginClassLoader();
  21.        }
  22.  
  23.        @Override
  24.        public AssetManager getAssets()
  25.        {
  26.            return getPluginAppContext().getResources().getAssets();
  27.        }
  28.    };
  29.     }

接下来处理LayoutInflater

第一种方式

  1. LayoutInflater inflater = LayoutInflater.from(getPluginAppContext());
  2. if (ReflectHelper.setValueAll(inflater, "mContext",getPluginContext()))
  3.    Logger.I("set mContext suc");

第二种方式,略长。。。

  1.   public LayoutInflater getLayoutInflater()
  2.     {
  3.    if (getAppContext().equals(getPluginAppContext()))
  4.    {
  5.        return LayoutInflater.from(getPluginAppContext());
  6.    }
  7.  
  8.  
  9.    LayoutInflater inflater = buildLayoutInflater();
  10.    inflater.setFactory(new Factory()
  11.    {
  12.        @Override
  13.        public View onCreateView(String name, Context context, AttributeSet set)
  14.        {
  15.       View view = null;
  16.       Class<?> cls;
  17.       try
  18.       {
  19.               cls = getViewClass(name);
  20.               Constructor<?> constructor = cls.getConstructor(Context.class,AttributeSet.class);
  21.               view = (View)constructor.newInstance(context,set);
  22.       }
  23.       catch(ClassNotFoundException e)
  24.       {
  25.           e.printStackTrace();
  26.       }
  27.       catch (SecurityException e)
  28.       {
  29.           e.printStackTrace();
  30.       }
  31.       catch (NoSuchMethodException e)
  32.       {
  33.           e.printStackTrace();
  34.       }
  35.       catch (IllegalArgumentException e)
  36.       {
  37.           e.printStackTrace();
  38.       }
  39.       catch (InstantiationException e)
  40.       {
  41.           e.printStackTrace();
  42.       }
  43.       catch (IllegalAccessException e)
  44.       {
  45.           e.printStackTrace();
  46.       }
  47.       catch (InvocationTargetException e)
  48.       {
  49.           e.printStackTrace();
  50.       }
  51.  
  52.       return view;
  53.        }
  54.    });
  55.  
  56.    return inflater;
  57.     }
  58.  
  59.     Class<?> getViewClass(String name) throws ClassNotFoundException
  60.     {
  61.    if(-1 == name.indexOf("."))
  62.        return View.class.getClassLoader().loadClass("android.widget."+name);
  63.    else
  64.        return getPluginClassLoader().loadClass(name);
  65.     }
  66.  
  67.     LayoutInflater buildLayoutInflater()
  68.     {
  69.    return new LayoutInflater( LayoutInflater.from(getPluginAppContext()),getPluginContext())
  70.    {
  71.        @Override
  72.        public LayoutInflater cloneInContext(Context context)
  73.        {
  74.       return this;
  75.        }
  76.    };
  77.     }

到此我能就可以加载带资源的插件APK了,具体使用

  1. import com.joyreach.plugin.IActivityHost;
  2. import com.joyreach.plugin.IActivityLifeCycle;
  3. import com.joyreach.plugin.IPlugin;
  4. import com.joyreach.plugin.PluginContext;
  5. import android.app.Activity;
  6. import android.app.Dialog;
  7. import android.content.Context;
  8. import android.graphics.Color;
  9. import android.graphics.drawable.ColorDrawable;
  10. import android.view.Window;
  11. import android.widget.Toast;
  12. //插件类
  13. public class TestPlugin implements IPlugin,IActivityHost,IActivityLifeCycle
  14. {
  15.    PluginContext _context;
  16.  
  17.    @Override
  18.    public void onLoaded(PluginContext context)
  19.    {
  20.       _context = context;
  21.    }
  22.  
  23.    @Override
  24.    public void onUnloaded(PluginContext context)
  25.    {
  26.    }
  27.  
  28.    @Override
  29.    public void attach(Activity activity)
  30.    {
  31.       new TestDialog(activity, _context).show();
  32.    }
  33.  
  34.    @Override
  35.    public void dattach()
  36.    {
  37.    }
  38.  
  39.    @Override
  40.    public void onCreate(Activity activity)
  41.    {
  42.        Toast.makeText(activity, "onCreate", Toast.LENGTH_SHORT).show();
  43.    }
  44.  
  45.    @Override
  46.    public void onResume(Activity activity)
  47.    {
  48.        Toast.makeText(activity, "onResume", Toast.LENGTH_SHORT).show();
  49.    }
  50.  
  51.    @Override
  52.    public void onPause(Activity activity)
  53.    {
  54.        Toast.makeText(activity, "onPause", Toast.LENGTH_SHORT).show();
  55.    }
  56.  
  57.    @Override
  58.    public void onDestroy(Activity activity)
  59.    {
  60.        Toast.makeText(activity, "onDestroy", Toast.LENGTH_SHORT).show();
  61.    }
  62.  
  63.    class TestDialog extends Dialog
  64.    {
  65.        public TestDialog(Context context,PluginContext pluginContext)
  66.        {
  67.       super(context);
  68.  
  69.       getWindow().requestFeature(Window.FEATURE_NO_TITLE);
  70.       setContentView(pluginContext.getLayoutInflater().inflate(anye.plugin.R.layout.testdialog, null));
  71.       getWindow().setBackgroundDrawable(new ColorDrawable(Color.argb(0, 0, 0, 0)));
  72.        }
  73.  
  74.    }
  75. }

 

  1. Model.getInstance().getPluginSystem().loadPlugin("core", new PluginContext.PluginContextLoadCallBack()
  2.    {
  3.        @Override
  4.        public void onLoad(PluginContext context)
  5.        {
  6.       Toast.makeText(PluginTestActivity.this,
  7.          PluginManager.current().getInstallPlugins() + ":current load " + context.getInfo().getName() + "_" + context.getInfo().getVersonCode(),
  8.          Toast.LENGTH_LONG).show();
  9.  
  10.       PluginContext.PluginContainer container = context.load("plugin.test.main.TestPlugin");
  11.       container.load();
  12.  
  13.       IActivityHost host = container.asType();
  14.       host.attach(PluginTestActivity.this);
  15.  
  16.       Toast.makeText(PluginTestActivity.this,getResources().getString(R.string.title_activity_data_event_test),
  17.          Toast.LENGTH_LONG).show();
  18.        }
  19.  
  20.        @Override
  21.        public void onError(int code, String msg)
  22.        {
  23.       Toast.makeText(PluginTestActivity.this, "load fail: " + msg, Toast.LENGTH_LONG).show();
  24.        }
  25.    });

anye.plugin.R.layout.testdialog布局文件

运行效果

先到这里^_^,下次介绍插件整个加载流程,项目构成,可以洗洗睡了!!!!!!!!!!!

原文地址:https://www.cnblogs.com/anye6488/p/3885988.html