apk动态加载机制(一): 框架的实现
来源:互联网 发布:网络调教男奴的步骤 编辑:程序博客网 时间:2024/06/06 05:36
转载请注明出处:http://blog.csdn.net/singwhatiwanna/article/details/22597587 (来自singwhatiwanna的csdn博客)
实战过程中有些思考,对原博客做了适量的修改。
背景
我们知道,apk必须安装才能运行,但实际上通过动态加载也可以运行未安装的apk。我们可以通过一个宿主程序来动态加载未安装的apk并将其放在自己的进程中执行,也就是插件apk,但是这些插件apk需要进行特殊的代码处理才能被加载。
本文只是对apk的动态加载机制框架的实现进行了介绍,还存在以下2个问题本文中不做介绍。
1. 资源的访问。因为将apk加载到宿主程序中去执行,就无法通过宿主程序的Context去取到插件apk中的资源,比如图片、文本等,这是很好理解的,因为apk自己已经不存在上下文了,它执行时所采用的上下文是宿主程序的上下文,用宿主的Context是无法得到自己的资源的。宿主的context指向的是宿主的apk资源,本文的DEMO需要的资源都是动态创建的。不存在这个问题。
2. activity的生命周期,因为apk被宿主程序加载执行后,它的activity其实就是一个普通的类,正常情况下,activity在manifest中注册了以后,生命周期是由系统自动来管理的,现在被宿主程序接管了以后,系统根本就不知道插件activity的存在,apk的加载是由宿主程序加载的而不是系统。需要替代系统对apk中的activity的生命周期进行管理。
工作原理
如下图所示,首先宿主程序会到文件系统比如sd卡去加载插件apk,然后通过一个叫做proxy的activity去执行插件apk中的activity。
关于动态加载apk,理论上可以用到的有DexClassLoader、PathClassLoader和URLClassLoader。
DexClassLoader :可以加载文件系统上的jar、dex、apk
PathClassLoader :可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk
URLClassLoader :可以加载java中的jar,但是由于dalvik不能直接识别jar,所以此方法在Android中无法使用,尽管还有这个类
关于jar、dex和apk,dex和apk是可以直接加载的,因为它们都是或者内部有dex文件,而原始的jar是不行的,必须转
换成dalvik所能识别的字节码文件,转换工具可以使用Android sdk中platform-tools目录下的dx
转换命令 :dx --dex --output=dest.jar src.jar
整个框架涉及到3个概念:
宿主程序:主应用程序,已经安装了的APK,用来启动代理activity
代理程序:运行在宿主程序中的一个真正的activity,用来动态加载插件apk中的activity和代替系统去管理插件activity的生命周期
插件apk: 需要按照特殊的代码进行处理才能被代理程序调用
示例
宿主程序的实现
1. 主界面的实现
很简单,放了一个button,点击就会调用代理activity, 由代理activity反射调起插件apk (插件apk已经开发好了并且放在了sd卡中),至于先把apk从网上下载到本地再加载其实是一个道理。
宿主程序就是用来启动代理activity的。
- @Override
- public void onClick(View v) {
- if (v == mOpenClient) {
- // 启动的是代理activity
- Intent intent = new Intent(this, ProxyActivity.class);
- // 把要动态加载的插件apk的路径传进去
- intent.putExtra(ProxyActivity.EXTRA_DEX_PATH, "/mnt/sdcard/DynamicLoadHost/plugin.apk");
- startActivity(intent);
- }
- }
点击button以后,代理activity会被调起,然后加载apk并调起的任务就交给它了
2. 代理activity的实现(proxy)
- package com.ryg.dynamicloadhost;
- import java.lang.reflect.Constructor;
- import java.lang.reflect.Method;
- import dalvik.system.DexClassLoader;
- import android.annotation.SuppressLint;
- import android.app.Activity;
- import android.content.pm.PackageInfo;
- import android.os.Bundle;
- import android.util.Log;
- public class ProxyActivity extends Activity {
- private static final String TAG = "ProxyActivity";
- public static final String FROM = "extra.from";
- public static final int FROM_EXTERNAL = 0;
- public static final int FROM_INTERNAL = 1;
- public static final String EXTRA_DEX_PATH = "extra.dex.path";
- public static final String EXTRA_CLASS = "extra.class";
- private String mClass;
- private String mDexPath;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDexPath = getIntent().getStringExtra(EXTRA_DEX_PATH);
- // 启动时可以指定加载插件apk中的哪个activity
- mClass = getIntent().getStringExtra(EXTRA_CLASS);
- if (mClass == null) {
- launchTargetActivity();
- } else {
- launchTargetActivity(mClass);
- }
- }
- @SuppressLint("NewApi")
- protected void launchTargetActivity() {
- // 如果没有指定activity,则获取插件包的acvitity信息,加载主activity
- // 这里读取出来插件apk的activities信息有2个。是通过插件apk的manifest文件读取出来的。所以插件中的activity信息需要在插件apk的manifest文件中声明,否则这里读取出来的activities的信息为null.如果是直接指定了启动的类名的话不需要在插件apk的manifest文件中声明。
- PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(
- mDexPath, 1);
- // activities是所有的activity的信息数组
- if ((packageInfo.activities != null)
- && (packageInfo.activities.length > 0)) {
- String activityName = packageInfo.activities[0].name;
- mClass = activityName;
- launchTargetActivity(mClass);
- }
- }
- @SuppressLint("NewApi")
- protected void launchTargetActivity(final String className) {
-
- File dexOutputDir = this.getDir("dex", 0);
- // 这里获取出来的路径是/data/data/宿主包名/app_dex
- final String dexOutputPath = dexOutputDir.getAbsolutePath();
- ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();
- DexClassLoader dexClassLoader = new DexClassLoader(mDexPath,
- dexOutputPath, null, localClassLoader);
- try {
- // 动态创建要加载类的对象
- Class<?> localClass = dexClassLoader.loadClass(className);
- Constructor<?> localConstructor = localClass
- .getConstructor(new Class[] {});
- Object instance = localConstructor.newInstance(new Object[] {});
- // 反射调用setProxy方法,这个方法是要调用起的插件apk的activity中的方法,需要自己实现
- Method setProxy = localClass.getMethod("setProxy",
- new Class[] { Activity.class });
- setProxy.setAccessible(true);
- setProxy.invoke(instance, new Object[] { this });
-
- // 反射调用onCreate方法,这个方法是要调用起的插件apk的activity中的方法,需要自己实现, 用来创建插件activity
- Method onCreate = localClass.getDeclaredMethod("onCreate",
- new Class[] { Bundle.class });
- onCreate.setAccessible(true);
- // 调用onCreate方法时传递的参数,后面实现插件apk中的activity时需要区分是否是动态加载的,因为这个DEMO设计的也可以独立运行插件apk
- Bundle bundle = new Bundle();
- bundle.putInt(FROM, FROM_EXTERNAL);
- onCreate.invoke(instance, new Object[] { bundle });
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
说明:程序不难理解,思路是这样的:采用DexClassLoader去加载apk,然后如果没有指定class,就调起插件中主activity,否则调起指定的class。activity被调起的过程是这样的:首先通过类加载器去加载apk中activity的类并创建一个新对象,然后通过反射去调用这个对象的setProxy方法和onCreate方法,setProxy方法的作用是将插件apk中activity的生命周期全部交由宿主程序中的代理activity,onCreate方法是activity的入口,setProxy以后就调用onCreate方法,这个时候activity就被调起来了。
这样调用起来的插件apk运行在宿主的进程中,不存在一个插件的apk的进程,无法打断点跟踪插件中代码的运行。
问题: 为什么只反射调用onCreate方法就能看到界面?这里没有onResume
答:看下插件里的onCreate的实现,一个是把代理activity的context传进去,一个是动态创建了要显示界面的view,然后调用mProxyActivity.setContentView方法在代理activity中显示这个插件的view。 setContentView这个方法一旦调用,就会立刻显示view界面。注意这里view是显示在代理activity里的,所以插件不需要调用onResume方法
插件apk的实现
1. 基类BaseActivity的实现
为了让代理activity全面接管插件apk中所有activity的执行,需要为插件中的所有activity定义一个基类BaseActivity,在基类中处理代理相关的事情,同时BaseActivity还对是否使用代理进行了判断,如果不使用代理,那么activity的逻辑仍然按照正常的方式执行,也就是说,这个apk既可以做为一个正常的apk执行,也可以由宿主程序来动态执行。
- package com.ryg.dynamicloadclient;
- import android.app.Activity;
- import android.content.Intent;
- import android.os.Bundle;
- import android.util.Log;
- import android.view.View;
- import android.view.ViewGroup.LayoutParams;
- public class BaseActivity extends Activity {
- private static final String TAG = "Client-BaseActivity";
- public static final String FROM = "extra.from";
- public static final int FROM_EXTERNAL = 0;
- public static final int FROM_INTERNAL = 1;
- public static final String EXTRA_DEX_PATH = "extra.dex.path";
- public static final String EXTRA_CLASS = "extra.class";
- public static final String PROXY_VIEW_ACTION = "com.ryg.dynamicloadhost.VIEW";
- public static final String DEX_PATH = "/mnt/sdcard/DynamicLoadHost/plugin.apk";
- protected Activity mProxyActivity; // 代理activity的实例
- protected int mFrom = FROM_INTERNAL;
-
- // 代理activity动态加载时反射调用的setProxy就是这个方法,用来传入代理activity的实例
- public void setProxy(Activity proxyActivity) {
- // 这里this指针不是空指针,因为这个类的对象是存在的,this指针不是空,但是系统代码中获取资源时有空指针,界面跳转时也会有空指针。应该是调用这些函数时用到了activity中的某个变量为null了。而这个变量是与context相关的。
- mProxyActivity = proxyActivity;
- }
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- if (savedInstanceState != null) {
- mFrom = savedInstanceState.getInt(FROM, FROM_INTERNAL);
- }
- // mFrom的默认的值是FROM_INTERNAL, 如果是代理activity动态加载的,反射调用时设置的参数为FROM_EXTERNAL。则会给mFrom重新赋值。这里走到这个分支说明是直接安装的apk
- if (mFrom == FROM_INTERNAL) {
- super.onCreate(savedInstanceState);
- mProxyActivity = this;
- }
- }
-
- // 由子activity调用,用来进行activity之间的跳转
- protected void startActivityByProxy(String className) {
- if (mProxyActivity == this) {
- Intent intent = new Intent();
- intent.setClassName(this, className);
- this.startActivity(intent);
- } else {
- Intent intent = new Intent(PROXY_VIEW_ACTION);
- intent.putExtra(EXTRA_DEX_PATH, DEX_PATH);
- intent.putExtra(EXTRA_CLASS, className);
- // 如果是动态加载,context要换成宿主程序的context
- mProxyActivity.startActivity(intent);
- }
- }
- @Override
- public void setContentView(View view) {
- if (mProxyActivity == this) {
- super.setContentView(view);
- } else {
- mProxyActivity.setContentView(view);
- }
- }
- @Override
- public void setContentView(View view, LayoutParams params) {
- if (mProxyActivity == this) {
- super.setContentView(view, params);
- } else {
- mProxyActivity.setContentView(view, params);
- }
- }
- @Deprecated
- @Override
- public void setContentView(int layoutResID) {
- if (mProxyActivity == this) {
- super.setContentView(layoutResID);
- } else {
- mProxyActivity.setContentView(layoutResID);
- }
- }
- @Override
- public void addContentView(View view, LayoutParams params) {
- if (mProxyActivity == this) {
- super.addContentView(view, params);
- } else {
- mProxyActivity.addContentView(view, params);
- }
- }
- }
说明:相信大家一看代码就明白了,其中setProxy方法的作用就是为了让宿主程序能够接管自己的执行,一旦被接管以后,其所有的执行均通过proxy,且Context也变成了宿主程序的Context,也许这么说比较形象:宿主程序其实就是个空壳,它只是把其它apk加载到自己的内部去执行,这也就更能理解为什么资源访问变得很困难
2. 加载的子类activity的实现
由代理activity直接反射动态加载的入口activity
- public class MainActivity extends BaseActivity {
- private static final String TAG = "Client-MainActivity";
-
- // 代理activity动态加载时反射调用的onCreate就是具体的activity的这个方法
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- initView(savedInstanceState);
- }
- private void initView(Bundle savedInstanceState) {
- // 需要替换为代理activity的context
- mProxyActivity.setContentView(generateContentView(mProxyActivity));
- }
- private View generateContentView(final Context context) {
- LinearLayout layout = new LinearLayout(context);
- layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
- LayoutParams.MATCH_PARENT));
- layout.setBackgroundColor(Color.parseColor("#F79AB5"));
- Button button = new Button(context);
- button.setText("button");
- layout.addView(button, LayoutParams.MATCH_PARENT,
- LayoutParams.WRAP_CONTENT);
- button.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- Toast.makeText(context, "you clicked button",
- Toast.LENGTH_SHORT).show();
- // 基类activity中实现的方法,用代理activity的环境去进行activity之间的跳转
- startActivityByProxy("com.ryg.dynamicloadclient.TestActivity");
- }
- });
- return layout;
- }
- }
说明:由于访问不到apk中的资源了,所以界面是代码写的,而不是写在xml中,因为xml读不到了,这也是个大问题。注意到主界面中有一个button,点击后跳到了另一个activity,这个时候是不能直接调用系统的startActivity方法的,即使是宿主的contetxt,而是必须发送intent,宿主程序中的proxy程序处理intent通过反射来执行,原因很简单,首先apk本身没有Context上下文的,所以它无法调起activity,另外由于这个子activity是apk中的,通过宿主程序直接调用它也是不行的,因为它对宿主程序来说是不可见的,没有在宿主的manifest里声明这个activity,就不能直接显示的调用。
3. 跳转的activity的实现
也在插件apk中,由入口activity跳转后启动的界面
- package com.ryg.dynamicloadclient;
- import android.graphics.Color;
- import android.os.Bundle;
- import android.view.ViewGroup.LayoutParams;
- import android.widget.Button;
- public class TestActivity extends BaseActivity{
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Button button = new Button(mProxyActivity);
- button.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
- LayoutParams.MATCH_PARENT));
- button.setBackgroundColor(Color.YELLOW);
- button.setText("这是测试页面");
- setContentView(button);
- }
- }
说明:代码很简单,不用介绍了,同理,界面还是用代码来写的。
运行效果
1. 首先看直接安装插件apk时的运行效果
2. 再看看未安装时被宿主程序动态执行的效果
说明:可以发现,安装和未安装,执行效果是一样的,差别在于:首先未安装的时候由于采用了反射,所以执行效率会略微降低,其次,应用的标题发生了改变,也就是说,尽管apk被执行了,但是它毕竟是在宿主程序里面执行的,所以它还是属于宿主程序的,因此apk未安装被执行时其标题不是自己的,不过这也可以间接证明,apk的确被宿主程序执行了,不信看标题。最后,我想说一下这么做的意义,这样做有利于实现模块化,同时还可以实现插件机制
代码下载:
https://github.com/singwhatiwanna/dynamic-load-apk
http://download.csdn.net/detail/singwhatiwanna/7121505
- apk动态加载机制(一): 框架的实现
- APK动态加载框架解析(一)
- Android apk动态加载机制的研究(一)
- APK动态加载框架(DL)解析(一)
- Android动态加载(一)--加载已经安装的APK
- Android apk动态加载机制的研究(二)
- Android apk动态加载机制的研究(二)
- Android apk动态加载机制的研究
- Android apk动态加载机制的研究
- Android apk动态加载机制的研究
- Android apk动态加载机制的研究
- Android apk动态加载机制的研究
- Android apk动态加载机制的研究
- Android apk动态加载机制的研究
- Android apk动态加载机制的研究
- Android apk动态加载机制的研究
- Android apk动态加载机制的研究
- Android apk动态加载机制的研究
- android布局中include的使用
- 算法竞赛入门经典(第2版)习题1-7详解
- js修改css样式
- 标准C++中的string类的用法总结
- 安卓使用SOAP调用WebService
- apk动态加载机制(一): 框架的实现
- linux之用户
- 图片双击放大,可以移动!
- Marked - 基于JavaScript的全功能Markdown解析器
- 在调用popen时报错popen:: Cannot allocate memory
- centos -bash-4.1$
- MySQL支持的数据类型
- 设置session超时的三种方式
- servlet3-客户端请求与服务端响应(request,response)