zoukankan      html  css  js  c++  java
  • Android之Android apk动态加载机制的研究

    转载请注明出处:http://blog.csdn.net/singwhatiwanna/article/details/22597587 (来自singwhatiwanna的csdn博客)

    背景

    问题是这样的:我们知道,apk必须安装才能运行,如果不安装要是也能运行该多好啊,事实上,这不是完全不可能的,尽管它比较难实现。在理论层面上,我们可以通过一个宿主程序来运行一些未安装的apk,当然,实践层面上也能实现,不过这对未安装的apk有要求。我们的想法是这样的,首先要明白apk未安装是不能被直接调起来的,但是我们可以采用一个程序(称之为宿主程序)去动态加载apk文件并将其放在自己的进程中执行,本文要介绍的就是这么一种方法,同时这种方法还有很多问题,尤其是资源的访问。因为将apk加载到宿主程序中去执行,就无法通过宿主程序的Context去取到apk中的资源,比如图片、文本等,这是很好理解的,因为apk已经不存在上下文了,它执行时所采用的上下文是宿主程序的上下文,用别人的Context是无法得到自己的资源的,不过这个问题貌似可以这么解决:将apk中的资源解压到某个目录,然后通过文件去操作资源,这只是理论上可行,实际上还是会有很多的难点的。除了资源存取的问题,还有一个问题是activity的生命周期,因为apk被宿主程序加载执行后,它的activity其实就是一个普通的类,正常情况下,activity的生命周期是由系统来管理的,现在被宿主程序接管了以后,如何替代系统对apk中的activity的生命周期进行管理是有难度的,不过这个问题比资源的访问好解决一些,比如我们可以在宿主程序中模拟activity的生命周期并合适地调用apk中activity的生命周期方法。本文暂时不对这两个问题进行解决,因为很难,本文仅仅对apk的动态执行机制进行介绍,尽管如此,听起来还是有点小激动,不是吗?

    工作原理

    如下图所示,首先宿主程序会到文件系统比如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

    示例

    宿主程序的实现

    1. 主界面很简单,放了一个button,点击就会调起apk,我把apk直接放在了sd卡中,至于先把apk从网上下载到本地再加载其实是一个道理。

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. @Override  
    2. public void onClick(View v) {  
    3.     if (v == mOpenClient) {  
    4.         Intent intent = new Intent(this, ProxyActivity.class);  
    5.         intent.putExtra(ProxyActivity.EXTRA_DEX_PATH, "/mnt/sdcard/DynamicLoadHost/plugin.apk");  
    6.         startActivity(intent);  
    7.     }  
    8.   
    9. }  

    点击button以后,proxy会被调起,然后加载apk并调起的任务就交给它了

    2. 代理activity的实现(proxy)

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. package com.ryg.dynamicloadhost;  
    2.   
    3. import java.lang.reflect.Constructor;  
    4. import java.lang.reflect.Method;  
    5.   
    6. import dalvik.system.DexClassLoader;  
    7. import android.annotation.SuppressLint;  
    8. import android.app.Activity;  
    9. import android.content.pm.PackageInfo;  
    10. import android.os.Bundle;  
    11. import android.util.Log;  
    12.   
    13. public class ProxyActivity extends Activity {  
    14.   
    15.     private static final String TAG = "ProxyActivity";  
    16.   
    17.     public static final String FROM = "extra.from";  
    18.     public static final int FROM_EXTERNAL = 0;  
    19.     public static final int FROM_INTERNAL = 1;  
    20.   
    21.     public static final String EXTRA_DEX_PATH = "extra.dex.path";  
    22.     public static final String EXTRA_CLASS = "extra.class";  
    23.   
    24.     private String mClass;  
    25.     private String mDexPath;  
    26.   
    27.     @Override  
    28.     protected void onCreate(Bundle savedInstanceState) {  
    29.         super.onCreate(savedInstanceState);  
    30.         mDexPath = getIntent().getStringExtra(EXTRA_DEX_PATH);  
    31.         mClass = getIntent().getStringExtra(EXTRA_CLASS);  
    32.   
    33.         Log.d(TAG, "mClass=" + mClass + " mDexPath=" + mDexPath);  
    34.         if (mClass == null) {  
    35.             launchTargetActivity();  
    36.         } else {  
    37.             launchTargetActivity(mClass);  
    38.         }  
    39.     }  
    40.   
    41.     @SuppressLint("NewApi")  
    42.     protected void launchTargetActivity() {  
    43.         PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(  
    44.                 mDexPath, 1);  
    45.         if ((packageInfo.activities != null)  
    46.                 && (packageInfo.activities.length > 0)) {  
    47.             String activityName = packageInfo.activities[0].name;  
    48.             mClass = activityName;  
    49.             launchTargetActivity(mClass);  
    50.         }  
    51.     }  
    52.   
    53.     @SuppressLint("NewApi")  
    54.     protected void launchTargetActivity(final String className) {  
    55.         Log.d(TAG, "start launchTargetActivity, className=" + className);  
    56.         File dexOutputDir = this.getDir("dex"0);  
    57.         final String dexOutputPath = dexOutputDir.getAbsolutePath();  
    58.         ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();  
    59.         DexClassLoader dexClassLoader = new DexClassLoader(mDexPath,  
    60.                 dexOutputPath, null, localClassLoader);  
    61.         try {  
    62.             Class<?> localClass = dexClassLoader.loadClass(className);  
    63.             Constructor<?> localConstructor = localClass  
    64.                     .getConstructor(new Class[] {});  
    65.             Object instance = localConstructor.newInstance(new Object[] {});  
    66.             Log.d(TAG, "instance = " + instance);  
    67.   
    68.             Method setProxy = localClass.getMethod("setProxy",  
    69.                     new Class[] { Activity.class });  
    70.             setProxy.setAccessible(true);  
    71.             setProxy.invoke(instance, new Object[] { this });  
    72.   
    73.             Method onCreate = localClass.getDeclaredMethod("onCreate",  
    74.                     new Class[] { Bundle.class });  
    75.             onCreate.setAccessible(true);  
    76.             Bundle bundle = new Bundle();  
    77.             bundle.putInt(FROM, FROM_EXTERNAL);  
    78.             onCreate.invoke(instance, new Object[] { bundle });  
    79.         } catch (Exception e) {  
    80.             e.printStackTrace();  
    81.         }  
    82.     }  
    83.   
    84. }  

    说明:程序不难理解,思路是这样的:采用DexClassLoader去加载apk,然后如果没有指定class,就调起主activity,否则调起指定的class。activity被调起的过程是这样的:首先通过类加载器去加载apk中activity的类并创建一个新对象,然后通过反射去调用这个对象的setProxy方法和onCreate方法,setProxy方法的作用是将activity内部的执行全部交由宿主程序中的proxy(也是一个activity),onCreate方法是activity的入口,setProxy以后就调用onCreate方法,这个时候activity就被调起来了。

    待执行apk的实现

    1. 为了让proxy全面接管apk中所有activity的执行,需要为activity定义一个基类BaseActivity,在基类中处理代理相关的事情,同时BaseActivity还对是否使用代理进行了判断,如果不使用代理,那么activity的逻辑仍然按照正常的方式执行,也就是说,这个apk既可以按照执行,也可以由宿主程序来执行。

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. package com.ryg.dynamicloadclient;  
    2.   
    3. import android.app.Activity;  
    4. import android.content.Intent;  
    5. import android.os.Bundle;  
    6. import android.util.Log;  
    7. import android.view.View;  
    8. import android.view.ViewGroup.LayoutParams;  
    9.   
    10. public class BaseActivity extends Activity {  
    11.   
    12.     private static final String TAG = "Client-BaseActivity";  
    13.   
    14.     public static final String FROM = "extra.from";  
    15.     public static final int FROM_EXTERNAL = 0;  
    16.     public static final int FROM_INTERNAL = 1;  
    17.     public static final String EXTRA_DEX_PATH = "extra.dex.path";  
    18.     public static final String EXTRA_CLASS = "extra.class";  
    19.   
    20.     public static final String PROXY_VIEW_ACTION = "com.ryg.dynamicloadhost.VIEW";  
    21.     public static final String DEX_PATH = "/mnt/sdcard/DynamicLoadHost/plugin.apk";  
    22.   
    23.     protected Activity mProxyActivity;  
    24.     protected int mFrom = FROM_INTERNAL;  
    25.   
    26.     public void setProxy(Activity proxyActivity) {  
    27.         Log.d(TAG, "setProxy: proxyActivity= " + proxyActivity);  
    28.         mProxyActivity = proxyActivity;  
    29.     }  
    30.   
    31.     @Override  
    32.     protected void onCreate(Bundle savedInstanceState) {  
    33.         if (savedInstanceState != null) {  
    34.             mFrom = savedInstanceState.getInt(FROM, FROM_INTERNAL);  
    35.         }  
    36.         if (mFrom == FROM_INTERNAL) {  
    37.             super.onCreate(savedInstanceState);  
    38.             mProxyActivity = this;  
    39.         }  
    40.         Log.d(TAG, "onCreate: from= " + mFrom);  
    41.     }  
    42.   
    43.     protected void startActivityByProxy(String className) {  
    44.         if (mProxyActivity == this) {  
    45.             Intent intent = new Intent();  
    46.             intent.setClassName(this, className);  
    47.             this.startActivity(intent);  
    48.         } else {  
    49.             Intent intent = new Intent(PROXY_VIEW_ACTION);  
    50.             intent.putExtra(EXTRA_DEX_PATH, DEX_PATH);  
    51.             intent.putExtra(EXTRA_CLASS, className);  
    52.             mProxyActivity.startActivity(intent);  
    53.         }  
    54.     }  
    55.   
    56.     @Override  
    57.     public void setContentView(View view) {  
    58.         if (mProxyActivity == this) {  
    59.             super.setContentView(view);  
    60.         } else {  
    61.             mProxyActivity.setContentView(view);  
    62.         }  
    63.     }  
    64.   
    65.     @Override  
    66.     public void setContentView(View view, LayoutParams params) {  
    67.         if (mProxyActivity == this) {  
    68.             super.setContentView(view, params);  
    69.         } else {  
    70.             mProxyActivity.setContentView(view, params);  
    71.         }  
    72.     }  
    73.   
    74.     @Deprecated  
    75.     @Override  
    76.     public void setContentView(int layoutResID) {  
    77.         if (mProxyActivity == this) {  
    78.             super.setContentView(layoutResID);  
    79.         } else {  
    80.             mProxyActivity.setContentView(layoutResID);  
    81.         }  
    82.     }  
    83.   
    84.     @Override  
    85.     public void addContentView(View view, LayoutParams params) {  
    86.         if (mProxyActivity == this) {  
    87.             super.addContentView(view, params);  
    88.         } else {  
    89.             mProxyActivity.addContentView(view, params);  
    90.         }  
    91.     }  
    92. }  

    说明:相信大家一看代码就明白了,其中setProxy方法的作用就是为了让宿主程序能够接管自己的执行,一旦被接管以后,其所有的执行均通过proxy,且Context也变成了宿主程序的Context,也许这么说比较形象:宿主程序其实就是个空壳,它只是把其它apk加载到自己的内部去执行,这也就更能理解为什么资源访问变得很困难,你会发现好像访问不到apk中的资源了,的确是这样的,但是目前我还没有很好的方法去解决。
    2. 入口activity的实现

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. public class MainActivity extends BaseActivity {  
    2.   
    3.     private static final String TAG = "Client-MainActivity";  
    4.   
    5.     @Override  
    6.     protected void onCreate(Bundle savedInstanceState) {  
    7.         super.onCreate(savedInstanceState);  
    8.         initView(savedInstanceState);  
    9.     }  
    10.   
    11.     private void initView(Bundle savedInstanceState) {  
    12.         mProxyActivity.setContentView(generateContentView(mProxyActivity));  
    13.     }  
    14.   
    15.     private View generateContentView(final Context context) {  
    16.         LinearLayout layout = new LinearLayout(context);  
    17.         layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,  
    18.                 LayoutParams.MATCH_PARENT));  
    19.         layout.setBackgroundColor(Color.parseColor("#F79AB5"));  
    20.         Button button = new Button(context);  
    21.         button.setText("button");  
    22.         layout.addView(button, LayoutParams.MATCH_PARENT,  
    23.                 LayoutParams.WRAP_CONTENT);  
    24.         button.setOnClickListener(new OnClickListener() {  
    25.             @Override  
    26.             public void onClick(View v) {  
    27.                 Toast.makeText(context, "you clicked button",  
    28.                         Toast.LENGTH_SHORT).show();  
    29.                 startActivityByProxy("com.ryg.dynamicloadclient.TestActivity");  
    30.             }  
    31.         });  
    32.         return layout;  
    33.     }  
    34.   
    35. }  

    说明:由于访问不到apk中的资源了,所以界面是代码写的,而不是写在xml中,因为xml读不到了,这也是个大问题。注意到主界面中有一个button,点击后跳到了另一个activity,这个时候是不能直接调用系统的startActivity方法的,而是必须通过宿主程序中的proxy来执行,原因很简单,首先apk本书没有Context,所以它无法调起activity,另外由于这个子activity是apk中的,通过宿主程序直接调用它也是不行的,因为它对宿主程序来说是不可见的,所以只能通过proxy来调用,是不是感觉很麻烦?但是,你还有更好的办法吗?

    3. 子activity的实现

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. package com.ryg.dynamicloadclient;  
    2.   
    3. import android.graphics.Color;  
    4. import android.os.Bundle;  
    5. import android.view.ViewGroup.LayoutParams;  
    6. import android.widget.Button;  
    7.   
    8. public class TestActivity extends BaseActivity{  
    9.   
    10.     @Override  
    11.     protected void onCreate(Bundle savedInstanceState) {  
    12.         super.onCreate(savedInstanceState);  
    13.         Button button = new Button(mProxyActivity);  
    14.         button.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,  
    15.                 LayoutParams.MATCH_PARENT));  
    16.         button.setBackgroundColor(Color.YELLOW);  
    17.         button.setText("这是测试页面");  
    18.         setContentView(button);  
    19.     }  
    20.   
    21. }  

    说明:代码很简单,不用介绍了,同理,界面还是用代码来写的。

    运行效果

    1. 首先看apk安装时的运行效果

    2. 再看看未安装时被宿主程序执行的效果

    说明:可以发现,安装和未安装,执行效果是一样的,差别在于:首先未安装的时候由于采用了反射,所以执行效率会略微降低,其次,应用的标题发生了改变,也就是说,尽管apk被执行了,但是它毕竟是在宿主程序里面执行的,所以它还是属于宿主程序的,因此apk未安装被执行时其标题不是自己的,不过这也可以间接证明,apk的确被宿主程序执行了,不信看标题。最后,我想说一下这么做的意义,这样做有利于实现模块化,同时还可以实现插件机制,但是问题还是很多的,最复杂的两个问题:资源的访问和activity生命周期的管理,期待大家有好的解决办法,欢迎交流。

    代码下载:

    https://github.com/singwhatiwanna/dynamic-load-apk

    http://download.csdn.net/detail/singwhatiwanna/7121505

  • 相关阅读:
    wpf读取mysql字段类型为text的问题
    设计模式简介
    为 RESTful API 配置 CORS 实现跨域请求
    js FileReader 读取文件
    js读取文件fileReader
    制作svg动态图形效果
    H5与Native交互之JSBridge技术
    位(bit)、字节(byte)、字符、编码之间的关系
    node.js的net模块实现socket通信
    Flexbox如何将页面底部固定在屏幕最下方
  • 原文地址:https://www.cnblogs.com/lee0oo0/p/3665013.html
Copyright © 2011-2022 走看看