相信這樣一個問題,你們都不會陌生,html
「有什麼的方法可使Android的程序APK不用安裝,而可以直接啓動」。java
發現最後的結局都是不能實現這個美好的願望,而騰訊Android手機遊戲平臺卻又能實現這個功能,下載的連連看,五子棋都沒有安裝過程,可是都能直接運行,這其中到底有什麼「玄機」呢,也有熱心童鞋問過我這個問題,本文就爲你們來揭開這個謎團。android
重要說明
在實踐的過程當中你們都會發現資源引用的問題,這裏重點聲明兩點:
1. 資源文件是不能直接inflate的,若是簡單的話直接在程序中用代碼書寫。
2. 資源文件是不能用R來引用的,由於上下文已經不一樣了,騰訊的作法是將資源文件打包(*.pak文件和APK打包在一塊兒),雖然APK是沒有進行安裝,可是資源文件是另外解壓到指定文件夾下面的,而後將文件夾的地址傳給了第三方應用程序,這樣第三方應用程序經過File的inputstream流仍是能夠讀取和使用這些資源的。canvas
我實現了一個小小的Demo,麻雀雖小五臟俱全,爲了突出原理,我就儘可能簡化了程序,經過這個實例來讓你們明白後臺的工做原理。ide
下載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卡上)的截圖,細心的同窗能夠停下來觀察一下他們之間的不一樣
this
後兩幅圖片的不一樣,也即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