Example for Custom Class Loading in Dalvik

依照《Custom Class Loading in Dalvik》文章中所写方法,写了一个测试project和ant build脚本,成功build出第二个dex并动态加载运行,贴出代码和脚本

总共有这么几个源文件,其中lib目录下的内容是要放到第二个Dex中去的

./src/com
./src/com/lib
./src/com/lib/MyActivity.java
./src/com/lib/MyFragment.java
./src/com/lib/Utils.java
./src/com/test
./src/com/test/MainActivity.java
./src/com/test/UtilsInterface.java

 

MainActivity.java

package com.test;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.Menu;
import android.widget.TextView;
import dalvik.system.DexClassLoader;

public class MainActivity extends FragmentActivity {
private static final String SECONDARY_DEX_NAME = “secondary_dex.jar”;
private TextView mTextView;

private DexClassLoader mDexClassLoader;
private UtilsInterface mUtilsInterface;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mTextView = (TextView) findViewById(R.id.text);
    test();
    fragment();
    //activity();
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}

private void test() {
    File dexInternalStoragePath = new File(getDir("dex",
            Context.MODE_PRIVATE), SECONDARY_DEX_NAME);
    Log.d("jfo",
            "dexInternalStoragePath->"
                    + dexInternalStoragePath.getAbsolutePath());
    BufferedInputStream bis = null;
    OutputStream dexWriter = null;
    final int BUF_SIZE = 8 * 1024;
    try {
        bis = new BufferedInputStream(getAssets().open(SECONDARY_DEX_NAME));
        dexWriter = new BufferedOutputStream(new FileOutputStream(
                dexInternalStoragePath));
        byte[] buf = new byte[BUF_SIZE];
        int len;
        while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
            dexWriter.write(buf, 0, len);
        }
        dexWriter.close();
        bis.close();

    } catch (Exception e) {
        e.printStackTrace();
    }

    final File optimizedDexOutputPath = getDir("outdex",
            Context.MODE_PRIVATE);
    Log.d("jfo",
            "optimizedDexOutputPath->"
                    + optimizedDexOutputPath.getAbsolutePath());
    DexClassLoader cl = new DexClassLoader(
            dexInternalStoragePath.getAbsolutePath(),
            optimizedDexOutputPath.getAbsolutePath(), null,
            getClassLoader());
    mDexClassLoader = cl;
    Class libProviderClazz = null;
    try {
        // Load the library.
        libProviderClazz = cl.loadClass("com.lib.Utils");
        // Cast the return object to the library interface so that the
        // caller can directly invoke methods in the interface.
        // Alternatively, the caller can invoke methods through reflection,
        // which is more verbose.
        UtilsInterface lib = (UtilsInterface) libProviderClazz
                .newInstance();
        mUtilsInterface = lib;
        String r = lib.sayHello();
        mTextView.setText(r);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

private void fragment() {
    mUtilsInterface.createMyFragment(this, R.id.fragment_container);
}

private void activity() {

// this.getApplication().getApplicationContext().getPackageManager().getActivityInfo(getComponentName(), 0).
mUtilsInterface.startMyActivity(this);
}
}
UtilsInterface.java

package com.test;

import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;

public interface UtilsInterface {
public String sayHello();
public Fragment createMyFragment(FragmentActivity act, int layoutId);
public void startMyActivity(Context context);
}
Utils.java

package com.lib;

import android.content.Context;
import android.content.Intent;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;

import com.test.UtilsInterface;

public class Utils implements UtilsInterface {
public String sayHello() {
Log.d(“Utils”, “Hello world!”);
return “Hello world!”;
}

public Fragment createMyFragment(FragmentActivity act, int layoutId) {
    FragmentManager fm = act.getSupportFragmentManager();
    FragmentTransaction ft = fm.beginTransaction();
    MyFragment fragment = new MyFragment();
    ft.add(layoutId, fragment);
    ft.commit();
    return fragment;
}

public void startMyActivity(Context context) {
    Intent intent = new Intent(context, MyActivity.class);
    context.startActivity(intent);
}

}
MyFragment.java

package com.lib;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.test.R;

public class MyFragment extends Fragment {

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
}

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

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_layout, container, false);
}

@Override
public void onDestroy() {
    super.onDestroy();
}

@Override
public void onResume() {
    super.onResume();
}

}
MyActivity.java

package com.lib;

import android.app.Activity;
import android.os.Bundle;

import com.test.R;

public class MyActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.fragment_layout);
}

}
activity_main.xml

<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”
android:paddingBottom=”@dimen/activity_vertical_margin”
android:paddingLeft=”@dimen/activity_horizontal_margin”
android:paddingRight=”@dimen/activity_horizontal_margin”
android:paddingTop=”@dimen/activity_vertical_margin”
tools:context=”.MainActivity” >

&lt;TextView
    android:id="@+id/text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_world" /&gt;

&lt;LinearLayout
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_below="@id/text" /&gt;

</RelativeLayout>
fragment_layout.xml

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:background=”#cccccc”
android:minHeight=”100dp” >

&lt;TextView
    android:id="@+id/text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="This is fragment..." /&gt;

</RelativeLayout>
AndroidManifest.xml

<?xml version=”1.0” encoding=”utf-8”?>
<manifest xmlns:android=”http://schemas.android.com/apk/res/android
package=”com.test”
android:versionCode=”1”
android:versionName=”1.0” >

&lt;uses-sdk
    android:minSdkVersion="8"
    android:targetSdkVersion="17" /&gt;

&lt;application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" &gt;
    &lt;activity
        android:name="com.test.MainActivity"
        android:label="@string/app_name" &gt;
        &lt;intent-filter&gt;
            &lt;action android:name="android.intent.action.MAIN" /&gt;
            &lt;category android:name="android.intent.category.LAUNCHER" /&gt;
        &lt;/intent-filter&gt;
    &lt;/activity&gt;
    &lt;activity android:name="com.lib.MyActivity" android:label="@string/app_name" /&gt;
    &lt;/application&gt;

</manifest>
 

ant build脚本build.xml

<?xml version=”1.0” encoding=”UTF-8”?>
<project name=”MainActivity” default=”help”>

&lt;!-- The local.properties file is created and updated by the 'android' tool.
     It contains the path to the SDK. It should *NOT* be checked into
     Version Control Systems. --&gt;
&lt;property file="local.properties" /&gt;

&lt;!-- The ant.properties file can be created by you. It is only edited by the
     'android' tool to add properties to it.
     This is the place to change some Ant specific build properties.
     Here are some properties you may want to change/update:

     source.dir
         The name of the source directory. Default is 'src'.
     out.dir
         The name of the output directory. Default is 'bin'.

     For other overridable properties, look at the beginning of the rules
     files in the SDK, at tools/ant/build.xml

     Properties related to the SDK location or the project target should
     be updated using the 'android' tool with the 'update' action.

     This file is an integral part of the build system for your
     application and should be checked into Version Control Systems.

     --&gt;
&lt;property file="ant.properties" /&gt;

&lt;!-- if sdk.dir was not set from one of the property file, then
     get it from the ANDROID_HOME env var.
     This must be done before we load project.properties since
     the proguard config can use sdk.dir --&gt;
&lt;property environment="env" /&gt;
&lt;condition property="sdk.dir" value="${env.ANDROID_HOME}"&gt;
    &lt;isset property="env.ANDROID_HOME" /&gt;
&lt;/condition&gt;

&lt;!-- The project.properties file is created and updated by the 'android'
     tool, as well as ADT.

     This contains project specific properties such as project target, and library
     dependencies. Lower level build properties are stored in ant.properties
     (or in .classpath for Eclipse projects).

     This file is an integral part of the build system for your
     application and should be checked into Version Control Systems. --&gt;
&lt;loadproperties srcFile="project.properties" /&gt;

&lt;!-- quick check on sdk.dir --&gt;
&lt;fail
        message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
        unless="sdk.dir"
/&gt;

&lt;!--
    Import per project custom build rules if present at the root of the project.
    This is the place to put custom intermediary targets such as:
        -pre-build
        -pre-compile
        -post-compile (This is typically used for code obfuscation.
                       Compiled code location: ${out.classes.absolute.dir}
                       If this is not done in place, override ${out.dex.input.absolute.dir})
        -post-package
        -post-build
        -pre-clean
--&gt;
&lt;import file="custom_rules.xml" optional="true" /&gt;

&lt;!-- Import the actual build file.

     To customize existing targets, there are two options:
     - Customize only one target:
         - copy/paste the target into this file, *before* the
           &lt;import&gt; task.
         - customize it to your needs.
     - Customize the whole content of build.xml
         - copy/paste the content of the rules files (minus the top node)
           into this file, replacing the &lt;import&gt; task.
         - customize to your needs.

     ***********************
     ****** IMPORTANT ******
     ***********************
     In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
     in order to avoid having your file be overridden by tools such as "android update project"
--&gt;

