天天看點

不安裝啟動遊戲

 前言

相信這樣一個問題,大家都不會陌生,

“有什麼的方法可以使Android的程式APK不用安裝,而能夠直接啟動”。

發現最後的結局都是不能實作這個美好的願望,而騰訊Android手機遊戲平台卻又能實作這個功能,下載下傳的連連看,五子棋都沒有安裝過程,但是都能直接運作,這其中到底有什麼“玄機”呢,也有熱心童鞋問過我這個問題,本文就為大家來揭開這個謎團。

實踐

我實作了一個小小的Demo,麻雀雖小五髒俱全,為了突出原理,我就盡量簡化了程式,通過這個執行個體來讓大家明白背景的工作原理。

這兩個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

        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;

  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() {

            canvas = sfh.lockCanvas();

            if (canvas != null) {

                canvas.drawColor(Color.WHITE);

                canvas.drawText("Time: " + System.currentTimeMillis(), 100,

                        100, paint);

        } finally {

                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() {

   canvas = sfh.lockCanvas();

   if (canvas != null) {

    canvas.drawColor(Color.WHITE);

    canvas.drawText("Time: " + System.currentTimeMillis(), 100,

      100, paint);

  } finally {

    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遊戲中心開發者平台,但這同時就損失了一個優點,那就是第三方開發的遊戲要通過安裝的方式才能運作。

結論

看到這裡,相信大家都比較熟悉這個背後的原理了吧,也希望大家能提供更好的回報資訊!

上一篇: linux面試題

繼續閱讀