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方式

这也是本文的重点。插件都是以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);    }}


下面是Fragment工厂管理类,demo比较简单,仅仅用于构造Fragment而已。
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);}}


如上,插件模块apk已经完成了,将编译生成的apk放入手机中(我选择的目录是:/data/local/,当然实际编写的插件管理时插件apk最好放在宿主应用空间,即/data/data/宿主应用包名/目录下以保证该插件正常读写和对外访问控制)。

  下面,看看宿主程序中如何获取插件模块提供的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中对应的类



0 0