&lt;macrodef name="dex-helper-mod"&gt;
    &lt;attribute name="input-dir" /&gt;
    &lt;attribute name="output-dex-file" /&gt;
    &lt;element name="external-libs" optional="yes" /&gt;
    &lt;attribute name="nolocals" default="false" /&gt;
    &lt;sequential&gt;
        &lt;!-- sets the primary input for dex. If a pre-dex task sets it to
             something else this has no effect --&gt;
        &lt;!--property name="out.dex.input.absolute.dir" value="${out.classes.absolute.dir}" /--&gt;
        &lt;!--property name="out.dex.input.absolute.dir" value="@{input-dir}" /--&gt;

        &lt;!-- set the secondary dx input: the project (and library) jar files
             If a pre-dex task sets it to something else this has no effect --&gt;
        &lt;if&gt;
            &lt;condition&gt;
                &lt;isreference refid="out.dex.jar.input.ref" /&gt;
            &lt;/condition&gt;
            &lt;else&gt;
                &lt;path id="out.dex.jar.input.ref"&gt;
                    &lt;path refid="project.all.jars.path" /&gt;
                &lt;/path&gt;
            &lt;/else&gt;
        &lt;/if&gt;

        &lt;echo&gt;Converting compiled files and external libraries into @{output-dex-file}...&lt;/echo&gt;
        &lt;dex executable="${dx}"
                output="@{output-dex-file}"
                dexedlibs="${out.dexed.absolute.dir}"
                nolocals="@{nolocals}"
                forceJumbo="${dex.force.jumbo}"
                disableDexMerger="${dex.disable.merger}"
                verbose="${verbose}"&gt;
            &lt;path path="@{input-dir}"/&gt;
            &lt;path refid="out.dex.jar.input.ref" /&gt;
            &lt;external-libs /&gt;
        &lt;/dex&gt;
    &lt;/sequential&gt;
&lt;/macrodef&gt;

&lt;macrodef name="dex-helper-mod2"&gt;
    &lt;attribute name="input-dir" /&gt;
    &lt;attribute name="output-dex-file" /&gt;
    &lt;element name="external-libs" optional="yes" /&gt;
    &lt;attribute name="nolocals" default="false" /&gt;
    &lt;sequential&gt;
        &lt;!-- sets the primary input for dex. If a pre-dex task sets it to
             something else this has no effect --&gt;
        &lt;!--property name="out.dex.input.absolute.dir" value="${out.classes.absolute.dir}" /--&gt;
        &lt;!--property name="out.dex.input.absolute.dir" value="@{input-dir}" /--&gt;

        &lt;!-- set the secondary dx input: the project (and library) jar files
             If a pre-dex task sets it to something else this has no effect --&gt;
        &lt;if&gt;
            &lt;condition&gt;
                &lt;isreference refid="out.dex.jar.input.ref" /&gt;
            &lt;/condition&gt;
            &lt;else&gt;
                &lt;path id="out.dex.jar.input.ref"&gt;
                    &lt;path refid="project.all.jars.path" /&gt;
                &lt;/path&gt;
            &lt;/else&gt;
        &lt;/if&gt;

        &lt;echo&gt;Converting compiled files and external libraries into @{output-dex-file}...&lt;/echo&gt;
        &lt;dex executable="${dx}"
                output="@{output-dex-file}"
                dexedlibs="${out.dexed.absolute.dir}"
                nolocals="@{nolocals}"
                forceJumbo="${dex.force.jumbo}"
                disableDexMerger="${dex.disable.merger}"
                verbose="${verbose}"&gt;
            &lt;path path="@{input-dir}"/&gt;
            &lt;!--path refid="out.dex.jar.input.ref" /--&gt;
            &lt;external-libs /&gt;
        &lt;/dex&gt;
    &lt;/sequential&gt;
&lt;/macrodef&gt;

