• 通过代理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

    创建ButtonToast完毕呢,就是需要设置到某个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 oast控件

     //利用反射机制获取到设置代理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

    这里写图片描述

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

  • 相关阅读:
    ab测试curl json语句
    Python进阶-----静态方法@property,@classmethod,@staticmethod【转】
    staticmethod写和不写有什么区别?【转】
    使用 febootstrap 制作自定义基础镜像【转】
    k8s
    Linux服务器扩容VG时报错 Couldn't create temporary archive name.
    2020年的一些思考和总结
    谈谈FTP
    NET::ERR_INCOMPLETE_CHUNKED_ENCODING 200 (OK)
    Centos7 cache/buff过高处理方法
  • 原文地址:https://www.cnblogs.com/vegetate/p/9997260.html
Copyright © 2020-2023  润新知