2014-10-19

Android DexClassLoader 續

之前介紹的方法讓我們的程式可以在執行時期動態載入其它的程式碼,後來發現 Facebook 的 Buck 當中也有類似的功能 ( Exopackage ),但它實作的方法和之前介紹的方法不同,底下敘述使用 Buck 的方法,並套用在前一篇範例上的流程 :

1. 製作 dex 檔案 :
與前一篇介紹的方法相同

2. 程式執行時期下載 dex 檔案  :
建立一個 com.example.dexclassloader 之 Android Application Project 並將主要 Activity (MainActivity.java ) 替換如下 :
package com.example.dexclassloader;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.Menu;
 
public class MainActivity extends Activity {
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
   
  Thread works =new Thread(){
   @Override
   public void run(){
    //download and save the jar
    downloadDexFile("http://localhost/PayloadActivity.dex.jar", "PayloadActivity.dex.jar");
    
    //install dex jar
    List<File> dexJars =new ArrayList<File>();
    File libFile =new File(getFilesDir()+"/PayloadActivity.dex.jar");
    dexJars.add(libFile);
    SystemClassLoaderAdder.installDexJars(getClassLoader(), getDir("outdex", Context.MODE_PRIVATE), dexJars);
     
    //load the jar
    loadAndInvokeMethod();
   }
  };
  works.start();
 }
 
 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // Inflate the menu; this adds items to the action bar if it is present.
  getMenuInflater().inflate(R.menu.main, menu);
  return true;
 }
  
 private void downloadDexFile(String url, String filename){
     try {
 
         URL u = new URL(url);
         HttpURLConnection c = (HttpURLConnection) u.openConnection();
         c.setRequestMethod("GET");
         c.setDoOutput(true);
         c.connect();
          
         FileOutputStream f =openFileOutput(filename, MODE_PRIVATE);
         InputStream in = c.getInputStream();
 
         byte[] buffer = new byte[1024];
         int len1 = 0;
          
         while ((len1 = in.read(buffer)) > 0) {
             f.write(buffer, 0, len1);
         }
         f.close();
          
     } catch (Exception e) {
         //error dialog
         Log.d("mainactivity", "error -"+e.toString());
 
     }
 }
  
 private void loadAndInvokeMethod(){
  try{  
         //LOAD CLASS
         Class<?> payloadClass =this.getClassLoader().loadClass("com.example.PayloadActivity");
          
         //LOAD METHOD
         Method starterFunc =payloadClass.getMethod("starter", Activity.class);
          
         //INVOKE METHOD
         starterFunc.invoke(null, this);
          
  }catch(Exception e){
    
  }
 }
 
}
觀察得知原本 overrideClassLoader 函數的部分改為呼叫 SystemClassLoaderAdder.installDexJars 函數,此外呼叫載入的 dex 裡的類別函數方法也變得簡單。前一篇的作法是建立一個自己的 ClassLoader 去替換原本的 ClassLoader,這篇的作法是直接去修改現有的 ClassLoader 因此也比上一篇的方法少了 MyClassLoader.java 檔案。

(ps. 程式碼裡呼叫的 installDexJars 函數位於檔案 SystemClassLoaderAdder.java,可以在這裡取得)

3. MyApplication.java

完成步驟1, 2 之後程式就可以運作,但不算真正的完成。有時候啟動程式仍然會得到 java.lang.ClassNotFoundException 的錯誤訊息。

為何說是 "有時候" ,因為這跟使用者操作方式的流程有關,假若現在使用者在程式裡啟動了 Acitivty ,而這個 Activity 是屬於 dex 檔案後來載入的程式碼,啟動後在 Activity 的介面操作到一半切換到其它的 App,此時有可能因為系統記憶體不足,釋放原本在背景執行的 App 的記憶體,之後使用者欲切換到原來的 App,系統會自動重新啟動 App 並呼叫上次啟動的 Activity,此時的 ClassLoader 是系統建立的 ClassLoader 不是我們更改過的 ClassLoader 因此找不到 dex 檔案而發生 Class Not Found Exception。

幸好在系統啟動 Activity 之前我們還有機會讓系統執行我們的程式。那就是 Application 的 attachBaseContext 函數。

底下是新增加的程式 MyApplication.java :
package com.example.dexclassloader;

import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
import java.util.ArrayList;
import java.util.List;
import java.io.File;

public class MyApplication extends Application
{
  public void installPayloadDexFile(){

    String libPath =getFilesDir()+"/PayloadActivity.dex.jar";
    File libFile =new File(libPath);

    if (libFile.exists()==false)
      return;

    File optimizedDexOutputPath = getDir("outdex", Context.MODE_PRIVATE);
    List<File> dexJars = new ArrayList<File>();
    dexJars.add(libFile);
    SystemClassLoaderAdder.installDexJars(getClassLoader(), optimizedDexOutputPath, dexJars);

  }

  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
  }

  @Override
  protected final void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    installPayloadDexFile();
    
  }

  @Override
  public final void onCreate() {
    super.onCreate();
    installPayloadDexFile();
  }

  @Override
  public final void onTerminate() {
    super.onTerminate();

  }

  @Override
  public final void onLowMemory() {
    super.onLowMemory();

  }

  @Override
  public final void onTrimMemory(int level) {
    super.onTrimMemory(level);

  }

}

觀察可知程式在啟動 Activity 之前首先呼叫 MyApplication.attachBaseContext 然後檢查是否存在檔案 PayloadActivity.dex.jar ,若有則載入 dex 並修改目前的 ClassLoader。

4. AndroidManifest.xml

同前一篇介紹的,除了要預先定義 Activity 到 AndroidManifest.xml 之外,若要讓 Class MyApplication 生效,還須定義 MyApplication 在 application 標籤內 :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.dexclassloader"
    android:versionCode="1"
    android:versionName="1.0" >
 
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="18" />
 
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" 
        android:name="com.example.dexclassloader.MyApplication" >
        <activity
            android:name="com.example.dexclassloader.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.example.PayloadActivity" android:label="@string/app_name" />
    </application>
     
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 
</manifest>

ref:http://facebook.github.io/buck/article/exopackage.html
ref:http://developer.android.com/reference/android/app/Application.html
ref:http://stackoverflow.com/questions/9873669/how-do-i-catch-content-provider-initialize


0 意見 :

張貼留言