Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 通過代理Activity模式,以移花接木的方式,加載sd卡目錄下的apk界面

通過代理Activity模式,以移花接木的方式,加載sd卡目錄下的apk界面

編輯:關於Android編程

動態加載、插件化開發很重要

當今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的代碼邏輯把

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

這裡寫圖片描述

以上就是這個小例子的邏輯,也算是對自己開啟動態加載學習的一個小入門理解把

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved