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;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initUI();javaShowBtn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// TODO Auto-generated method stubString str = readFromAssets("test.txt"); //notes.txt System.getProperty("file.encoding")+"\n" showTextView.setText("Java读取:" + str);}});jniShowBtn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// TODO Auto-generated method stub//AssetManager assetManager = getAssets();String str = MyLib.readFromAssets(getAssets(), "test.txt"); //notes.txtshowTextView.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);}@Overridepublic 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 blocke.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 blocke1.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 blocke.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 这个文件是加载JNIReadAsset.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)
关于Android.mk文件的解释可以参见:http://blog.csdn.net/xuxinyl/article/details/6555762 http://www.2cto.com/kf/201310/253386.html
Application.mk文件
APP_STL:=gnustl_static
APP_CPPFLAGS:=-frtti -fexceptions
APP_ABI:= armeabi-v7a
关于Application.mk文件解释:http://www.52rd.com/Blog/Detail_RD.Blog_liuxijob_29763.html
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 blocke.printStackTrace();}}return modelPath;}
大家想用的话再改改就行了,别忘了配置文件里加sdcard的写文件权限。
代码下载链接:http://download.csdn.net/detail/yanzi1225627/7008015
本文代码运行效果:
---------------本文系原创,转载请注明作者:yanzi1225627
更多相关文章
- MAC Android(安卓)Studio编写Android(安卓)Activity模板的一些总
- Android(安卓)中的拿来主义(编译,反编译,AXMLPrinter2,smali,baksm
- Myeclispe 8.5下断网安装ADT和Android(安卓)SDK
- Gradle Android最新自动化编译脚本教程(提供demo源码)
- [置顶] 解决android某些应用开发某些类无法解析/找到的问题--使
- Android(安卓)源码编译加速 使用ccache
- 在 Ubuntu 下使用 Android(安卓)NDK r4b 编译 FFmpeg 0.6.3
- android 加载外部 dex文件中的类 的源码实例
- android解析xml文件的方式之DOM解析