在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获取相应的资源。
例如:
目前该方式主要用来作为主应用换肤的插件。例如:微博的夜间模式。
总结:利:插件与主应用都是独立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;
}
@Override
public 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;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
Log.d(TAG, "onActivityCreated");
super.onActivityCreated(savedInstanceState);
}
@Override
public void onAttach(Activity activity) {
Log.d(TAG, "onAttach");
super.onAttach(activity);
}
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
}
@Override
public void onDestroy() {
Log.d(TAG, "onDestroy");
super.onDestroy();
}
@Override
public void onDestroyView() {
Log.d(TAG, "onDestroyView");
super.onDestroyView();
}
@Override
public void onDetach() {
Log.d(TAG, "onDetach");
super.onDetach();
}
@Override
public void onPause() {
Log.d(TAG, "onPause");
super.onPause();
}
@Override
public void onResume() {
Log.d(TAG, "onResume");
super.onResume();
}
@Override
public void onStart() {
Log.d(TAG, "onStart");
super.onStart();
}
@Override
public 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;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mAddBtn = (Button)findViewById(R.id.add);
mAddBtn.setOnClickListener(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
@Override
public 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之旅:等几个章节。
首先反射构造一个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中对应的类