zoukankan      html  css  js  c++  java
  • 插件化二(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布局文件

    运行效果

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

  • 相关阅读:
    你有犯错的权利
    面对人生这道程序,该如何编码?
    如何面对失败?
    知行:成长的迭代之路
    一份软件工程行业生存指南
    知行:程序员如何保持二者的平衡
    花钱的习惯
    互联网金融涌动下的冲动与借债
    docker 常用命令
    java 静态变量 静态代码块 加载顺序问题
  • 原文地址:https://www.cnblogs.com/anye6488/p/3885988.html
Copyright © 2011-2022 走看看