不安裝APK直接啟動應用


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

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

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

重要說明

在實踐的過程中大家都會發現資源引用的問題,這里重點聲明兩點:
1. 資源文件是不能直接inflate的,如果簡單的話直接在程序中用代碼書寫。
2. 資源文件是不能用R來引用的,因為上下文已經不同了,騰訊的做法是將資源文件打包(*.pak文件和APK打包在一起),雖然APK是沒有進行安裝,但是 資源文件是另外解壓到指定文件夾下面的,然后將文件夾的地址傳給了第三方應用程序,這樣第三方應用程序通過File的inputstream流還是可以讀 取和使用這些資源的。

 

實踐

我實現了一個小小的Demo,麻雀雖小五臟俱全,為了突出原理,我就盡量簡化了程序,通過這個實例來讓大家明白后台的工作原理。

  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卡上)的截圖,細心的同學可以停下來觀察一下他們之間的不同
  4. 后兩幅圖片的不同,也即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);
            }
        });
    }

 

 代碼解析:這就是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();
		}
	}

 

代碼解析:這個函數要做的工作如下:加載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;
	}
}

 代碼解析:看完程序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) {
	}
}

騰訊游戲平台解析

說了這么多,都是背景,O(∩_∩)O哈哈~

其實騰訊游戲平台就是這么個實現原理,我也是通過它才學習到這種方式的,還得好好感謝感謝呢。

騰訊Android游戲平台的游戲分成兩類,第一類是騰訊自主研發的,像斗地主,五子棋,連連看什么的,所以實現機制就如上面的所示,A代表游戲大 廳,B代表斗地主類的小游戲。第二類是第三方軟件公司開發的,可就不能已這種方式來運作了,畢竟騰訊不能限制別人開發代碼的方式啊,所以騰訊就開放了一個 sdk包出來,讓第三方應用可以和游戲大廳相結合,具體可參見QQ游戲中心開發者平台,但這同時就損失了一個優點,那就是第三方開發的游戲要通過安裝的方式才能運行。

結論

看到這里,相信大家都比較熟悉這個背后的原理了吧,也希望大家能提供更好的反饋信息!

程序源碼下載source


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM