探秘騰訊Android手機遊戲平臺之不安裝遊戲APK直接啟動法

 前言
相信這樣一個問題,大傢都不會陌生,
“有什麼的方法可以使Android的程序APK不用安裝,而能夠直接啟動”。
發現最後的結局都是不能實現這個美好的願望,而騰訊Android手機遊戲平臺卻又能實現這個功能,下載的連連看,五子棋都沒有安裝過程,但是都能直接運行,這其中到底有什麼“玄機”呢,也有熱心童鞋問過我這個問題,本文就為大傢來揭開這個謎團。
實踐
我實現瞭一個小小的Demo,麻雀雖小五臟俱全,為瞭突出原理,我就盡量簡化瞭程序,通過這個實例來讓大傢明白後臺的工作原理。
下載demo的apk程序apks,其中包括瞭兩個apk,分別是A和B
這兩個APK可分別安裝和運行,A程序界面隻顯示一個Button,B程序界面會動態顯示當前的時間
下面的三幅圖片分別為直接啟動運行A程序(安裝TestA.apk),直接啟動運行B程序(安裝TestB.apk)和由A程序動態啟動B程序(安裝TestA.apk,TestB.apk不用安裝,而是放在/mnt/sdcard/目錄中,即SD卡上)的截圖,細心的同學可以停下來觀察一下他們之間的不同

後兩幅圖片的不同,也即Title的不同,則解釋出瞭我們將要分析的後臺實現原理的機制
實現原理
最能講明白道理的莫過於源碼瞭,下面我們就來分析一下A和B的實現機制,首先來分析TestA.apk的主要代碼實現:

     @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        Button btn = (Button) findViewById(R.id.btn);
        btn.setOnClickListener(new OnClickListener() {
 
            @Override
            public void onClick(View v) {
                Bundle paramBundle = new Bundle();
                paramBundle.putBoolean("KEY_START_FROM_OTHER_ACTIVITY", true);
                String dexpath = "/mnt/sdcard/TestB.apk";
                String dexoutputpath = "/mnt/sdcard/";
                LoadAPK(paramBundle, dexpath, dexoutputpath);
            }
        });
    }
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  Button btn = (Button) findViewById(R.id.btn);
  btn.setOnClickListener(new OnClickListener() {

   @Override
   public void onClick(View v) {
    Bundle paramBundle = new Bundle();
    paramBundle.putBoolean("KEY_START_FROM_OTHER_ACTIVITY", true);
    String dexpath = "/mnt/sdcard/TestB.apk";
    String dexoutputpath = "/mnt/sdcard/";
    LoadAPK(paramBundle, dexpath, dexoutputpath);
   }
  });
 }
代碼解析:這就是OnCreate函數要做的事情,裝載view界面,綁定button事件,大傢都熟悉瞭,還有就是設置程序B的放置路徑,因為我程序中代碼是從/mnt/sdcard/TestB.apk中動態加載,這也就是為什麼要讓大傢把TestB.apk放在SD卡上面的原因瞭。關鍵的函數就是最後一個瞭LoadAPK,它來實現動態加載B程序。

    public void LoadAPK(Bundle paramBundle, String dexpath, String dexoutputpath) {
        ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();
        DexClassLoader localDexClassLoader = new DexClassLoader(dexpath,
                dexoutputpath, null, localClassLoader);
        try {
            PackageInfo plocalObject = getPackageManager()
                    .getPackageArchiveInfo(dexpath, 1);
 
            if ((plocalObject.activities != null)
                    && (plocalObject.activities.length > 0)) {
                String activityname = plocalObject.activities[0].name;
                Log.d(TAG, "activityname = " + activityname);
 
                Class localClass = localDexClassLoader.loadClass(activityname);
                Constructor localConstructor = localClass
                        .getConstructor(new Class[] {});
                Object instance = localConstructor.newInstance(new Object[] {});
                Log.d(TAG, "instance = " + instance);
 
                Method localMethodSetActivity = localClass.getDeclaredMethod(
                        "setActivity", new Class[] { Activity.class });
                localMethodSetActivity.setAccessible(true);
                localMethodSetActivity.invoke(instance, new Object[] { this });
 
                Method methodonCreate = localClass.getDeclaredMethod(
                        "onCreate", new Class[] { Bundle.class });
                methodonCreate.setAccessible(true);
                methodonCreate.invoke(instance, new Object[] { paramBundle });
            }
            return;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
 public void LoadAPK(Bundle paramBundle, String dexpath, String dexoutputpath) {
  ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();
  DexClassLoader localDexClassLoader = new DexClassLoader(dexpath,
    dexoutputpath, null, localClassLoader);
  try {
   PackageInfo plocalObject = getPackageManager()
     .getPackageArchiveInfo(dexpath, 1);

   if ((plocalObject.activities != null)
     && (plocalObject.activities.length > 0)) {
    String activityname = plocalObject.activities[0].name;
    Log.d(TAG, "activityname = " + activityname);

    Class localClass = localDexClassLoader.loadClass(activityname);
    Constructor localConstructor = localClass
      .getConstructor(new Class[] {});
    Object instance = localConstructor.newInstance(new Object[] {});
    Log.d(TAG, "instance = " + instance);

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

    Method methodonCreate = localClass.getDeclaredMethod(
      "onCreate", new Class[] { Bundle.class });
    methodonCreate.setAccessible(true);
    methodonCreate.invoke(instance, new Object[] { paramBundle });
   }
   return;
  } catch (Exception ex) {
   ex.printStackTrace();
  }
 }
代碼解析:這個函數要做的工作如下:加載B程序的APK文件,通過類加載器DexClassLoader來解析APK文件,這樣會在SD卡上面生成一個同名的後綴為dex的文件,例如/mnt/sdcard/TestB.apk==>/mnt/sdcard/TestB.dex,接下來就是通過java反射機制,動態實例化B中的Activity對象,並依次調用瞭其中的兩個函數,分別為setActivity和onCreate.看到這裡,大傢是不是覺得有點奇怪,Activity的啟動函數是onCreate,為什麼要先調用setActivity,而更奇怪的是setActivity並不是系統的函數,確實,那是我們自定義的,這也就是核心的地方。
好瞭帶著這些疑問,我們再來分析B程序的主代碼:

 public class TestBActivity extends Activity {
    private static final String TAG = "TestBActivity";
    private Activity otherActivity;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        boolean b = false;
        if (savedInstanceState != null) {
            b = savedInstanceState.getBoolean("KEY_START_FROM_OTHER_ACTIVITY", false);
            if (b) {
                this.otherActivity.setContentView(new TBSurfaceView(
                        this.otherActivity));
            }
        }
        if (!b) {
            super.onCreate(savedInstanceState);
            // setContentView(R.layout.main);
            setContentView(new TBSurfaceView(this));
        }
    }
 
    public void setActivity(Activity paramActivity) {
        Log.d(TAG, "setActivity…" + paramActivity);
        this.otherActivity = paramActivity;
    }
}
public class TestBActivity extends Activity {
 private static final String TAG = "TestBActivity";
 private Activity otherActivity;

 @Override
 public void onCreate(Bundle savedInstanceState) {
  boolean b = false;
  if (savedInstanceState != null) {
   b = savedInstanceState.getBoolean("KEY_START_FROM_OTHER_ACTIVITY", false);
   if (b) {
    this.otherActivity.setContentView(new TBSurfaceView(
      this.otherActivity));
   }
  }
  if (!b) {
   super.onCreate(savedInstanceState);
   // setContentView(R.layout.main);
   setContentView(new TBSurfaceView(this));
  }
 }

 public void setActivity(Activity paramActivity) {
  Log.d(TAG, "setActivity…" + paramActivity);
  this.otherActivity = paramActivity;
 }
}
代碼解析:看完程序B的實現機制,大傢是不是有種恍然大悟的感覺,這根本就是“偷梁換柱”嘛,是滴,程序B動態借用瞭程序A的上下文執行環境,這也就是上面後兩幅圖的差異,最後一幅圖運行的是B的程序,但是title表示的卻是A的信息,而沒有重新初始化自己的,實際上這也是不可能的,所以有些童鞋雖然通過java的反射機制,正確呼叫瞭被調程序的onCreate函數,但是期望的結果還是沒有出現,原因就是這個上下文環境沒有正確建立起來,但是若通過startActivity的方式來啟動APK的話,android系統會替你建立正確的執行時環境,所以就沒問題。至於那個TBSurfaceView,那就是自定義的一個view畫面,動態畫當前的時間

 public class TBSurfaceView extends SurfaceView implements Callback, Runnable {
    private SurfaceHolder sfh;
    private Thread th;
    private Canvas canvas;
    private Paint paint;
 
    public TBSurfaceView(Context context) {
        super(context);
        th = new Thread(this);
        sfh = this.getHolder();
        sfh.addCallback(this);
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.RED);
        this.setKeepScreenOn(true);
    }
 
    public void surfaceCreated(SurfaceHolder holder) {
        th.start();
    }
 
    private void draw() {
        try {
            canvas = sfh.lockCanvas();
            if (canvas != null) {
                canvas.drawColor(Color.WHITE);
                canvas.drawText("Time: " + System.currentTimeMillis(), 100,
                        100, paint);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            if (canvas != null) {
                sfh.unlockCanvasAndPost(canvas);
            }
        }
    }
 
    public void run() {
        while (true) {
            draw();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
    }
 
    public void surfaceDestroyed(SurfaceHolder holder) {
    }
}
public class TBSurfaceView extends SurfaceView implements Callback, Runnable {
 private SurfaceHolder sfh;
 private Thread th;
 private Canvas canvas;
 private Paint paint;

 public TBSurfaceView(Context context) {
  super(context);
  th = new Thread(this);
  sfh = this.getHolder();
  sfh.addCallback(this);
  paint = new Paint();
  paint.setAntiAlias(true);
  paint.setColor(Color.RED);
  this.setKeepScreenOn(true);
 }

 public void surfaceCreated(SurfaceHolder holder) {
  th.start();
 }

 private void draw() {
  try {
   canvas = sfh.lockCanvas();
   if (canvas != null) {
    canvas.drawColor(Color.WHITE);
    canvas.drawText("Time: " + System.currentTimeMillis(), 100,
      100, paint);
   }
  } catch (Exception ex) {
   ex.printStackTrace();
  } finally {
   if (canvas != null) {
    sfh.unlockCanvasAndPost(canvas);
   }
  }
 }

 public void run() {
  while (true) {
   draw();
   try {
    Thread.sleep(100);
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
  }
 }

 public void surfaceChanged(SurfaceHolder holder, int format, int width,
   int height) {
 }

 public void surfaceDestroyed(SurfaceHolder holder) {
 }
}
騰訊遊戲平臺解析
說瞭這麼多,都是背景,O(∩_∩)O哈哈~
其實騰訊遊戲平臺就是這麼個實現原理,我也是通過它才學習到這種方式的,還得好好感謝感謝呢。
騰訊Android遊戲平臺的遊戲分成兩類,第一類是騰訊自主研發的,像鬥地主,五子棋,連連看什麼的,所以實現機制就如上面的所示,A代表遊戲大廳,B代表鬥地主類的小遊戲。第二類是第三方軟件公司開發的,可就不能已這種方式來運作瞭,畢竟騰訊不能限制別人開發代碼的方式啊,所以騰訊就開放瞭一個sdk包出來,讓第三方應用可以和遊戲大廳相結合,具體可參見QQ遊戲中心開發者平臺,但這同時就損失瞭一個優點,那就是第三方開發的遊戲要通過安裝的方式才能運行。
結論
看到這裡,相信大傢都比較熟悉這個背後的原理瞭吧,也希望大傢能提供更好的反饋信息!
程序源碼下載source:http://up.aiwalls.com/2012/0429/20120429095938970.zip

摘自 潤物無聲

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *