1. 程式人生 > >Android apk動態載入機制的研究

Android apk動態載入機制的研究

背景

問題是這樣的:我們知道,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從網上下載到本地再載入其實是一個道理。

    @Override
    public void onClick(View v) {
        if (v == mOpenClient) {
            Intent intent = new Intent(this, ProxyActivity.class);
            intent.putExtra(ProxyActivity.EXTRA_DEX_PATH, "/mnt/sdcard/DynamicLoadHost/plugin.apk");
            startActivity(intent);
        }

    }

點選button以後,proxy會被調起,然後載入apk並調起的任務就交給它了

2. 代理activity的實現(proxy)

package com.ryg.dynamicloadhost;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import dalvik.system.DexClassLoader;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.pm.PackageInfo;
import android.os.Bundle;
import android.util.Log;

public class ProxyActivity extends Activity {

    private static final String TAG = "ProxyActivity";

    public static final String FROM = "extra.from";
    public static final int FROM_EXTERNAL = 0;
    public static final int FROM_INTERNAL = 1;

    public static final String EXTRA_DEX_PATH = "extra.dex.path";
    public static final String EXTRA_CLASS = "extra.class";

    private String mClass;
    private String mDexPath;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mDexPath = getIntent().getStringExtra(EXTRA_DEX_PATH);
        mClass = getIntent().getStringExtra(EXTRA_CLASS);

        Log.d(TAG, "mClass=" + mClass + " mDexPath=" + mDexPath);
        if (mClass == null) {
            launchTargetActivity();
        } else {
            launchTargetActivity(mClass);
        }
    }

    @SuppressLint("NewApi")
    protected void launchTargetActivity() {
        PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(
                mDexPath, 1);
        if ((packageInfo.activities != null)
                && (packageInfo.activities.length > 0)) {
            String activityName = packageInfo.activities[0].name;
            mClass = activityName;
            launchTargetActivity(mClass);
        }
    }

    @SuppressLint("NewApi")
    protected void launchTargetActivity(final String className) {
        Log.d(TAG, "start launchTargetActivity, className=" + className);
        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[] {});
            Log.d(TAG, "instance = " + instance);

            Method setProxy = localClass.getMethod("setProxy",
                    new Class[] { Activity.class });
            setProxy.setAccessible(true);
            setProxy.invoke(instance, new Object[] { this });

            Method onCreate = localClass.getDeclaredMethod("onCreate",
                    new Class[] { Bundle.class });
            onCreate.setAccessible(true);
            Bundle bundle = new Bundle();
            bundle.putInt(FROM, FROM_EXTERNAL);
            onCreate.invoke(instance, new Object[] { bundle });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

說明:程式不難理解,思路是這樣的:採用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既可以按照執行,也可以由宿主程式來執行。

package com.ryg.dynamicloadclient;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup.LayoutParams;

public class BaseActivity extends Activity {

    private static final String TAG = "Client-BaseActivity";

    public static final String FROM = "extra.from";
    public static final int FROM_EXTERNAL = 0;
    public static final int FROM_INTERNAL = 1;
    public static final String EXTRA_DEX_PATH = "extra.dex.path";
    public static final String EXTRA_CLASS = "extra.class";

    public static final String PROXY_VIEW_ACTION = "com.ryg.dynamicloadhost.VIEW";
    public static final String DEX_PATH = "/mnt/sdcard/DynamicLoadHost/plugin.apk";

    protected Activity mProxyActivity;
    protected int mFrom = FROM_INTERNAL;

    public void setProxy(Activity proxyActivity) {
        Log.d(TAG, "setProxy: proxyActivity= " + proxyActivity);
        mProxyActivity = proxyActivity;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        if (savedInstanceState != null) {
            mFrom = savedInstanceState.getInt(FROM, FROM_INTERNAL);
        }
        if (mFrom == FROM_INTERNAL) {
            super.onCreate(savedInstanceState);
            mProxyActivity = this;
        }
        Log.d(TAG, "onCreate: from= " + mFrom);
    }

    protected void startActivityByProxy(String className) {
        if (mProxyActivity == this) {
            Intent intent = new Intent();
            intent.setClassName(this, className);
            this.startActivity(intent);
        } else {
            Intent intent = new Intent(PROXY_VIEW_ACTION);
            intent.putExtra(EXTRA_DEX_PATH, DEX_PATH);
            intent.putExtra(EXTRA_CLASS, className);
            mProxyActivity.startActivity(intent);
        }
    }

