Android应用插件化开发
来源:互联网 发布:内部网络管理软件 编辑:程序博客网 时间:2024/05/16 11:58
在android的项目开发中,都会遇到后期功能拓展(增强)与主程序代码变更的现实矛盾。随着移动APP的版本迭代,仅仅满足基本功能的APP,在发展路径上多少都会受挫,而提供更多的增强功能又会让APP变得臃肿。怎样平衡用户的需求与APP的臃肿度呢?一个简单的办法就是打造APP插件化,给胖APP瘦身,而这一切,都是根据用户的需求进行的选择。参见:http://mobile.51cto.com/hot-436653.htm
1 插件化开发方式
1.1 安装apk方式
该方式下,插件apk需要安装到Android手机中,用户可以再“管理应用程序--—已下载”中看到对应的插件apk信息。
1.1.1 独立运行apk
插件apk以独立运行方式为花粉客户端提供一个功能或输入方式。反之,花粉客户端管理插件apk的一个入口。如下所示为“支付宝---我的生活”页面。
快的打车、淘宝等插件都是可独立运行的apk。支付宝客户端维护这两个插件的入口,并统一提供账号的登录鉴权。
目前独立安装apk插件方式为Android客户端开发的主流。微博、微信、qq等都使用该方式。
1.1.2 仅作为资源提供方
基本原理:通过package获取被调用应用的Context,通过Context获取相应的资源。
例如:http://www.cnblogs.com/over140/archive/2012/04/19/2446119.html
目前该方式主要用来作为主应用换肤的插件。例如:微博的夜间模式。
总结:利:插件与主应用都是独立apk,耦合性小,开发容易。
弊:每一个插件都需要用户安装一个独立apk,降低了用户体验。
1.2 基于DexClassloader的非安装apk方式
下面是我写的demo:demo中插件用于提供一个Fragment给宿主应用程序使用(展示)
先来看看插件模块的代码。首先看看插件模块使用的布局文件fragment_plug.xml,如下:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="wrap_content"/><TextView android:id="@+id/text1" android:text="adfadfasdfadf" android:layout_below="@+id/text" android:layout_width="match_parent" android:layout_height="wrap_content"/></RelativeLayout>下面是插件模块提供的Fragment代码
package com.example.subfragmentplug;import android.annotation.SuppressLint;import android.app.Activity;import android.content.Context;import android.content.res.Resources;import android.os.Bundle;import android.support.v4.app.Fragment;import android.util.Log;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.TextView;@SuppressLint("NewApi")public class MyPlugFragment extends Fragment {private static final String PACKAGE_NAME = "com.example.subfragmentplug";private static final String TAG = "MyPlugFragment";private Resources mRes;public MyPlugFragment(Resources res) {mRes = res;}@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {Log.d(TAG, "onCreateView");if (mRes != null) {int id = mRes.getIdentifier("fragment_plug", "layout", "com.example.subfragmentplug");View view = getView(getActivity(), mRes, id);TextView text = (TextView)view.findViewById(mRes.getIdentifier("text", "id", PACKAGE_NAME));text.setText(mRes.getString(mRes.getIdentifier("hello_world_sub", "string", PACKAGE_NAME))); return view;}return null;}@Overridepublic void onActivityCreated(Bundle savedInstanceState) {Log.d(TAG, "onActivityCreated");super.onActivityCreated(savedInstanceState);}@Overridepublic void onAttach(Activity activity) {Log.d(TAG, "onAttach");super.onAttach(activity);}@Overridepublic void onCreate(Bundle savedInstanceState) {Log.d(TAG, "onCreate");super.onCreate(savedInstanceState);}@Overridepublic void onDestroy() {Log.d(TAG, "onDestroy");super.onDestroy();}@Overridepublic void onDestroyView() {Log.d(TAG, "onDestroyView");super.onDestroyView();}@Overridepublic void onDetach() {Log.d(TAG, "onDetach");super.onDetach();}@Overridepublic void onPause() {Log.d(TAG, "onPause");super.onPause();}@Overridepublic void onResume() {Log.d(TAG, "onResume");super.onResume();}@Overridepublic void onStart() {Log.d(TAG, "onStart");super.onStart();}@Overridepublic void onStop() {Log.d(TAG, "onStop");super.onStop();}/** * 获取资源对应的编号,具体参见Resource.getIdentifier()方法 * * @param testb * @param resName * @param resType layout、drawable、string * @return */ private int getId(Resources res, String resType, String resName) { return res.getIdentifier(resName, resType, PACKAGE_NAME); } /** * 获取视图 * * @param ctx * @param id * @return */ public View getView(Context ctx, int id) { return LayoutInflater.from(ctx).inflate(id,null); } /** * 获取视图 * * @param ctx * @param id * @return */ public View getView(Context ctx, Resources res, int id) { return LayoutInflater.from(ctx).inflate(res.getLayout(id), null); }}
package com.example.subfragmentplug;import android.content.res.Resources;import android.support.v4.app.Fragment;public class MyClass {public Fragment getFragment(Resources res) {return new MyPlugFragment(res);}}
下面,看看宿主程序中如何获取插件模块提供的Fragment并显示的。
首先宿主应用主页面布局activity_main.xml如下,一个按钮用于获取插件模块提供的fragment,一个用于展示Fragment的容器
<LinearLayout 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:orientation="vertical" tools:context=".MainActivity" > <Button android:id="@+id/add" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" android:text="添加fragment"/> <LinearLayout android:id="@+id/container" android:layout_width="match_parent" android:orientation="vertical" android:layout_height="wrap_content"> </LinearLayout></LinearLayout>
然后看下,主页面MainActivity.java中如何处理未安装的插件apk资源和代码加载的。
package com.example.mainfragmentmanager;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import dalvik.system.DexClassLoader;import android.os.Bundle;import android.content.Context;import android.content.pm.PackageManager.NameNotFoundException;import android.content.res.Resources;import android.support.v4.app.Fragment;import android.support.v4.app.FragmentActivity;import android.support.v4.app.FragmentTransaction;import android.util.Log;import android.view.LayoutInflater;import android.view.Menu;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;public class MainActivity extends FragmentActivity implements OnClickListener{private static final String PACKAGE_TEST_B = "com.example.subfragmentplug";private Button mAddBtn;private boolean ResLoadFlag = false;private Resources mRes;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mAddBtn = (Button)findViewById(R.id.add);mAddBtn.setOnClickListener(this);}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {getMenuInflater().inflate(R.menu.activity_main, menu);return true;}@Overridepublic void onClick(View v) {String apkPath = "/data/local/SubFragmentPlug.apk";loadResFromNonInstalledApk(apkPath);Fragment fragment = (Fragment)loadDexFromNonInstalledApk(apkPath, "com.example.subfragmentplug.MyClass");if (fragment != null) {FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();transaction.add(R.id.container, fragment, "MyPlugFragment");transaction.commit();}}private void loadResFromNonInstalledApk(String fileName) {if (ResLoadFlag) {return;}ResLoadFlag = true;Resources res = null;try { Class<?> class_AssetManager = getAssets().getClass(); Object assetMag = class_AssetManager.newInstance(); Method method_addAssetPath = class_AssetManager .getDeclaredMethod("addAssetPath", String.class); method_addAssetPath.invoke(assetMag, fileName); res = this.getResources(); Constructor<?> constructor_Resources = Resources.class .getConstructor(class_AssetManager, res.getDisplayMetrics() .getClass(), res.getConfiguration().getClass()); mRes = (Resources) constructor_Resources.newInstance(assetMag, res.getDisplayMetrics(), res.getConfiguration());} catch (Exception e) { e.printStackTrace();}}private Object loadDexFromNonInstalledApk(String fileName, String className) {Log.e("MainActivity", "loadDexFromNonInstalledApk");DexClassLoader loader = new DexClassLoader(fileName, getApplicationInfo().dataDir, null, getClassLoader());try {Class<?> clazz = loader.loadClass(className); Object obj = clazz.newInstance(); Class classes[] = {Resources.class}; Resources res[] = {mRes}; Method m = clazz.getMethod("getFragment", classes); return m.invoke(obj, res);} catch (Exception e) { e.printStackTrace(); } return null;}/** * 获取资源对应的编号 * * @param testb * @param resName * @param resType * layout、drawable、string * @return */ private int getId(Resources res, String resType, String resName) { return res.getIdentifier(resName, resType, PACKAGE_TEST_B); } /** * 获取视图 * * @param ctx * @param id * @return */ public View getView(Context ctx, int id) { return LayoutInflater.from(ctx).inflate(id,null); } /** * 获取视图 * * @param ctx * @param id * @return */ public View getView(Context ctx, Resources res, int id) { return LayoutInflater.from(ctx).inflate(res.getLayout(id), null); } /** * 获取TestB的Context * * @return * @throws NameNotFoundException */ private Context getTestBContext() throws NameNotFoundException { return createPackageContext(PACKAGE_TEST_B, Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE); }}
其中重要的有两个方法
1.loadResFromNonInstalledAPk()方法
该方法中模拟了一个Android应用进程启动时资源加载过程,具体请参见老罗的android之旅:http://blog.csdn.net/luoshengyang/article/details/8791064等几个章节。
首先反射构造一个AssetManager对象,然后将插件apk所在路径完整路径加入到该AssetManager对象的资源路径Vector中,即反射调用其addAssetPath()方法。最后使用该AssetManager对象构造一个Resources对象,此时就可以使用该Resources对象来获取插件apk中资源了。这里我把Resources对象传递给插件apk的Fragment中,用以获取布局等资源。
2.loadDexFromNonInstalledAPk()方法
这里首先使用了DexClassLoader用以加载未安装的插件apk。然后加载并通过反射获取到插件apk中提供的Fragment。
宿主程序中“添加Fragment”的按钮onclick事件中,依次执行上面两个方法将插件apk的资源和代码都加载后,获取Fragment并显示到主页面上。
文章最后总结下--基于DexClassloader的非安装apk方式--的优点
1.宿主程序中可以管理插件,比如添加,删除,禁用等。插件apk不需要安装,仅仅放在/data/data/对应目录即可,方便宿主程序管理。
2.android插件开发,最麻烦的界面相关资源问题,这里使用Fragment方式解决了。因为Fragment的生命周期由其宿主Activity或者Fragment所绑定的FragmentManager进行管理即可,不想Activity那么复杂需要Manifest中注册等等。(android4.2开始支持Fragment中添加多个子Fragment,使用V13-support支持包即可)
关于宿主应用和插件apk中使用相同jar作为基础模块(例如demo中宿主应用和插件apk都依赖了android-support-v14.jar),会引起如下异常
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
解决办法如下,将v13jar包从libs目录删除,然后使用下图所示方法,将该jar加入依赖。(使用Add External JARs 按钮将v13 jar包引入即可)此时插件编译生成的apk中不会包含V13 jar包中的类,当插件apk运行时需要使用V13jar中的类时会自动加载宿主程序apk中对应的类
- Android应用插件化开发
- Android应用开发的插件化 模块化
- Android应用插件式开发
- [Android] Android应用插件式开发解决方法
- Android应用开发的插件化 模块化
- Android应用插件式开发解决方法之一
- Android应用插件式开发解决方法之一
- Android应用插件式开发解决方法之一
- Android应用插件式开发解决方法
- Android应用插件式开发解决方法
- Android应用插件式开发解决方法
- Android应用插件式开发解决方法
- Android应用插件式开发解决方法
- Android应用插件式开发解决方法
- Android应用插件式开发解决方法
- Android应用插件式开发解决方法
- Android应用插件式开发解决方法
- Android应用插件式开发解决方法
- 关于UIWebView的一些应用,包括与JS交互,前进,后退等
- 极限编程的集成测试工具-Dunit
- Count The Carries
- DevExpress 行事历(Scheduler)的常用属性、事件和方法
- FFMpeg分析1:URLProtocol,URLContext和ByteIOContext
- Android应用插件化开发
- Linux中文显示乱码设置
- 导出Excel工具类
- iOS 开发者必知的 75 个工具(译文)
- SVN myeclipse6.5
- 让oozie优雅的支持hbase
- x264函数调用关系图 FFMPEG中MPEG-2编解码函数调用关系图MPEG-4 AVC/H.264 信息FFMPEG 学习笔记x264中的Decoderx264 代码下载信息H.264/AVC中C
- 常用网络名词记录
- Android开发