通过代理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);
        }
    }
}

 

 

简单说下里面的逻辑关系 
这个界面做了什么呢?就是创建了一个以某个Activity为环境的Button,然后点击button,会产生一个以某个Activity为环境的Toast,我们先抛开让测试的apk进行通过动态加载的方式,以代理Activity调起的情况,首先它是一个可以独立运行编译的apk文件,所以说,我们先来分析下它需要的Activity,我们先定义一个Activity类

protected Activity mProxyActivity;

 

在onCreate方法中

if (mProxyActivity == null) {
            super.onCreate(savedInstanceState);
            mProxyActivity = this;
        }

意思就是如果没有托管的Activity类,就使用原生的Activity,那么如果有托管的Activity呢?我们就进行如下的设置

public void setProxy(Activity proxyActivity) {
        mProxyActivity = proxyActivity;
    }

在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();
            }
        });

创建Button\Toast完毕呢,就是需要设置到某个Activity环境下,这里还得需要判断是原生的Activity环境,还是代理Activity环境?

if (mProxyActivity == this) {
            super.setContentView(button);
        } else {
            mProxyActivity.setContentView(button);
        }

然后编译后,运行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>

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);
            }

        });
    }
}

先来看下主界面的代码,这里就是一个button,点击后带过去一个sd卡跟目录下那个apk1的绝对路径,然后调到ProxyActivity类

String root = android.os.Environment.getExternalStorageDirectory().getPath()
                        + "/TargetProject.apk";

然后就看我们的代理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();  
        }  
    }  

}

 

来说下上面的代码意思 
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;   
            }  
        }

log输出如下

ProxyActivity(23526): com.example.targetproject.MainActivity

然后就看下launchTargetActivity里面的代码

File dexOutputDir = this.getDir("dex", 0); 
dexOutputPath--/data/data/com.example.test/app_dex 

以上是dex解压释放后的目录 ,log输出的目录,以下是截图 

然后获取一个DexClassLoader,这里面参数为sd卡apk1的绝对路径、app_dex路径,然后还有一个ClassLoader.getSystemClassLoader()对象 
参数如下

(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)

如下方法就是获取apk1中MainActivity的构造然后new一个实例

Class<?> localClass = dexClassLoader.loadClass(className);  
            Constructor<?> localConstructor = localClass.getConstructor(new Class[] {});  
            Object instance = localConstructor.newInstance(new Object[] {});

以下就是反射去获取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 });  

这里写图片描述

以上就是这个小例子的逻辑,也算是对自己开启动态加载学习的一个小入门理解把

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值