&lt;target name="-dex" depends="-compile, -post-compile, -obfuscate"
        unless="do.not.compile"&gt;
    &lt;if condition="${manifest.hasCode}"&gt;
        &lt;then&gt;
            &lt;!-- Create staging directories to store .class files to be converted to the --&gt;
            &lt;!-- default dex and the secondary dex. --&gt;
            &lt;mkdir dir="${out.classes.absolute.dir}.1"/&gt;
            &lt;mkdir dir="${out.classes.absolute.dir}.2"/&gt;

            &lt;!-- Primary dex to include everything but the concrete library implementation. --&gt;
            &lt;copy todir="${out.classes.absolute.dir}.1" &gt;
                &lt;fileset dir="${out.classes.absolute.dir}" &gt;
                    &lt;exclude name="com/lib/**" /&gt;
                &lt;/fileset&gt;
            &lt;/copy&gt;
            &lt;!-- Secondary dex to include the concrete library implementation. --&gt;
            &lt;copy todir="${out.classes.absolute.dir}.2" &gt;
                &lt;fileset dir="${out.classes.absolute.dir}" &gt;
                    &lt;include name="com/lib/**" /&gt;
                &lt;/fileset&gt;
            &lt;/copy&gt;

            &lt;!-- Compile .class files from the two stage directories to the apppropriate dex files. --&gt;
            &lt;dex-helper-mod input-dir="${out.classes.absolute.dir}.1"
                output-dex-file="${out.absolute.dir}/${dex.file.name}" /&gt;
            &lt;mkdir dir="${out.absolute.dir}/secondary_dex_dir" /&gt;
            &lt;dex-helper-mod2 input-dir="${out.classes.absolute.dir}.2"
                output-dex-file="${out.absolute.dir}/secondary_dex_dir/classes.dex" /&gt;
            &lt;!-- Jar the secondary dex file so it can be consumed by the DexClassLoader. --&gt;
            &lt;!-- Package the output in the assets directory of the apk. --&gt;
            &lt;jar destfile="${asset.absolute.dir}/secondary_dex.jar"
                 basedir="${out.absolute.dir}/secondary_dex_dir" includes="classes.dex" /&gt;
        &lt;/then&gt;
        &lt;else&gt;
            &lt;echo&gt;hasCode = false. Skipping...&lt;/echo&gt;
        &lt;/else&gt;
    &lt;/if&gt;
&lt;/target&gt;

&lt;!-- version-tag: 1 --&gt;
&lt;import file="${sdk.dir}/tools/ant/build.xml" /&gt;

</project>
注意build.xml中的dex-helper-mod2,注释掉了<!–path refid=”out.dex.jar.input.ref” /–>,表示build第二个dex时不要包含libs目录下的jar文件,否则加载fragment时会报错

另外在MainActivity中尝试调用activity()动态加载第二个dex中的activity,但是没有成功,错误日志显示ClassNotFoundException

09-09 15:23:01.362: E/AndroidRuntime(16824): java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.test/com.lib.MyActivity}: java.lang.ClassNotFoundException: Didn’t find class “com.lib.MyActivity” on path: /data/app/com.test-1.apk
09-09 15:23:01.362: E/AndroidRuntime(16824): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2220)
09-09 15:23:01.362: E/AndroidRuntime(16824): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2354)
09-09 15:23:01.362: E/AndroidRuntime(16824): at android.app.ActivityThread.access$600(ActivityThread.java:150)
09-09 15:23:01.362: E/AndroidRuntime(16824): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1244)
09-09 15:23:01.362: E/AndroidRuntime(16824): at android.os.Handler.dispatchMessage(Handler.java:99)
09-09 15:23:01.362: E/AndroidRuntime(16824): at android.os.Looper.loop(Looper.java:137)
09-09 15:23:01.362: E/AndroidRuntime(16824): at android.app.ActivityThread.main(ActivityThread.java:5191)
09-09 15:23:01.362: E/AndroidRuntime(16824): at java.lang.reflect.Method.invokeNative(Native Method)
09-09 15:23:01.362: E/AndroidRuntime(16824): at java.lang.reflect.Method.invoke(Method.java:511)
09-09 15:23:01.362: E/AndroidRuntime(16824): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:795)
09-09 15:23:01.362: E/AndroidRuntime(16824): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:562)
09-09 15:23:01.362: E/AndroidRuntime(16824): at dalvik.system.NativeStart.main(Native Method)
09-09 15:23:01.362: E/AndroidRuntime(16824): Caused by: java.lang.ClassNotFoundException: Didn’t find class “com.lib.MyActivity” on path: /data/app/com.test-1.apk
09-09 15:23:01.362: E/AndroidRuntime(16824): at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:65)
09-09 15:23:01.362: E/AndroidRuntime(16824): at java.lang.ClassLoader.loadClass(ClassLoader.java:501)
09-09 15:23:01.362: E/AndroidRuntime(16824): at java.lang.ClassLoader.loadClass(ClassLoader.java:461)
09-09 15:23:01.362: E/AndroidRuntime(16824): at android.app.Instrumentation.newActivity(Instrumentation.java:1054)
09-09 15:23:01.362: E/AndroidRuntime(16824): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2211)
09-09 15:23:01.362: E/AndroidRuntime(16824): … 11 more

 

运行ant debug之后在bin目录下就生成了apk,安装运行,可以看到灰色区域的fragment成功加载了

android动态加载class