1. 程式人生 > >Android apk安裝過程及Java、JNI讀取安裝包內assets資原始檔的兩種方法(附原始碼例項)

Android apk安裝過程及Java、JNI讀取安裝包內assets資原始檔的兩種方法(附原始碼例項)

問題背景:在PC上的程式可以輕鬆使用./或不用指明,預設讀取的就是程式所在路徑內的檔案。但在Android上,應用程式被打包成apk,程式執行時無法直接獲取apk(壓縮包)內的檔案。但在一些特殊場合,如載入影象處理訓練好的分類器、模型等資料,要求每個apk到手機上都能執行,就必須解決這個問題。本文深入研究apk安裝過程,給出三種方法解決這個問題。

一、android apk安裝過程

Android apk檔案是將AndroidManifinest.xml、應用程式程式碼(.dex)、資原始檔和其他檔案打包成的一個壓縮包檔案,其中的.dex檔案即使android上的可執行檔案,由Java程式碼編譯後的class檔案連結而成。因此可以用unzip直接將apk開啟。如下圖所示,將本文後面要附原始碼的一個apk解壓後示意圖下:

1、assets資料夾,這個本文後面的原始碼專門就講它,暫略。

2、lib資料夾,這裡放著我們jni編譯後生成的so檔案。

3、META-INF資料夾,這個要追溯到java的jar檔案。jar檔案和zip檔案唯一的區別就是包含一個META-INF資料夾,詳見:這裡

4、res資料夾,就是所謂的資原始檔,裡面放的有各種圖片資源(drawable資料夾下的東西)和佈局xml檔案。示圖如下:


因此如果想借用一個apk的圖片資源的話,直接解壓就ok了。

5、AndroidManifinest.xml檔案,這個就不多說了,每個Android工程檔案都有。

6、classes.dex檔案,Dex是Dalvik VM executes的全稱,即Android Dalvik執行程式,並非

Java ME位元組碼而是Dalvik位元組碼

7、resources.arsc檔案,是編譯後的二進位制資原始檔。

apk具體的核心安裝步驟及牽涉到資料夾路徑如下(以安裝ReadAssets.apk為例):

第一步:複製apk檔案到data/app/目錄下,解壓並掃描安裝包,名字是以包名命名的,並不是apk的名字。如下:


第二步:將.dex檔案儲存到data/dalvik-cache目錄,


第三步:在/data/data/目錄下建立對應的應用資料目錄,目錄名字是apk的包名:


其中cache資料夾下的內容如下:


lib資料夾下是jni裡生成的庫,libReadAsset.so,如下:


參考連結1 參考連結2

二、Java和JNI讀取assets資料夾內的檔案

關於assets資料夾和res資料夾的區別見http://blog.sina.com.cn/s/blog_4b93170a0102dqxj.html ,即res資料夾內的東西會再R.java生成id,而assets資料夾不會生成標記,只能利用assetmanager進行訪問。其中的raw資料夾也不會被編譯跟assets一樣。

下面的程式碼是我寫的一個demo,從java和jni兩種方式讀取assets資料夾內的一個txt檔案。

1、佈局檔案:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <TextView
        android:id="@+id/textview_show"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:text="@string/hello_world" />

    <Button
        android:id="@+id/btn_javashow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:text="Java讀取" />

    <Button
        android:id="@+id/btn_jnishow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_toRightOf="@id/btn_javashow"
        android:text="JNI讀取" />



</RelativeLayout>

2、MainActivity.java檔案
package org.yanzi.readassets;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import org.yanzi.lib.MyLib;

