通过代理Activity模式,以移花接木的方式,加载sd卡目录下的apk界面
动态加载、插件化开发很重要
当今360手机助手(DroidPlugin),个人开源(VirtualApp)、百度DL、携程DynamicAPK都用到了该技术
本例的大概思路是:
1、apk1初始化就一个主界面MainActivity,主界面只有一个Button按钮,点击后,弹出Toast,然后我们把编译好的apk1放到手机根目录SD卡下
2、apk2有一个MainActivity界面,界面上也有一个Button,点击按钮后,去加载SD目录下的apk1,调起来apk1,点击apk1中的button,弹出Toast即可
以上就是一个简单的逻辑?其实呢这里面问题好多,这里先简单说下问题点。
1、其实点击apk2的button启动的不是apk1的界面,而是将apk1的界面托管给一个静态代理类Activity,然后以静态代理Activity去构建类似于apk1的button,继而在静态代理Activity的上下文环境下,弹出Toast
2、这个例子只是在静态代理Activity类里,进行了简单的反射调用apk1的onCreate方法,被反射的apk1的主界面类,其实本质是一个java类,它没有Activity里面的逻辑,比如你拿不到里面的layout等资源,所以这个demo也就是一个对动态加载的一个小小的理解,没有涉及到Activity4大组建的动态代理、binder机制等
首先来看下apk的代码把
MainActivity
package com.example.targetproject;
import android.annotation.SuppressLint;
import android.app.ActionBar.LayoutParams;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends Activity {
public static final String KEY_APK_PATH = "apkPath";
public static final String KEY_CLASS = "class";
public static final String DEX_PATH = android.os.Environment
.getExternalStorageDirectory().getPath() + "/TargetProject.apk";
protected Activity mProxyActivity;
public void setProxy(Activity proxyActivity) {
mProxyActivity = proxyActivity;
}
@SuppressLint("NewApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
if (mProxyActivity == null) {
super.onCreate(savedInstanceState);
mProxyActivity = this;
}
Button button = new Button(mProxyActivity);
button.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT));
button.setText("按我");
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(mProxyActivity, "你点击了按钮啦!", Toast.LENGTH_SHORT)
.show();
}
});
if (mProxyActivity == this) {
super.setContentView(button);
} else {
mProxyActivity.setContentView(button);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
简单说下里面的逻辑关系
这个界面做了什么呢?就是创建了一个以某个Activity为环境的Button,然后点击button,会产生一个以某个Activity为环境的Toast,我们先抛开让测试的apk进行通过动态加载的方式,以代理Activity调起的情况,首先它是一个可以独立运行编译的apk文件,所以说,我们先来分析下它需要的Activity,我们先定义一个Activity类
protected Activity mProxyActivity;
- 1
- 1
在onCreate方法中
if (mProxyActivity == null) {
super.onCreate(savedInstanceState);
mProxyActivity = this;
}
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
意思就是如果没有托管的Activity类,就使用原生的Activity,那么如果有托管的Activity呢?我们就进行如下的设置
public void setProxy(Activity proxyActivity) {
mProxyActivity = proxyActivity;
}
- 1
- 2
- 3
- 1
- 2
- 3
在apk的主界面,加入一个代理类Activity,然后在代理类Activity环境下去创建button,Toast
以下就是创建Button的代码
Button button = new Button(mProxyActivity);
button.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT));
button.setText("按我");
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(mProxyActivity, "你点击了按钮啦!", Toast.LENGTH_SHORT)
.show();
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
创建Button\Toast完毕呢,就是需要设置到某个Activity环境下,这里还得需要判断是原生的Activity环境,还是代理Activity环境?
if (mProxyActivity == this) {
super.setContentView(button);
} else {
mProxyActivity.setContentView(button);
}
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
然后编译后,运行apk没问题,就放到手机的sd卡根目录下,以下是我手机nexus5的路径目录
然后我们就来看测试apk的代码逻辑把
<RelativeLayout 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"
tools:context="com.example.test.MainActivity" >
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/load" />
</RelativeLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
MainActivity
package com.example.test;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn = (Button) this.findViewById(R.id.btn);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String root = android.os.Environment
.getExternalStorageDirectory().getPath()
+ "/TargetProject.apk";
Intent intent = new Intent(MainActivity.this,
ProxyActivity.class);
intent.putExtra(ProxyActivity.KEY_APK_PATH, root);
startActivity(intent);
}
});
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
先来看下主界面的代码,这里就是一个button,点击后带过去一个sd卡跟目录下那个apk1的绝对路径,然后调到ProxyActivity类
String root = android.os.Environment.getExternalStorageDirectory().getPath()
+ "/TargetProject.apk";
- 1
- 2
- 1
- 2
然后就看我们的代理Activity类
package com.example.test;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.os.Bundle;
import android.util.Log;
import dalvik.system.DexClassLoader;
/**
* 代理类
* @author safly
*
*/
public class ProxyActivity extends Activity{
public static final String KEY_APK_PATH = "apkPath";
public static final String KEY_CLASS = "class";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//获取指定的apk文件路径和启动类名
String mDexPath = getIntent().getStringExtra(KEY_APK_PATH);
String mClass = getIntent().getStringExtra(KEY_CLASS);
if (mClass == null) {
PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(mDexPath, 1);
if ((packageInfo.activities != null) && (packageInfo.activities.length > 0)) {
Log.e("ProxyActivity", ""+packageInfo.activities[0].name);
mClass = packageInfo.activities[0].name;
}
}
launchTargetActivity(mDexPath,mClass);
}
/**
* 利用ClassLoader,DexClassLoader和反射将apk中的界面启动
* @param mDexPath apk 动态加载的apk本地路径
* @param className 要打开的动态加载类的类名
*/
protected void launchTargetActivity(String mDexPath,String className) {
Log.e("ProxyActivity", "launchTargetActivity");
File dexOutputDir = this.getDir("dex", 0);
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[] {});
//利用反射机制获取到设置代理Activity的方法
Method setProxy = localClass.getMethod("setProxy",new Class[] { Activity.class });
setProxy.setAccessible(true);
setProxy.invoke(instance, new Object[] { this });
//利用反射机制调用onCreate方法
Method onCreate = localClass.getDeclaredMethod("onCreate",new Class[] { Bundle.class });
onCreate.setAccessible(true);
onCreate.invoke(instance, new Object[] { null });
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
来说下上面的代码意思
ProxyActivity类onCreate方法中获取传递过来的KEY_APK_PATH(sd卡下apk1的绝对路径),我们还需要一个类,就是apk1的主界面的全类名,因为我们需要调用里面的onCreate方法,然后去添加button、toast控件
if (mClass == null) {
PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(mDexPath, 1);
if ((packageInfo.activities != null) && (packageInfo.activities.length > 0)) {
Log.e("ProxyActivity", ""+packageInfo.activities[0].name);
mClass = packageInfo.activities[0].name;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
log输出如下
ProxyActivity(23526): com.example.targetproject.MainActivity
- 1
- 1
然后就看下launchTargetActivity里面的代码
File dexOutputDir = this.getDir("dex", 0);
dexOutputPath--/data/data/com.example.test/app_dex
- 1
- 2
- 1
- 2
以上是dex解压释放后的目录 ,log输出的目录,以下是截图
然后获取一个DexClassLoader,这里面参数为sd卡apk1的绝对路径、app_dex路径,然后还有一个ClassLoader.getSystemClassLoader()对象
参数如下
(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
- 1
- 1
如下方法就是获取apk1中MainActivity的构造然后new一个实例
Class<?> localClass = dexClassLoader.loadClass(className);
Constructor<?> localConstructor = localClass.getConstructor(new Class[] {});
Object instance = localConstructor.newInstance(new Object[] {});
- 1
- 2
- 3
- 1
- 2
- 3
以下就是反射去获取setProxy,onCreate方法,进行设置代理Activity类,然后在代理Activity类中进行设置button\toast控件
//利用反射机制获取到设置代理Activity的方法
Method setProxy = localClass.getMethod("setProxy",new Class[] { Activity.class });
setProxy.setAccessible(true);
setProxy.invoke(instance, new Object[] { this });
//利用反射机制调用onCreate方法
Method onCreate = localClass.getDeclaredMethod("onCreate",new Class[] { Bundle.class });
onCreate.setAccessible(true);
onCreate.invoke(instance, new Object[] { null });
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
以上就是这个小例子的逻辑,也算是对自己开启动态加载学习的一个小入门理解把