Android应用插件化开发

在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;
	}
	@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之旅: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
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值