import android.app.Activity;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity {

	Button javaShowBtn;
	Button jniShowBtn;
	TextView showTextView;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		initUI();
		javaShowBtn.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub

				String str = readFromAssets("test.txt"); //notes.txt System.getProperty("file.encoding")+"\n" 
				showTextView.setText("Java讀取:" + str);
			}
		});
		
		jniShowBtn.setOnClickListener(new View.OnClickListener() {
			
			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
//				AssetManager assetManager = getAssets();
				String str = MyLib.readFromAssets(getAssets(), "test.txt"); //notes.txt
				showTextView.setText("Jni讀取:" + str);
			}
		});
		
	}

	public void initUI(){
		javaShowBtn = (Button)findViewById(R.id.btn_javashow);
		jniShowBtn = (Button)findViewById(R.id.btn_jnishow);
		showTextView = (TextView)findViewById(R.id.textview_show);

	}
	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

	/**
	 * @param name
	 * @return
	 * 從Java層讀取Assects資料夾內東西
	 */
	public String readFromAssets(String name){
		String resultStr = "";
		try {
			InputStream inStream = this.getResources().getAssets().open(name);
			resultStr = convertStream2String(inStream);

			//convertStreamToString(inStream);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}


		return resultStr;
	}
	public String convertStream2String(InputStream is){
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		int i = 0;
		String str = "";
		//		String str2 = "";
		int l = 0;
		try {
			l = is.available();
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		byte[] buffer = new byte[l];
		try {
			while((i = is.read()) != -1){
				baos.write(i);
			}
			is.close();
			str	= baos.toString("utf-8");
			//			str2 = new String(baos.toByteArray(), "utf-8");
			//			str2 = EncodingUtils.getString(buffer, "utf-8"); //gb2312
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return str;
	}


	public String convertStreamToString(InputStream is) throws IOException {

		/*

		  28          * 為了將InputStream轉換成String我們使用函式BufferedReader.readLine().

		  29          * 我們迭代呼叫BufferedReader直到其返回null, null意味著沒有其他的資料要讀取了.

		  30          * 每一行將會追加到StringBuilder的末尾, StringBuilder將作為String返回。

		  31          *

		  32          */

		if (is != null) {

			StringBuilder sb = new StringBuilder();

			String line;
			try {

				BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));

				while ((line = reader.readLine()) != null) {

					sb.append(line).append("\n");

				}

			} finally {

				is.close();

			}
			return sb.toString();

		} else {

			return "";

		}

	}




}

[注:關於InputStream轉String可以參考http://blog.csdn.net/iplayvs2008/article/details/11484627,需注意預設的在windows下新建文字文件txt格式是gb2312,因此程式碼裡轉換時也必須指明是gb2312,如果是txt文件經過notepad++建立的,則預設的是UTF-8格式,無需指定預設的就是utf-8格式,因此轉出來不亂碼。]

3、MyLib.java 這個檔案是載入JNI ReadAsset.so的,如下:

package org.yanzi.lib;

import android.content.res.AssetManager;

public class MyLib {
	static{
		System.loadLibrary("ReadAsset");
	}
	
	public static native String readFromAssets(AssetManager assetManager, String name);
}

4、jni資料夾下有四個檔案,分別是Android.mk、Application.mk、mylog.h、readAssets.cpp。

Android.mk檔案如下:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_LDLIBS    := -lm -llog -landroid
LOCAL_MODULE    := ReadAsset
LOCAL_SRC_FILES := readAssets.cpp
#LOCAL_C_INCLUDES+= D:\ProgramFile\android-ndk-r7\platforms\android-14\arch-arm\usr\include\android
include $(BUILD_SHARED_LIBRARY) 

Application.mk檔案

APP_STL:=gnustl_static
APP_CPPFLAGS:=-frtti -fexceptions 
APP_ABI:= armeabi-v7a 

readAssets.cpp

#include "mylog.h"
#include <jni.h>
#include <sys/types.h>
#include <stdlib.h>
#include <android/asset_manager_jni.h>
#include <android/asset_manager.h>

//jclass clazz,
extern "C"{
JNIEXPORT  jstring  JNICALL Java_org_yanzi_lib_MyLib_readFromAssets(JNIEnv* env,jclass clazz, jobject assetManager,jstring name);

JNIEXPORT  jstring JNICALL Java_org_yanzi_lib_MyLib_readFromAssets(JNIEnv* env, jclass clazz, jobject assetManager,jstring name){
	LOGI("readFromAssets enter..."); //jclass this,
	jstring resultStr;
	LOGI("readFromAssets enter111...");
	AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
	LOGI("readFromAssets enter000...");
	if(mgr==NULL)
	{
		LOGI(" %s","AAssetManager==NULL");
		return 0;
	}

	/*獲取檔名並開啟*/
	jboolean iscopy;
	const char *mfile = env->GetStringUTFChars(name, &iscopy); //(*env)->GetStringUTFChars(name, &iscopy); env,
	AAsset* asset = AAssetManager_open(mgr, mfile,AASSET_MODE_UNKNOWN);
	env->ReleaseStringUTFChars(name, mfile); //env,
	if(asset==NULL)
	{
		LOGI(" %s","asset==NULL");
		return 0;
	}
	/*獲取檔案大小*/
	off_t bufferSize = AAsset_getLength(asset);
	LOGI("file size : %d\n",bufferSize);
	char *buffer=(char *)malloc(bufferSize+1);
	buffer[bufferSize]=0;
	int numBytesRead = AAsset_read(asset, buffer, bufferSize);
	LOGI("readFromAssets: %s",buffer);
	resultStr = env->NewStringUTF(buffer);
	free(buffer);
	/*關閉檔案*/
	AAsset_close(asset);
	LOGI("readFromAssets exit...");
	return resultStr;
}
}

關於這個cpp檔案,注意事項如下:

a、標頭檔案是一個都不能少,其實是ndk安裝目錄下的檔案:


b、關於jni裡將java的string轉成jstring,C檔案和C++是不同的,C++裡按我上面的程式就ok:

const char *mfile = env->GetStringUTFChars(name, &iscopy);

但在C檔案裡是:const char *mfile = (*env)->GetStringUTFChars(env, filename, &iscopy);

c、這個jni本地函式的宣告如下:
JNIEXPORT  jstring  JNICALL Java_org_yanzi_lib_MyLib_readFromAssets(JNIEnv* env,jclass clazz, jobject assetManager,jstring name);
其中的jclass clazz不能去掉,儘管在程式碼裡沒有呼叫到。如果去掉,去找不到filed之類的錯誤,猜測jclass傳下來的是類似包名之類的東西,AssetManager要讀東西必須要它。
d、jni裡將char * 或const char*型別轉為jstring的程式碼:
resultStr = env->NewStringUTF(buffer);
e、mk檔案裡必須有
LOCAL_LDLIBS    := -lm -llog -landroid裡的-landroid否則的話是打開不了AssetManager的!!!!


mylog.h檔案是為了在jni裡新增log:
#ifndef _MYLOG_H
#define _MYLOG_H
#include <android/log.h>
#define TAG "yan"
//#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, TAG, __VA_ARGS__)
//#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG , TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO , TAG, __VA_ARGS__)
//#define LOGW(...) __android_log_print(ANDROID_LOG_WARN , TAG, __VA_ARGS__)
//#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , TAG, __VA_ARGS__)
#endif
要想列印log,除了這些外,還要在mk檔案裡新增:LOCAL_LDLIBS    := -lm -llog -landroid裡的-llog

參考文件:
1、http://blog.sina.com.cn/s/blog_4a657c5a01016t2y.html
2、http://blog.sina.com.cn/s/blog_4a4f9fb5010101tb.html
這兩個參考文件僅僅是程式碼片段,雜家根據這兩篇文章搞了兩晚才徹底讓jni裡能訪問assets資料夾裡的東西。西可以用java讀取畢竟方便,然後傳給jni以string的方式,實在不行了讓jni裡讀。但也有例外情況,如果jni裡需要頻繁的讀取,(assets檔案裡的東西只能讀不能寫),這種jni讀取的方法就不適合了。原因是需要每次都開啟assetsmanager,很不方便。為此,我們可以再java裡將assets檔案裡的東西拷貝到sdcard中,然後將這個檔案的路徑傳給jni,在jni裡知道了檔案在sdcard上的絕對路徑就可以暢通的讀取了,下面是示例程式碼:

public String copyModelToSdcard(){
		String dir = Environment.getExternalStorageDirectory() + "/"+ "ScanBankCard";
		File dirFile = new File(dir);
		if(!dirFile.exists()){
			dirFile.mkdir();
		}
		String modelName = "card.model";
		String modelPath = dir + "/" + modelName;
		File model = new File(modelPath);
		if(model.exists()){
			return modelPath;	
		}else		{
			try {
				InputStream in = this.getResources().getAssets().open(modelName);
				int length = in.available();
				byte[] buffer = new byte[length];
				in.read(buffer);
				OutputStream out = new FileOutputStream(modelPath);
				out.write(buffer);
				out.flush();
				out.close();
				in.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

		}
		return modelPath;

	}

大家想用的話再改改就行了,別忘了配置檔案里加sdcard的寫檔案許可權。

本文程式碼執行效果:



---------------本文系原創,轉載請註明作者:yanzi1225627