    @Override
    public void setContentView(View view) {
        if (mProxyActivity == this) {
            super.setContentView(view);
        } else {
            mProxyActivity.setContentView(view);
        }
    }

    @Override
    public void setContentView(View view, LayoutParams params) {
        if (mProxyActivity == this) {
            super.setContentView(view, params);
        } else {
            mProxyActivity.setContentView(view, params);
        }
    }

    @Deprecated
    @Override
    public void setContentView(int layoutResID) {
        if (mProxyActivity == this) {
            super.setContentView(layoutResID);
        } else {
            mProxyActivity.setContentView(layoutResID);
        }
    }

    @Override
    public void addContentView(View view, LayoutParams params) {
        if (mProxyActivity == this) {
            super.addContentView(view, params);
        } else {
            mProxyActivity.addContentView(view, params);
        }
    }
}

說明:相信大家一看程式碼就明白了,其中setProxy方法的作用就是為了讓宿主程式能夠接管自己的執行,一旦被接管以後,其所有的執行均通過proxy,且Context也變成了宿主程式的Context,也許這麼說比較形象:宿主程式其實就是個空殼,它只是把其它apk載入到自己的內部去執行,這也就更能理解為什麼資源訪問變得很困難,你會發現好像訪問不到apk中的資源了,的確是這樣的,但是目前我還沒有很好的方法去解決。
2. 入口activity的實現

public class MainActivity extends BaseActivity {

    private static final String TAG = "Client-MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initView(savedInstanceState);
    }

    private void initView(Bundle savedInstanceState) {
        mProxyActivity.setContentView(generateContentView(mProxyActivity));
    }

    private View generateContentView(final Context context) {
        LinearLayout layout = new LinearLayout(context);
        layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT));
        layout.setBackgroundColor(Color.parseColor("#F79AB5"));
        Button button = new Button(context);
        button.setText("button");
        layout.addView(button, LayoutParams.MATCH_PARENT,
                LayoutParams.WRAP_CONTENT);
        button.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(context, "you clicked button",
                        Toast.LENGTH_SHORT).show();
                startActivityByProxy("com.ryg.dynamicloadclient.TestActivity");
            }
        });
        return layout;
    }

}

說明:由於訪問不到apk中的資源了,所以介面是程式碼寫的,而不是寫在xml中,因為xml讀不到了,這也是個大問題。注意到主介面中有一個button,點選後跳到了另一個activity,這個時候是不能直接呼叫系統的startActivity方法的,而是必須通過宿主程式中的proxy來執行,原因很簡單,首先apk本書沒有Context,所以它無法調起activity,另外由於這個子activity是apk中的,通過宿主程式直接呼叫它也是不行的,因為它對宿主程式來說是不可見的,所以只能通過proxy來呼叫,是不是感覺很麻煩?但是,你還有更好的辦法嗎?

3. 子activity的實現

package com.ryg.dynamicloadclient;

import android.graphics.Color;
import android.os.Bundle;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;

public class TestActivity extends BaseActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Button button = new Button(mProxyActivity);
        button.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT));
        button.setBackgroundColor(Color.YELLOW);
        button.setText("這是測試頁面");
        setContentView(button);
    }

}

說明:程式碼很簡單,不用介紹了,同理,介面還是用程式碼來寫的。

執行效果

1. 首先看apk安裝時的執行效果

2. 再看看未安裝時被宿主程式執行的效果

說明:可以發現,安裝和未安裝,執行效果是一樣的,差別在於:首先未安裝的時候由於採用了反射,所以執行效率會略微降低,其次,應用的標題發生了改變,也就是說,儘管apk被執行了,但是它畢竟是在宿主程式裡面執行的,所以它還是屬於宿主程式的,因此apk未安裝被執行時其標題不是自己的,不過這也可以間接證明,apk的確被宿主程式執行了,不信看標題。最後,我想說一下這麼做的意義,這樣做有利於實現模組化,同時還可以實現外掛機制,但是問題還是很多的,最複雜的兩個問題:資源的訪問和activity生命週期的管理,期待大家有好的解決辦法,歡迎交流。

程式碼下載: