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

前言

相信這樣一個問題,你們都不會陌生,html

「有什麼的方法可使Android的程序APK不用安裝,而可以直接啓動」。java

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

重要說明

在實踐的過程當中你們都會發現資源引用的問題,這裏重點聲明兩點:
1. 資源文件是不能直接inflate的,若是簡單的話直接在程序中用代碼書寫。
2. 資源文件是不能用R來引用的,由於上下文已經不一樣了,騰訊的作法是將資源文件打包(*.pak文件和APK打包在一塊兒),雖然APK是沒有進行安裝,可是資源文件是另外解壓到指定文件夾下面的,而後將文件夾的地址傳給了第三方應用程序,這樣第三方應用程序經過File的inputstream流仍是能夠讀取和使用這些資源的。canvas

實踐

我實現了一個小小的Demo,麻雀雖小五臟俱全,爲了突出原理,我就儘可能簡化了程序,經過這個實例來讓你們明白後臺的工做原理。ide

  1. 下載demo的apk程序apks,其中包括了兩個apk,分別是A和B函數

  2. 這兩個APK可分別安裝和運行,A程序界面只顯示一個Button,B程序界面會動態顯示當前的時間學習

  3. 下面的三幅圖片分別爲直接啓動運行A程序(安裝TestA.apk),直接啓動運行B程序(安裝TestB.apk)和由A程序動態啓動B程序(安裝TestA.apk,TestB.apk不用安裝,而是放在/mnt/sdcard/目錄中,即 SD卡上)的截圖,細心的同窗能夠停下來觀察一下他們之間的不一樣
    this

  4. 後兩幅圖片的不一樣,也即Title的不一樣,則解釋出了咱們將要分析的後臺實現原理的機制spa

實現原理

最能講明白道理的莫過於源碼了,下面咱們就來分析一下A和B的實現機制,首先來分析TestA.apk的主要代碼實現:code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    @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程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    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程序的主代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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畫面,動態畫當前的時間

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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

相關文章
相關標籤/搜索