生成Android能动态加载的Jar包
这样可以直接build生成jar包,生成的jar包会在如下图片的路径下
具体项目里没显示也不知道为什么。如果想要更改成release版本只需在structure里
之后选择
将其改变为release即可
利用gradle打jar包
task makeJar(type: Copy) {
//删除存在的
delete 'build/libs/myjar.jar'
//设置拷贝的文件
from('build/intermediates/aar_main_jar/release/')
//打进jar包后的文件目录
into('build/libs/')
//将classes.jar放入build/libs/目录下
//include ,exclude参数来设置过滤
include('classes.jar')
//重命名
rename ('classes.jar', 'myjar.jar')
}
makeJar.dependsOn(build)
然后在命令行输入
gradlew makeJar
或者直接点击运行也行
之后可以利用d8 将生成的jar包提取到桌面
d8 --dex --output=test.jar /Users/ocean/Cybersecurity/Android_Project/Study_Android/creatJar/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classs.jar
动态加载Jar包
参考: https://blog.csdn.net/fengyulinde/article/details/79623743 https://blog.csdn.net/u012121105/article/details/129297666
这里主要记录下重要的逻辑
package com.lgf.dynamicload.demo;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;
import com.lgf.plugin.IDynamic;
import java.io.File;
import dalvik.system.DexClassLoader;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
// layout相应的View中使用onClick属性
public void showMessage(View view) {
// 此路径为插件存放路径
File dexPathFile = new File(Environment.getExternalStorageDirectory() + File.separator + "plugin.jar");
String dexPath = dexPathFile.getAbsolutePath();
String dexDecompressPath = getDir("dex", MODE_PRIVATE).getAbsolutePath(); // dex解压后的路径
// String dexDecompressPath = Environment.getExternalStorageDirectory().getAbsolutePath(); // 不能放在SD卡下,否则会报错
/**
* DexClassLoader参数说明
* 参数1 dexPath:待加载的dex文件路径,如果是外存路径,一定要加上读外存文件的权限(<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> )
* 参数2 optimizedDirectory:解压后的dex存放位置,此位置一定要是可读写且仅该应用可读写(安全性考虑),所以只能放在data/data下。本文getDir("dex", MODE_PRIVATE)会在/data/data/**package/下创建一个名叫”app_dex1“的文件夹,其内存放的文件是自动生成output.dex;如果不满足条件,Android会报错误
* 参数3 libraryPath:指向包含本地库(so)的文件夹路径,可以设为null
* 参数4 parent:父级类加载器,一般可以通过Context.getClassLoader获取到,也可以通过ClassLoader.getSystemClassLoader()获取到。
*/
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, dexDecompressPath, null, getClassLoader());
Class libClazz = null;
try {
libClazz = dexClassLoader.loadClass("com.lgf.base.DynamicTest");
IDynamic lib = (IDynamic) libClazz.newInstance();
Toast.makeText(this, lib.show(), Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
}
}
}
记得添加下权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
NDK编程
JNI 是什么
JNI 是 Java Native Interface 的缩写,即 Java 的本地接口。
目的是使得 Java 与本地其他语言(如 C/C++)进行交互。
JNI 是属于 Java 的,与 Android 无直接关系。
NDK 是什么
NDK 是 Native Development Kit 的缩写,是 Android 的工具开发包。
作用是快速开发 C/C++ 的动态库,并自动将动态库与应用一起打包到 apk。
NDK 是属于 Android 的,与 Java 无直接关系。
JNI 与 NDK 的关系
JNI 是实现的目的,NDK 是 Android 中实现 JNI 的手段。
第一个JNI程序
新建一个c++Native程序,Android Studio 会自动帮你生成一个可执行的 Hello World 程序,我们简单看一下这个工程
其中 cpp 目录就是我们的 C/C++ 代码、预编译库的默认路径了,而 CMakeList.txt 就是编译的脚本文件了
package com.example.ndkstudy;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import com.example.ndkstudy.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
// Used to load the 'ndkstudy' library on application startup.
static {
System.loadLibrary("ndkstudy");
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
}
/**
* A native method that is implemented by the 'ndkstudy' native library, * which is packaged with this application. */ public native String stringFromJNI();
}
Java 调用本地方法,是使用 native 关键字。而本例中,本地方法的实现是在一个叫做 “native-lib” 的动态库里(动态库的名称是在 CMakeList.txt 中指定的),要想使用这个动态库,就必须先加载这个库,即 System.loadLibrary(native-lib) 。这些都是 Java 的语法定义。
看一下cpp的代码
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkstudy_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
上面提到的 public native String stringFromJNI() 方法的实现就是在这里,那怎么知道 Java 中的某个 native 方法是对应的 cpp 中的哪个方法呢?这就和 JNI 的注册有关了,本例中使用的是静态注册,即 “Java包名类名_方法名” 的形式,其中包名也是用下划线替代点号。
总结下流程:
1.Gradle 调用您的外部构建脚本 CMakeLists.txt。
2.CMake 按照构建脚本中的命令将 C++ 源文件 native-lib.cpp 编译到共享的对象库中,并命名为 libnative-lib.so,Gradle 随后会将其打包到 APK 中。
3.运行时,应用的 MainActivity 会使用 System.loadLibrary() 加载原生库。现在,应用可以使用库的原生函数 stringFromJNI()。
4.MainActivity.onCreate() 调用 stringFromJNI(),这将返回“Hello from C++”并使用这些文字更新 TextView。
在列一下CMakeLists.txt
cmake_minimum_required(VERSION 3.22.1)
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
# Since this is the top level CMakeLists.txt, the project name is also accessible
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
# build script scope).
project("ndkstudy")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
#
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
# is preferred for the same purpose.
#
# In order to load a library into your app from Java/Kotlin, you must call
# System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file.
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
native-lib.cpp)
# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log)
解释下含义用gpt
cmake_minimum_required(VERSION 3.22.1)`
-
含义:声明本项目使用的最低 CMake 版本是 3.22.1
。这是为了确保 CMake 的语法和功能版本兼容。
project("ndkstudy")
-
含义:定义项目名称为 ndkstudy
。 -
作用: -
会设置变量 PROJECT_NAME
为ndkstudy
; -
在顶层 CMakeLists.txt
中,PROJECT_NAME
与CMAKE_PROJECT_NAME
是一样的; -
这个名称也通常被用作生成库文件的前缀,例如生成 libndkstudy.so
。
-
add_library(${CMAKE_PROJECT_NAME} SHARED native-lib.cpp)
-
含义: -
创建一个共享库( SHARED
),名字就是上面定义的项目名ndkstudy
; -
包含一个源文件 native-lib.cpp
,即这个库的源代码;
-
-
结果: -
会构建出一个名为 libndkstudy.so
的共享库。
-
target_link_libraries(${CMAKE_PROJECT_NAME} android log)
-
含义: -
为这个原生库链接上其他的依赖库: -
android
: Android 原生 API; -
log
: 用于调用 Android 的日志系统(如__android_log_print
);
-
-
效果: -
可以在 native-lib.cpp
中使用 Android 系统的功能,例如打印 log 信息。
-
数据类型
基本数据类型
|
|
|
|
---|---|---|---|
boolean |
jboolean |
unsigned char |
|
byte |
jbyte |
signed char |
|
char |
jchar |
unsigned short |
|
short |
jshort |
signed short |
|
int |
jint |
signed int |
|
long |
jlong |
signed long |
|
float |
jfloat |
float |
|
double |
jdouble |
double |
|
上表显示了java对应的c的数据类型使用JNI进行中转
引用数据类型
以下是你Java 类类型 与 JNI 引用类型 的对应关系
|
|
|
---|---|---|
java.lang.Object |
jobject |
|
java.lang.String |
jstring |
String 字符串对象 |
java.lang.Class |
jclass |
Class 类型对象(用于静态方法的强制参数) |
Object[] |
jobjectArray |
|
boolean[] |
jbooleanArray |
boolean 的数组表示形式 |
byte[] |
jbyteArray |
byte 的数组表示形式 |
char[] |
jcharArray |
char 的数组表示形式 |
short[] |
jshortArray |
short 的数组表示形式 |
int[] |
jintArray |
int 的数组表示形式 |
long[] |
jlongArray |
long 的数组表示形式 |
float[] |
jfloatArray |
float 的数组表示形式 |
double[] |
jdoubleArray |
double 的数组表示形式 |
java.lang.Throwable |
jthrowable |
|
void |
void |
|
数据类型描述符
|
|
|
---|---|---|
int |
I |
|
long |
J |
|
byte |
B |
|
short |
S |
|
char |
C |
|
float |
F |
|
double |
D |
|
boolean |
Z |
|
void |
V |
|
Java 引用类型描述符
|
|
|
---|---|---|
|
L<类的全限定名>; |
Ljava/lang/String;
String |
|
[
|
[I
int[] ,[Ljava/lang/String; 表示 String[] |
JNI 方法签名格式
(参数列表)返回值
|
|
---|---|
void foo() |
()V |
int sum(int a, int b) |
(II)I |
String concat(String a, String b) |
(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; |
int[] getData() |
()[I |
void setValues(float[] values, boolean flag) |
([FZ)V |
可以验证一下
可以看到我们定义的一个方法,编译一下之后用jabap看一下他的签名
javap -s com.example.ndkstudy.MainActivity
符合表格的规则。来写一个读取scard目录的文件功能练习
public class MainActivity extends AppCompatActivity {
private final static int MY_PERMISSIONS_REQUEST_WRITE_CODE = 11;
// Used to load the 'ndk01' library on application startup.
static {
System.loadLibrary("ndk01");
}
public int testFun(String a, double b, long c){
return 1;
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
testFun("aa", 4.5, 5);
// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int ret = ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (ret == PackageManager.PERMISSION_GRANTED){
Log.i("tttttttt", "已经有写SDCard的权限了");
String fp1 = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
String fc = readSDCardFile(fp1+"/b.txt");
Log.i("tttttttt", "文件内容:" + fc);
}else{
Log.i("tttttttt", "还没有写SDCard的权限");
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, MY_PERMISSIONS_REQUEST_WRITE_CODE);
}
}
});
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case MY_PERMISSIONS_REQUEST_WRITE_CODE:{
if (grantResults.length > 0 && grantResults[0] != -1){
Log.i("tttttttt", "写SDCard权限申请成功");
}else{
Log.i("tttttttt", "写SDCard权限申请失败");
}
break;
} case 33:{
Log.i("tttttttt", "这里是其他权限申请的结果");
break;
}
}
}
/**
* A native method that is implemented by the 'ndk01' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
public native String readSDCardFile(String filePath);
}
cpp实现读取文件的功能
#include <jni.h>
#include <string>
#include <android/log.h>
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "tttttttt", __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "tttttttt", __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, "tttttttt", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "tttttttt", __VA_ARGS__)
extern "C" JNIEXPORT jstring JNICALL
Java_a_b_c_ndk01_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_a_b_c_ndk01_MainActivity_readSDCardFile(JNIEnv *env, jobject thiz, jstring file_path) {
//先将java类型的字符串转换成c类型的字符
const char* filePath = env->GetStringUTFChars(file_path, nullptr);
FILE *fp = fopen(filePath, "r");
if (fp == nullptr) {
//类似于android中的log.i就是打印日志的功能
LOGE("Failed to open file: %s", filePath);
//释放内存
env->ReleaseStringUTFChars(file_path, filePath);
return env->NewStringUTF("open file failed");
}
char buffer[1024];
std::string result; // 使用 std::string 拼接内容
while (fgets(buffer, sizeof(buffer), fp) != nullptr) {
result += buffer;
}
fclose(fp);
env->ReleaseStringUTFChars(file_path, filePath);
return env->NewStringUTF(result.c_str());
}
JNI方法
参考: https://blog.csdn.net/afei__/article/details/81016413
写一个例子,反射获取类和方法
// 告诉编译器使用 C 语言的函数命名方式,避免函数名被 C++ 编译器修改(名称改编)
extern "C"
// 定义一个 JNI 函数,返回一个 jint(int 类型),对应 Java 中 native 方法:callJavaFunFromJNI
JNIEXPORT jint JNICALL
Java_a_b_c_ndk01_MainActivity_callJavaFunFromJNI(JNIEnv *env, jobject thiz, jobject param) {
// 获取 param 对象的类对象(即 Java 层传入的 Student 对象)
jclass jclass_student = env->GetObjectClass(param);
// 通过类名查找 Student 类(可选,这里其实没用上)
jclass jclass_student2 = env->FindClass("a/b/c/ndk01/Student");
// 获取名为 study 的成员方法 ID,签名表示接收一个 int,返回一个 String
jmethodID jmethodId_study = env->GetMethodID(jclass_student, "study", "(I)Ljava/lang/String;");
// 定义参数传入 Java 的 study 方法
int flag = 34;
// 调用 param 对象的 study 方法,返回一个 jobject(其实是 jstring 类型)
jobject jobject_ret = env->CallObjectMethod(param, jmethodId_study, flag);
// 将返回的 jstring 转换为 C 字符串
char* t = (char*)env->GetStringUTFChars((jstring)jobject_ret, 0);
// 打印 JNI 调用返回的字符串
LOGI("ndk call study ret: %s", t);
// 返回 flag 值(这里只是演示,实际返回值可以按需求设置)
return flag;
}
// 继续使用 C 语言命名规则
extern "C"
// 定义一个 JNI 函数,返回 jstring(Java 字符串),对应 Java 中 native 方法:callStaticJavaFunFromJNI
JNIEXPORT jstring JNICALL
Java_a_b_c_ndk01_MainActivity_callStaticJavaFunFromJNI(JNIEnv *env, jobject thiz) {
// 找到 Student 类
jclass jclass_student2 = env->FindClass("a/b/c/ndk01/Student");
// 获取名为 calcLength 的静态方法 ID,签名表示接收一个 String,返回一个 int
jmethodID jmethodId_calcLength = env->GetStaticMethodID(jclass_student2, "calcLength", "(Ljava/lang/String;)I");
// 创建一个 Java 字符串作为参数传入
jstring jstring_param = env->NewStringUTF("hahahaha");
// 调用静态方法 calcLength,传入字符串参数,获取返回的 int 值
jint jint_ret = env->CallStaticIntMethod(jclass_student2, jmethodId_calcLength, jstring_param);
// 打印返回的 int 值
LOGI("ndk call calcLength ret: %d", jint_ret);
// 返回原始字符串参数(只是为了展示)
return jstring_param;
}
JNI注册
静态注册
静态注册就是通过 JNIEXPORT 和 JNICALL 两个宏定义声明,在虚拟机加载 so 时发现上面两个宏定义的函数时就会链接到对应的 native 方法。
注册的规则:
Java + 包名 + 类名 + 方法名
其中使用下划线将每部分隔开,包名也使用下划线隔开,如果名称中本来就包含下划线,将使用下划线加数字替换。
示例 包名:com.afei.jnidemo,类名:MainActivity)
// Java native method
public native String stringFromJNI();
// JNI method
JNIEXPORT jstring JNICALL
Java_com_afei_jnidemo_MainActivity_stringFromJNI( JNIEnv *env, jobject instance);
// Java native method
public native String stringFrom_JNI();
// JNI method
JNIEXPORT jstring JNICALL
Java_com_afei_jnidemo_MainActivity_stringFrom_1JNI(JNIEnv *env, jobject instance);
动态注册
通过 RegisterNatives 方法手动完成 native 方法和 so 中的方法的绑定,这样虚拟机就可以通过这个函数映射表直接找到相应的方法了。
来看一个例子
public native String stringFromJNI();
public static native int add(int a, int b);
一般在JNI_OnLoad完成注册
#include <jni.h>
#include <string>
#include "log.hpp"
extern "C" {
jstring stringFromJNI(JNIEnv *env, jobject instance) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
jint add(JNIEnv *env, jclass clazz, jint a, jint b) {
return a + b;
}
jint RegisterNatives(JNIEnv *env) {
jclass clazz = env->FindClass("com/afei/jnidemo/MainActivity");
if (clazz == NULL) {
LOGE("con't find class: com/afei/jnidemo/MainActivity");
return JNI_ERR;
}
JNINativeMethod methods_MainActivity[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI},
{"add", "(II)I", (void *) add}
};
// int len = sizeof(methods_MainActivity) / sizeof(methods_MainActivity[0]);
return env->RegisterNatives(clazz, methods_MainActivity,
sizeof(methods_MainActivity) / sizeof(methods_MainActivity[0]));
}
jint JNI_OnLoad(JavaVM *vm, void *reserved) {//这个方法是一个override方法,在加载动态库时,会自动调用,一般用来做一些初始化操作,动态注册的代码就可以写在这
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {//首先需要获取JNIEnv *env指针,因为registerNativeMethod方法会用到
return JNI_ERR;
}
// 注册本地方法(动态注册的关键一步)
jint result = RegisterNatives(env);
LOGD("RegisterNatives result: %d", result);
return JNI_VERSION_1_6;
}
}
RegisterNatives方法解析
定义: jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)
clazz:指定的类,即 native 方法所属的类
methods:方法数组,这里需要了解一下 JNINativeMethod 结构体
nMethods:方法数组的长度
JNINativeMethod
typedef struct {
const char* name; // native 的方法名
const char* signature; // 方法签名,例如 ()Ljava/lang/String;
void* fnPtr; // 函数指针
} JNINativeMethod;
Android.mk 和 CMake 语法
参考:Android.mk 语法和变量介绍CMakeLists.txt 语法介绍与实例演练
Android Studio 中使用 NDK
gradle 配置 cmake,及各参数详解
gardle 配置 ndk 指定 ABI: abiFilters 详解
APK文件结构
参考: https://bbs.kanxue.com/thread-278112.htm
assets文件夹
assets 这里存放的是静态资源文件(图片,视频等),这个文件夹下的资源文件不会被编译。不被编译的资源文件是指在编译过程中不会被转换成二进制代码的文件,而是直接被打包到最终的程序中。这些文件通常是一些静态资源,如图片、音频、文本文件等。
lib文件夹
lib:.so库(c或c++编译的动态链接库)。APK文件中的动态链接库(Dynamic Link Library,简称DLL)是一种可重用的代码库,它包含在应用程序中,以便在运行时被调用。这些库通常包含许多常见的函数和程序,可以在多个应用程序中共享,从而提高了代码的复用性和效率。
lib文件夹下的每个目录都适用于不同的环境下,armeabi-v7a目录基本通用所有android设备,arm64-v8a目录只适用于64位的android设备,x86目录常见用于android模拟器,x86-64目录适用于支持x86_64架构的Android设备(适用于支持通常称为“x86-64”的指令集的 CPU)
META-INF文件夹
META-INF:在Android应用的APK文件中,META-INF文件夹是存放数字签名相关文件的文件夹,包含以下三个文件:
-
MANIFEST.MF:MANIFEST.MF 是一个摘要清单文件,它包含了 APK 文件中除自身外所有文件的数字摘要。这些摘要通常是通过特定的哈希算法(如 SHA - 1、SHA - 256 等)对文件内容进行计算得到的,用于确保文件内容在传输或存储过程中未被篡改。 -
CERT.SF:CERT.SF 文件存储了 MANIFEST.MF 文件的数字摘要以及 MANIFEST.MF 中每个文件条目的数字摘要的二次摘要。开发者使用自己的私钥对 CERT.SF 进行签名,以保证 CERT.SF 文件内容的完整性和真实性。 -
CERT.RSA:CERT.RSA 文件包含了使用开发者私钥对 CERT.SF 文件进行签名得到的数字签名以及签名时所使用的数字证书。当验证 APK 的签名时,系统会使用数字证书中的公钥来验证 CERT.SF 文件的数字签名是否有效,从而确保 CERT.SF 文件未被篡改,进而验证 MANIFEST.MF 文件和整个 APK 的完整性。
AndroidManifest.xml配置文件
AndroidManifest.xml是Android应用程序中最重要的文件之一,它包含了应用程序的基本信息,如应用程序的名称、图标、版本号、权限、组件(Activity、Service、BroadcastReceiver、Content Provider)等等。在应用程序运行时,系统会根据这个文件来管理应用程序的生命周期,启动和关闭应用程序,管理应用程序的组件等等。
我们来了解一下AndroidManifest.xml文件的主要组成部分:
-
manifest标签
manifest标签是AndroidManifest.xml文件的根标签,它包含了应用程序的基本信息,如包名、版本号、SDK版本、应用程序的名称和图标等等。
-
application标签
application标签是应用程序的主要标签,它包含了应用程序的所有组件,如Activity(活动)、Service(服务)、Broadcast Receiver(广播接收器)、Content Provider(内容提供者)等等。在application标签中,也可以设置应用程序的全局属性,如主题、权限等等。
-
activity标签
activity标签定义了一个Activity组件,它包含了Activity的基本信息,如Activity的名称、图标、主题、启动模式等等。在activity标签中,还可以定义Activity的布局、Intent过滤器等等。
-
service标签
service标签定义了一个Service组件,它包含了Service的基本信息,如Service的名称、图标、启动模式等等。在service标签中,还可以定义Service的Intent过滤器等等。
-
receiver标签
receiver标签定义了一个BroadcastReceiver组件,它包含了BroadcastReceiver的基本信息,如BroadcastReceiver的名称、图标、权限等等。在receiver标签中,还可以定义BroadcastReceiver的Intent过滤器等等。
-
provider标签
provider标签定义了一个Content Provider组件,它包含了Content Provider的基本信息,如Content Provider的名称、图标、权限等等。在provider标签中,还可以定义Content Provider的URI和Mime Type等等。
-
uses-permission标签
uses-permission标签定义了应用程序需要的权限,如访问网络、读取SD卡等等。在应用程序安装时,系统会提示用户授权这些权限。
-
uses-feature标签
uses-feature标签定义了应用程序需要的硬件或软件特性,如摄像头、GPS等等。在应用程序安装时,系统会检查设备是否支持这些特性。
以上是AndroidManifest.xml文件的主要组成部分,它们共同定义了应用程序的基本信息和组件,是应用程序的重要配置文件。现在如果看起来有点懵,没关系,后面实战会使用到它的,以后也会对它进行详解,那时你或许会有一点对它的理解了。
resources.arsc文件
resources.arsc文件是Android应用程序的资源文件之一,它是一个二进制文件,包含了应用程序的所有资源信息,例如布局文件、字符串、图片等。这个文件在应用程序编译过程中由aapt工具生成,并被打包进APK文件中。
resources.arsc文件的主要作用是提供资源的索引和映射关系。它将资源文件名、类型、值等信息映射到一个唯一的整数ID上。这个ID在R文件中定义,并且可以通过代码中的R类来引用。例如,R.layout.main表示布局文件main.xml对应的ID,R.string.app_name表示字符串资源app_name对应的ID。
当应用程序运行时,系统会根据R类中的ID来查找对应的资源,并将其加载到内存中,供应用程序使用。这个过程是通过解析resources.arsc文件和R类实现的。通过这种方式,应用程序可以方便地访问和使用资源,而不需要手动处理资源文件的位置和命名等问题。
需要注意的是,resources.arsc文件只包含资源的索引和映射关系,并不包含实际的资源内容。实际的资源内容存储在res文件夹中,按照资源类型和名称进行组织。当应用程序需要使用资源时,系统会根据resources.arsc文件中的索引信息找到对应的资源文件,并将其加载到内存中。
总之,resources.arsc文件是Android应用程序的资源文件之一,包含了资源的索引和映射关系。它和R类一起构成了应用程序访问和使用资源的基础。通过解析resources.arsc文件和使用R类,应用程序可以方便地加载和使用资源。
参考其他的文章Android资源管理及资源的编译和打包过程分析 - 掘金 (juejin.cn)9J5c8Y4m8G2M7%4c8Q4x3V1j53y4o6b7%5E5x3K6f1#2z5e0j5J5y4o6l9@1z5o6M7%4)
(32条消息) 手把手教你解析Resources.arsc_beyond702的博客-CSDN博客9J5k6h3&6W2N6q4)9J5c8X3u0W2P5h3!0F1k6o6M7H3x3W2)9J5c8X3q4J5N6r3W2U0L8r3g2Q4x3V1k6V1k6i4c8S2K9h3I4K6i4K6u0r3y4e0p5%4y4o6b7H3z5o6t1%60.)
Android逆向:resource.arsc文件解析(Config List) - 掘金 (juejin.cn)9J5c8Y4m8G2M7%4c8Q4x3V1j5%4x3o6M7J5z5o6V1%4x3K6t1%4y4e0l9&6x3U0M7@1y4U0x3I4i4K6t1K6K9r3g2S2k6r3W2F1k6#2)9J5k6o6M7%60.)
(32条消息) resource.arsc二进制内容解析 之 Dynamic package reference_BennuCTech的博客-CSDN博客9J5k6h3&6W2N6q4)9J5c8X3y4Z5P5Y4m8Z5L83I4W2i4K6u0r3k6r3g2@1j5h3W2D9M7#2)9J5c8U0R3H3y4e0j5&6y4e0j5%4)
res文件夹
res:资源文件目录,二进制格式。实际上,APK文件下的res文件夹并不是二进制格式,而是经过编译后的二进制资源文件。在Android应用程序开发中,资源文件通常是以XML格式存储的,如布局文件、字符串资源、颜色资源等。在编译时,Android编译器会将这些XML资源文件编译成二进制格式的资源文件,以提高应用程序的运行效率和安全性。虽然res文件夹下的二进制资源文件不能直接编辑和修改,但是开发者仍然可以通过Android提供的资源管理工具,如aapt、apktool等,来反编译和编辑这些资源文件的。
在res文件夹中,主要包含以下子文件夹和文件:
|
|
---|---|
animator/ |
. .)的 XML 文件。 |
anim/ |
animator/ 目录。 |
color/ |
|
drawable/ |
.9.png 、JPG 或 GIF)或编译为以下可绘制资源子类型的 XML 文件:位图文件九宫图(可调整大小的位图)状态列表形状动画可绘制对象其他可绘制对象如需了解详情,请参阅可绘制资源9J5c8Y4c8G2M7r3W2U0M7#2)9J5c8Y4u0W2M73!0#2M7X3y4W2M7#2)9J5c8X3c8J5j5i4N6S2j5X3I4W2i4K6u0V1M7X3g2K6L8%4g2J5j53g2Q4x3@1k6Z5L8q4)9K6c8s2A6Z5i4K6u0V1j5$3^5%60.)。 |
mipmap/ |
mipmap/ 文件夹管理启动器图标,请参阅将应用图标放在 mipmap 目录中9J5c8X3#2#2L8s2c8A6M73y4J5k6h3g2F1k6r3g2F1M7$3W2@1K9h3g2K6i4K6y4r3K9r3I4Q4x3@1c8*7K9q4)9J5k6r3y4F1i4K6t1K6L8h3W2H3L8h3q4H3)。 |
layout/ |
|
menu/ |
. .)。 |
raw/ |
InputStream 打开这些资源,请使用资源 ID(即 R.raw.*filename* )调用 Resources.openRawResource() 。但是,如需访问原始文件名和文件层次结构,请考虑将资源保存在 assets/ 目录(而非 res/raw/ )下。assets/ 中的文件没有资源 ID,因此您只能使用 AssetManager 读取这些文件。 |
values/ |
res/ 子目录中的 XML 资源文件会根据 XML 文件名定义单个资源,而 values/ 目录中的文件可描述多个资源。对于此目录中的文件,<resources> 元素的每个子元素均会定义一个资源。例如,<string> 元素会创建 R.string 资源,<color> 元素会创建 R.color 资源。由于每个资源均使用自己的 XML 元素进行定义,因此您可以随意命名文件,并在某个文件中放入不同的资源类型。但是,您可能需要将独特的资源类型放在不同的文件中,使其一目了然。例如,对于可在此目录中创建的资源,下面给出了相应的文件名约定:arrays.xml 用于资源数组(类型化数组9J5c8Y4c8G2M7r3W2U0M7#2)9J5c8Y4u0W2M73!0#2M7X3y4W2M7#2)9J5c8X3#2G2M7X3g2Q4x3X3c8J5k6i4y4G2N6i4u0U0k6i4y4Q4x3@1k6Z5L8q4)9K6c8s2A6Z5i4K6u0V1j53&6Q4x3U0y4f1P5i4m8W2k6p5q4J5M7X3q4&6))colors.xml 用于颜色值9J5c8Y4c8G2M7r3W2U0M7#2)9J5c8Y4u0W2M73!0#2M7X3y4W2M7#2)9J5c8X3#2G2M7X3g2Q4x3X3c8J5k6i4y4G2N6i4u0U0k6i4y4Q4x3@1k6Z5L8q4)9K6c8s2A6Z5i4K6u0V1j53&6Q4x3U0y4o6L8$3I4G2M7R3. .)dimens.xml 用于维度值9J5c8Y4c8G2M7r3W2U0M7#2)9J5c8Y4u0W2M73!0#2M7X3y4W2M7#2)9J5c8X3#2G2M7X3g2Q4x3X3c8J5k6i4y4G2N6i4u0U0k6i4y4Q4x3@1k6Z5L8q4)9K6c8s2A6Z5i4K6u0V1j53&6Q4x3U0y4p5K9h3#2W2L8Y4y4A6L8$3^5.) strings.xml 用于[字符串值](https://bbs.kanxue.com/elink@eb4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2i4K6u0W2j5$3&6Q4x3V1k6Y4N6h3W2V1k6g2)9J5c8Y4c8G2M7r3W2U0M7#2)9J5c8Y4u0W2M7$3!0#2M7X3y4W2M7#2)9J5c8Y4y4@1M7X3W2F1k6#2)9J5k6s2u0W2M7$3!0#2M7X3y4W2i4K6y4r3K9r3I4Q4x3@1c8*7K9q4)9J5k6r3y4F1) styles.xml 用于[样式](https://bbs.kanxue.com/elink@fedK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2i4K6u0W2j5$3&6Q4x3V1k6Y4N6h3W2V1k6g2)9J5c8Y4c8G2M7r3W2U0M7#2)9J5c8Y4u0W2M7$3!0#2M7X3y4W2M7#2)9J5c8Y4y4@1P5h3I4W2i4K6u0V1M7X3g2K6L8%4g2J5j5$3g2Q4x3@1k6Z5L8q4)9K6c8s2A6Z5i4K6u0V1j5$3^5%60.)如需了解详情,请参阅[字符串资源](https://bbs.kanxue.com/elink@971K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2i4K6u0W2j5$3&6Q4x3V1k6Y4N6h3W2V1k6g2)9J5c8Y4c8G2M7r3W2U0M7#2)9J5c8Y4u0W2M7$3!0#2M7X3y4W2M7#2)9J5c8Y4y4@1M7X3W2F1k6#2)9J5k6s2u0W2M7$3!0#2M7X3y4W2i4K6y4r3K9r3I4Q4x3@1c8*7K9q4)9J5k6r3y4F1)、[样式资源](https://bbs.kanxue.com/elink@3cbK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2i4K6u0W2j5$3&6Q4x3V1k6Y4N6h3W2V1k6g2)9J5c8Y4c8G2M7r3W2U0M7#2)9J5c8Y4u0W2M7$3!0#2M7X3y4W2M7#2)9J5c8Y4y4@1P5h3I4W2i4K6u0V1M7X3g2K6L8%4g2J5j5$3g2Q4x3@1k6Z5L8q4)9K6c8s2A6Z5i4K6u0V1j5$3^5%60.)和[更多资源类型](https://bbs.kanxue.com/elink@f35K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2i4K6u0W2j5$3&6Q4x3V1k6Y4N6h3W2V1k6g2)9J5c8Y4c8G2M7r3W2U0M7#2)9J5c8Y4u0W2M7$3!0#2M7X3y4W2M7#2)9J5c8X3#2G2M7X3g2Q4x3X3c8J5k6i4y4G2N6i4u0U0k6i4y4Q4x3@1k6Z5L8q4)9K6c8s2A6Z5i4K6u0V1j5$3^5 .)。 |
xml/ |
Resources.getXML() 读取的任意 XML 文件。各种 XML 配置文件(例如搜索配置9J5c8Y4c8G2M7r3W2U0M7#2)9J5c8Y4y4W2j5i4u0U0K9q4)9J5c8Y4y4W2j5i4u0U0K9r3q4T1L8r3g2Q4x3X3c8U0L83&6X3K9h3N6Q4x3@1k6Z5L8q4)9K6c8s2A6Z5i4K6u0V1j53^5`.))都必须保存在此处。 |
font/ |
<font-family> 元素的 XML 文件。如需详细了解以资源形式使用的字体,请参阅将字体添加为 XML 资源9J5c8Y4c8G2M7r3W2U0M7#2)9J5c8Y4g2A6i4K6u0r3L8r3!0G2K9#2)9J5k6r3q4F1k6q4)9J5k6r3k6W2k6h3I4Q4x3V1k6X3L8$3&6@1M7#2)9J5k6r3W2F1i4K6u0V1P5r3#2D9i4K6y4r3K9r3I4Q4x3@1c8*7K9q4)9J5k6r3y4F1)。 |
dex文件结构
https://cloud.tencent.com/developer/article/1663852 https://juejin.cn/post/6844903847647772686
如何将编译dex文件
./d8 --debug --output dex输出路径 class文件
再来一个简洁的图
-
header :
DEX 文件头,记录了一些当前文件的信息以及其他数据结构在文件中的偏移量 -
string_ids :
字符串的偏移量 -
type_ids :
类型信息的偏移量 -
proto_ids :
方法声明的偏移量 -
field_ids :
字段信息的偏移量 -
method_ids :
方法信息(所在类,方法声明以及方法名)的偏移量 -
class_def :
类信息的偏移量 -
data :
: 数据区 -
link_data :
静态链接数据区
从 header
到 data
之间都是偏移量数组,并不存储真实数据,所有数据都存在 data
数据区,根据其偏移量区查找。对 DEX 文件有了一个大概的认识之后,我们就来详细分析一下各个部分。
DEX 文件头部分的具体格式可以参考 DexFile.h 中的定义:
struct DexHeader {
u1 magic[8]; // 魔数
u4 checksum; // adler 校验值
u1 signature[kSHA1DigestLen]; // sha1 校验值
u4 fileSize; // DEX 文件大小
u4 headerSize; // DEX 文件头大小
u4 endianTag; // 字节序
u4 linkSize; // 链接段大小
u4 linkOff; // 链接段的偏移量
u4 mapOff; // DexMapList 偏移量
u4 stringIdsSize; // DexStringId 个数
u4 stringIdsOff; // DexStringId 偏移量
u4 typeIdsSize; // DexTypeId 个数
u4 typeIdsOff; // DexTypeId 偏移量
u4 protoIdsSize; // DexProtoId 个数
u4 protoIdsOff; // DexProtoId 偏移量
u4 fieldIdsSize; // DexFieldId 个数
u4 fieldIdsOff; // DexFieldId 偏移量
u4 methodIdsSize; // DexMethodId 个数
u4 methodIdsOff; // DexMethodId 偏移量
u4 classDefsSize; // DexCLassDef 个数
u4 classDefsOff; // DexClassDef 偏移量
u4 dataSize; // 数据段大小
u4 dataOff; // 数据段偏移量
};
magic
一般是常量,用来标记 DEX 文件,它可以分解为:
文件标识 dex + 换行符 + DEX 版本 + 0
字符串格式为 dexn035�
,十六进制为 0x6465780A30333500
。
checksum
是对去除 magic
、 checksum
以外的文件部分作 alder32 算法得到的校验值,用于判断 DEX 文件是否被篡改。
signature
是对除去 magic
、 checksum
、 signature
以外的文件部分作 sha1 得到的文件哈希值。
endianTag
用于标记 DEX 文件是大端表示还是小端表示。由于 DEX 文件是运行在 Android 系统中的,所以一般都是小端表示,这个值也是恒定值 0x12345678
。
string_ids
string_ids
是一个表,保存了 所有字符串的引用地址
struct DexStringId {
u4 stringDataOff;
};
先来写一个工具类后面的也都用这个工具类
// 从字节数组中读取 4 字节 little-endian 整数
public int readInt(byte[] data, int offset) {
return ((data[offset] & 0xFF)) |
((data[offset + 1] & 0xFF) << 8) |
((data[offset + 2] & 0xFF) << 16) |
((data[offset + 3] & 0xFF) << 24);
}
// 从指定 offset 位置开始读取一个 ULEB128 编码的整数
// 返回值:[整数值,占用字节数]
public int[] readUleb128(byte[] data, int offset) {
int result = 0; // 解析结果
int count = 0; // ULEB128 占用的字节数
int cur;
int shift = 0;
do {
cur = data[offset + count] & 0xFF; // 当前字节
result |= (cur & 0x7F) << shift; // 去掉最高位后累加到结果
shift += 7;
count++;
} while ((cur & 0x80) != 0); // 如果最高位为1,继续读下一个字节
return new int[]{result, count};
}
// 从 offset 开始读取 UTF-8 编码的字符串,直到遇到 � 结束
public String readString(byte[] data, int offset) {
int end = offset;
while (data[end] != 0) end++; // 找到 null terminator
return new String(data, offset, end - offset, StandardCharsets.UTF_8);
}
解析
// 解析 DEX 文件中的 string_ids 表
public void parseStringIds(byte[] dexData) {
// 从 DEX 文件头偏移 0x38 读取 string_ids_size(字符串数量)
int stringIdsSize = readInt(dexData, 0x38);
// 从偏移 0x3C 读取 string_ids_off(string_id 表的起始位置)
int stringIdsOff = readInt(dexData, 0x3C);
System.out.println("Total strings: " + stringIdsSize);
// 遍历所有 string_id_item
for (int i = 0; i < stringIdsSize; i++) {
// 每个 string_id 占 4 字节,表示 string_data_item 的偏移地址
int stringDataOff = readInt(dexData, stringIdsOff + i * 4);
// 读取 ULEB128 编码的字符串长度(字符数量,不是字节数),并获得该编码占用的字节数
int[] result = readUleb128(dexData, stringDataOff);
int utf16Size = result[0]; // 实际字符数(通常你可以忽略它)
int stringOffset = stringDataOff + result[1]; // 字符串真实内容的起始位置
// 从 offset 开始读取 UTF-8 编码的字符串内容(直到遇到 0x00 为止)
String value = readString(dexData, stringOffset);
// 输出解析到的字符串
System.out.printf("string[%d] = %sn", i, value);
}
}
type_ids
struct DexTypeId {
u4 descriptorIdx;
};
type_ids
表示的是类型信息,descriptorIdx
指向 string_ids
中元素。根据索引直接在上一步读取到的字符串池即可解析对应的类型信息,代码如下:
int typeIdsSize = Utils.readInt(dexData, 0x40);
int typeIdsOff = Utils.readInt(dexData, 0x44);
for (int i = 0; i < typeIdsSize; i++) {
int stringIndex = Utils.readInt(dexData, typeIdsOff + i * 4); // 获取 type_id 对应的 string_ids 索引
// 接下来就是去 string_ids 表中找到那个 string_data_off
int stringIdsOff = Utils.readInt(dexData, 0x3C); // string_ids 的起始地址
int stringDataOff = Utils.readInt(dexData, stringIdsOff + stringIndex * 4); // 找到该字符串的偏移
// 读取实际字符串内容(记得是 ULEB128 编码)
int[] result = Utils.readUleb128(dexData, stringDataOff);
int size = result[0];
int offset = stringDataOff + result[1];
String typeString = Utils.readString(dexData, offset); // 实际读取字符串内容
System.out.println("type[" + i + "] = " + typeString);
}
proto_ids
struct DexProtoId {
u4 shortyIdx; /* index into stringIds for shorty descriptor */
u4 returnTypeIdx; /* index into typeIds list for return type */
u4 parametersOff; /* file offset to type_list for parameter types */
};
proto_ids
表示方法声明信息,它包含以下三个变量:
-
shortyIdx : 指向 string_ids ,表示方法声明的字符串 -
returnTypeIdx : 指向 type_ids ,表示方法的返回类型 -
parametersOff : 方法参数列表的偏移量 方法参数列表的数据结构在 DexFile.h 中用 DexTypeList
来表示:
struct DexTypeList {
u4 size; /* #of entries in list */
DexTypeItem list[1]; /* entries */
};
struct DexTypeItem {
u2 typeIdx; /* index into typeIds */
};
size
表示方法参数的个数,参数用 DexTypeItem
表示,它只有一个属性 typeIdx
,指向 type_ids
中对应项。
解析
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class DexProtoParser {
public static void main(String[] args) throws IOException {
// 读取整个 dex 文件内容到 byte[] 中
FileInputStream fis = new FileInputStream("your.dex"); // 修改为实际路径
byte[] dexData = fis.readAllBytes();
fis.close();
// 调用解析方法
parseProtoIds(dexData);
}
// 解析 proto_ids 表
public static void parseProtoIds(byte[] dexData) {
// 从 header 中读取 proto_ids 的数量和偏移地址
int protoIdsSize = readInt(dexData, 0x44); // proto_ids_size
int protoIdsOff = readInt(dexData, 0x48); // proto_ids_off
System.out.println("Total proto_ids: " + protoIdsSize);
for (int i = 0; i < protoIdsSize; i++) {
// 每个 proto_id 项固定占 12 字节
int base = protoIdsOff + i * 12;
int shortyIdx = readInt(dexData, base); // 指向 string_ids 表,用于描述方法签名(shorty)
int returnTypeIdx = readInt(dexData, base + 4); // 指向 type_ids,表示返回类型
int parametersOff = readInt(dexData, base + 8); // 偏移到参数类型表(type_list)
// 获取字符串表示
String shorty = getStringById(dexData, shortyIdx);
String returnType = getTypeString(dexData, returnTypeIdx);
String[] params = getParamTypeList(dexData, parametersOff);
// 打印解析结果
System.out.printf("proto[%d]: shorty=%s, return=%s, params=%sn",
i, shorty, returnType, String.join(", ", params));
}
}
// 从 byte[] 中读取一个小端 4 字节整数
public static int readInt(byte[] data, int offset) {
return ((data[offset] & 0xFF)) |
((data[offset + 1] & 0xFF) << 8) |
((data[offset + 2] & 0xFF) << 16) |
((data[offset + 3] & 0xFF) << 24);
}
// 读取 ULEB128(可变长度整型编码),返回:[值,占用字节数]
public static int[] readUleb128(byte[] data, int offset) {
int result = 0;
int count = 0;
int shift = 0;
int b;
do {
b = data[offset + count] & 0xFF;
result |= (b & 0x7F) << shift;
shift += 7;
count++;
} while ((b & 0x80) != 0);
return new int[]{result, count};
}
// 读取字符串:string_ids → string_data_item → UTF-8字符串
public static String getStringById(byte[] data, int stringId) {
// string_ids_off 存储在 header 的 0x3C 处
int stringIdsOff = readInt(data, 0x3C);
// 每个 string_id 占 4 字节,值是 string_data_item 的偏移地址
int stringDataOff = readInt(data, stringIdsOff + stringId * 4);
// 跳过前面的 uleb128(表示 utf16 字符长度),读取 UTF-8 内容
int[] result = readUleb128(data, stringDataOff);
int contentOffset = stringDataOff + result[1];
return readString(data, contentOffset);
}
// 从 type_ids 中获取类型描述字符串(例如 Ljava/lang/String;)
public static String getTypeString(byte[] data, int typeIdx) {
int typeIdsOff = readInt(data, 0x40); // header 中的 type_ids_off
int descriptorIdx = readInt(data, typeIdsOff + typeIdx * 4); // 指向 string_ids
return getStringById(data, descriptorIdx);
}
// 读取 type_list(参数类型列表)
public static String[] getParamTypeList(byte[] data, int parametersOff) {
if (parametersOff == 0) return new String[0]; // 没有参数
// 读取参数数量(4字节)
int size = readInt(data, parametersOff);
String[] types = new String[size];
// 每个参数类型索引占 2 字节(type_idx)
for (int i = 0; i < size; i++) {
int typeIdx = ((data[parametersOff + 4 + i * 2] & 0xFF)
| ((data[parametersOff + 4 + i * 2 + 1] & 0xFF) << 8));
types[i] = getTypeString(data, typeIdx);
}
return types;
}
// 读取 null 结尾的 UTF-8 字符串
public static String readString(byte[] data, int offset) {
int end = offset;
while (end < data.length && data[end] != 0) {
end++;
}
return new String(data, offset, end - offset, StandardCharsets.UTF_8);
}
}
field_ids
struct DexFieldId {
u2 classIdx; /* index into typeIds list for defining class */
u2 typeIdx; /* index into typeIds for field type */
u4 nameIdx; /* index into stringIds for field name */
};
field_ids
表示的是字段信息,指明了字段所在的类,字段的类型以及字段名称,在 DexFile.h
中定义为 DexFieldId
, 其各个字段含义如下:
-
classIdx : 指向 type_ids ,表示字段所在类的信息 -
typeIdx : 指向 ype_ids ,表示字段的类型信息 -
nameIdx : 指向 string_ids ,表示字段名称
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class DexFieldParser {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("your.dex"); // 替换成实际 dex 路径
byte[] dexData = fis.readAllBytes();
fis.close();
parseFieldIds(dexData);
}
public static void parseFieldIds(byte[] dexData) {
// 读取 header 中 field_ids 的数量和偏移
int fieldIdsSize = readInt(dexData, 0x58); // field_ids_size
int fieldIdsOff = readInt(dexData, 0x5C); // field_ids_off
System.out.println("Total field_ids: " + fieldIdsSize);
for (int i = 0; i < fieldIdsSize; i++) {
int base = fieldIdsOff + i * 8;
int classIdx = readU2(dexData, base); // 指向 declaring class (type_ids 索引)
int typeIdx = readU2(dexData, base + 2); // 指向 field type (type_ids 索引)
int nameIdx = readInt(dexData, base + 4); // 指向 field name (string_ids 索引)
String classType = getTypeString(dexData, classIdx);
String fieldType = getTypeString(dexData, typeIdx);
String fieldName = getStringById(dexData, nameIdx);
System.out.printf("field[%d]: class=%s, type=%s, name=%sn", i, classType, fieldType, fieldName);
}
}
// 读取小端 4 字节整数
public static int readInt(byte[] data, int offset) {
return (data[offset] & 0xFF)
| ((data[offset + 1] & 0xFF) << 8)
| ((data[offset + 2] & 0xFF) << 16)
| ((data[offset + 3] & 0xFF) << 24);
}
// 读取小端 2 字节整数(U2)
public static int readU2(byte[] data, int offset) {
return (data[offset] & 0xFF)
| ((data[offset + 1] & 0xFF) << 8);
}
// 读取 string_id → string_data → UTF-8 字符串
public static String getStringById(byte[] data, int stringId) {
int stringIdsOff = readInt(data, 0x3C); // string_ids_off
int stringDataOff = readInt(data, stringIdsOff + stringId * 4);
int[] uleb = readUleb128(data, stringDataOff);
int stringOffset = stringDataOff + uleb[1]; // 跳过 uleb128 的字符长度
return readString(data, stringOffset);
}
// 读取 type_id → string_id → string_data → UTF-8 类型字符串
public static String getTypeString(byte[] data, int typeId) {
int typeIdsOff = readInt(data, 0x40);
int descriptorIdx = readInt(data, typeIdsOff + typeId * 4);
return getStringById(data, descriptorIdx);
}
// 读取 null 结尾 UTF-8 字符串
public static String readString(byte[] data, int offset) {
int end = offset;
while (end < data.length && data[end] != 0) {
end++;
}
return new String(data, offset, end - offset, StandardCharsets.UTF_8);
}
// 读取 ULEB128 编码,返回:值 + 占用字节数
public static int[] readUleb128(byte[] data, int offset) {
int result = 0;
int count = 0;
int shift = 0;
int b;
do {
b = data[offset + count] & 0xFF;
result |= (b & 0x7F) << shift;
shift += 7;
count++;
} while ((b & 0x80) != 0);
return new int[]{result, count};
}
}
method_ids
struct DexMethodId {
u2 classIdx; /* index into typeIds list for defining class */
u2 protoIdx; /* index into protoIds for method prototype */
u4 nameIdx; /* index into stringIds for method name */
};
method_ids
指明了方法所在的类、方法声明以及方法名。在 DexFile.h 中用 DexMethodId
表示该项,其属性含义如下:
-
classIdx : 指向 type_ids ,表示类的类型 -
protoIdx : 指向 type_ids ,表示方法声明 -
nameIdx : 指向 string_ids ,表示方法名
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class DexMethodParser {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("your.dex"); // 替换为实际 dex 文件路径
byte[] dexData = fis.readAllBytes();
fis.close();
parseMethodIds(dexData);
}
public static void parseMethodIds(byte[] dexData) {
int methodIdsSize = readInt(dexData, 0x60); // method_ids_size 偏移为 0x60
int methodIdsOff = readInt(dexData, 0x64); // method_ids_off 偏移为 0x64
System.out.println("Total method_ids: " + methodIdsSize);
for (int i = 0; i < methodIdsSize; i++) {
int base = methodIdsOff + i * 8;
int classIdx = readU2(dexData, base); // method 所属类,type_ids 的索引
int protoIdx = readU2(dexData, base + 2); // 方法的签名结构,proto_ids 的索引
int nameIdx = readInt(dexData, base + 4); // 方法名字符串,string_ids 的索引
String classType = getTypeString(dexData, classIdx);
String methodName = getStringById(dexData, nameIdx);
String protoDesc = getProtoString(dexData, protoIdx);
System.out.printf("method[%d]: class=%s, proto=%s, name=%sn", i, classType, protoDesc, methodName);
}
}
// ----------- 以下是通用辅助方法 ------------
public static int readInt(byte[] data, int offset) {
return (data[offset] & 0xFF)
| ((data[offset + 1] & 0xFF) << 8)
| ((data[offset + 2] & 0xFF) << 16)
| ((data[offset + 3] & 0xFF) << 24);
}
public static int readU2(byte[] data, int offset) {
return (data[offset] & 0xFF)
| ((data[offset + 1] & 0xFF) << 8);
}
public static String getStringById(byte[] data, int stringId) {
int stringIdsOff = readInt(data, 0x3C); // string_ids_off
int stringDataOff = readInt(data, stringIdsOff + stringId * 4);
int[] uleb = readUleb128(data, stringDataOff);
int stringOffset = stringDataOff + uleb[1];
return readString(data, stringOffset);
}
public static String getTypeString(byte[] data, int typeId) {
int typeIdsOff = readInt(data, 0x40);
int descriptorIdx = readInt(data, typeIdsOff + typeId * 4);
return getStringById(data, descriptorIdx);
}
public static String getProtoString(byte[] data, int protoId) {
int protoIdsOff = readInt(data, 0x44);
int base = protoIdsOff + protoId * 12;
int shortyIdx = readInt(data, base);
int returnTypeIdx = readInt(data, base + 4);
int parametersOff = readInt(data, base + 8);
String returnType = getTypeString(data, returnTypeIdx);
StringBuilder params = new StringBuilder();
if (parametersOff != 0) {
int size = readInt(data, parametersOff);
for (int i = 0; i < size; i++) {
int typeIdx = readU2(data, parametersOff + 4 + i * 2);
String typeStr = getTypeString(data, typeIdx);
params.append(typeStr);
if (i != size - 1) {
params.append(", ");
}
}
}
return "(" + params + ") → " + returnType;
}
public static String readString(byte[] data, int offset) {
int end = offset;
while (end < data.length && data[end] != 0) {
end++;
}
return new String(data, offset, end - offset, StandardCharsets.UTF_8);
}
public static int[] readUleb128(byte[] data, int offset) {
int result = 0;
int count = 0;
int shift = 0;
int b;
do {
b = data[offset + count] & 0xFF;
result |= (b & 0x7F) << shift;
shift += 7;
count++;
} while ((b & 0x80) != 0);
return new int[]{result, count};
}
}
class_def
struct DexClassDef {
u4 classIdx; /* index into typeIds for this class */
u4 accessFlags;
u4 superclassIdx; /* index into typeIds for superclass */
u4 interfacesOff; /* file offset to DexTypeList */
u4 sourceFileIdx; /* index into stringIds for source file name */
u4 annotationsOff; /* file offset to annotations_directory_item */
u4 classDataOff; /* file offset to class_data_item */
u4 staticValuesOff; /* file offset to DexEncodedArray */
};
class_def
是 DEX 文件结构中最复杂也是最核心的部分,它表示了类的所有信息,对应 DexFile.h
中的 DexClassDef
:
-
classIdx : 指向 type_ids ,表示类信息 -
accessFlags : 访问标识符 -
superclassIdx : 指向 type_ids ,表示父类信息 -
interfacesOff : 指向 DexTypeList 的偏移量,表示接口信息 -
sourceFileIdx : 指向 string_ids ,表示源文件名称 -
annotationOff : 注解信息 -
classDataOff : 指向 DexClassData 的偏移量,表示类的数据部分 -
staticValueOff :指向 DexEncodedArray 的偏移量,表示类的静态数据
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class DexClassDefsParser {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("your.dex"); // 将 "your.dex" 替换为你的 dex 文件路径
byte[] dexData = fis.readAllBytes(); // 读取整个 dex 文件为字节数组
fis.close();
parseClassDefs(dexData);
}
public static void parseClassDefs(byte[] dexData) {
// 读取 class_defs_size(类定义数量),偏移地址 0x70
int classDefsSize = readInt(dexData, 0x70);
// 读取 class_defs_off(类定义开始偏移),偏移地址 0x74
int classDefsOff = readInt(dexData, 0x74);
System.out.println("Total class_defs: " + classDefsSize);
// 遍历每一个 class_def 项,每项固定 32 字节
for (int i = 0; i < classDefsSize; i++) {
int base = classDefsOff + i * 32;
int classIdx = readInt(dexData, base); // 当前类在 type_ids 中的索引
int accessFlags = readInt(dexData, base + 4); // 访问标志(public、final 等)
int superClassIdx = readInt(dexData, base + 8); // 父类索引(type_ids)
int interfacesOff = readInt(dexData, base + 12); // 实现的接口偏移(指向 type_list)
int sourceFileIdx = readInt(dexData, base + 16); // 源文件名在 string_ids 中的索引
int annotationsOff = readInt(dexData, base + 20);// 注解偏移
int classDataOff = readInt(dexData, base + 24); // 类字段/方法数据偏移
int staticValuesOff = readInt(dexData, base + 28);// 静态变量初始值偏移
// 获取类名、父类名和源文件名字符串
String className = getTypeString(dexData, classIdx);
String superClassName = getTypeString(dexData, superClassIdx);
String sourceFile = getStringById(dexData, sourceFileIdx);
System.out.printf("class[%d]: %s extends %s from [%s], class_data_off=0x%xn",
i, className, superClassName, sourceFile, classDataOff);
}
}
// ------------------ 工具方法部分 ------------------
// 从指定偏移读取 4 字节,按小端格式转换为 int
public static int readInt(byte[] data, int offset) {
return (data[offset] & 0xFF)
| ((data[offset + 1] & 0xFF) << 8)
| ((data[offset + 2] & 0xFF) << 16)
| ((data[offset + 3] & 0xFF) << 24);
}
// 根据 type_id 取出对应的类型字符串(如 Ljava/lang/String;)
public static String getTypeString(byte[] data, int typeId) {
if (typeId < 0) return "null";
int typeIdsOff = readInt(data, 0x40); // type_ids 起始偏移,0x40 位置
int descriptorIdx = readInt(data, typeIdsOff + typeId * 4); // 每个 type_id 对应一个 string_id
return getStringById(data, descriptorIdx);
}
// 根据 string_id 取出对应的字符串内容
public static String getStringById(byte[] data, int stringId) {
if (stringId < 0) return "null";
int stringIdsOff = readInt(data, 0x3C); // string_ids 起始偏移,0x3C 位置
int stringDataOff = readInt(data, stringIdsOff + stringId * 4); // 获取该字符串的偏移
int[] uleb = readUleb128(data, stringDataOff); // 前缀是 ULEB128 格式的字符串长度
int offset = stringDataOff + uleb[1]; // 字符串实际数据起始位置
return readString(data, offset);
}
// 从 offset 开始读取一个以 0 结尾的字符串(UTF-8 编码)
public static String readString(byte[] data, int offset) {
int end = offset;
while (end < data.length && data[end] != 0) end++;
return new String(data, offset, end - offset, StandardCharsets.UTF_8);
}
// 读取一个 ULEB128 编码整数,返回 [值, 所占字节数]
public static int[] readUleb128(byte[] data, int offset) {
int result = 0;
int count = 0;
int shift = 0;
int b;
do {
b = data[offset + count] & 0xFF;
result |= (b & 0x7F) << shift;
shift += 7;
count++;
} while ((b & 0x80) != 0);
return new int[]{result, count};
}
}
DefCLassData
重点是 classDataOff
这个字段,它包含了一个类的核心数据,在 Android 源码中定义为 DexClassData
,它不在 DexFile.h 中了,而是在 DexClass.h 中:
struct DexClassData {
DexClassDataHeader header;
DexField* staticFields;
DexField* instanceFields;
DexMethod* directMethods;
DexMethod* virtualMethods;
};
DexClassDataHeader
定义了类中字段和方法的数目,它也定义在 DexClass.h 中:
struct DexClassDataHeader {
u4 staticFieldsSize;
u4 instanceFieldsSize;
u4 directMethodsSize;
u4 virtualMethodsSize;
};
-
staticFieldsSize : 静态字段个数 -
instanceFieldsSize : 实例字段个数 -
directMethodsSize : 直接方法个数 -
virtualMethodsSize : 虚方法个数
public class DexClassDataParser {
public static void parseClassData(byte[] dexData, int classDataOff) {
// 解析 class_data_item(32 字节结构)
int staticFieldsSize = readUleb128(dexData, classDataOff)[0]; // 静态字段数量
int instanceFieldsSize = readUleb128(dexData, classDataOff + 1)[0]; // 实例字段数量
int directMethodsSize = readUleb128(dexData, classDataOff + 2)[0]; // 直接方法数量
int virtualMethodsSize = readUleb128(dexData, classDataOff + 3)[0]; // 虚拟方法数量
// 打印解析出来的字段数量信息
System.out.println("Static Fields: " + staticFieldsSize);
System.out.println("Instance Fields: " + instanceFieldsSize);
System.out.println("Direct Methods: " + directMethodsSize);
System.out.println("Virtual Methods: " + virtualMethodsSize);
// 静态字段解析
int currentOffset = classDataOff + 4;
for (int i = 0; i < staticFieldsSize; i++) {
int fieldId = readUleb128(dexData, currentOffset)[0]; // 获取字段 ID
String fieldName = getFieldName(dexData, fieldId);
System.out.println("Static Field [" + i + "]: " + fieldName);
currentOffset += 1; // 偏移量调整
}
// 实例字段解析
for (int i = 0; i < instanceFieldsSize; i++) {
int fieldId = readUleb128(dexData, currentOffset)[0]; // 获取字段 ID
String fieldName = getFieldName(dexData, fieldId);
System.out.println("Instance Field [" + i + "]: " + fieldName);
currentOffset += 1; // 偏移量调整
}
// 直接方法解析
for (int i = 0; i < directMethodsSize; i++) {
int methodId = readUleb128(dexData, currentOffset)[0]; // 获取方法 ID
String methodName = getMethodName(dexData, methodId);
System.out.println("Direct Method [" + i + "]: " + methodName);
currentOffset += 1; // 偏移量调整
}
// 虚拟方法解析
for (int i = 0; i < virtualMethodsSize; i++) {
int methodId = readUleb128(dexData, currentOffset)[0]; // 获取方法 ID
String methodName = getMethodName(dexData, methodId);
System.out.println("Virtual Method [" + i + "]: " + methodName);
currentOffset += 1; // 偏移量调整
}
}
// ------------------ 工具方法部分 ------------------
// 读取一个 ULEB128 编码整数,返回 [值, 所占字节数]
public static int[] readUleb128(byte[] data, int offset) {
int result = 0;
int count = 0;
int shift = 0;
int b;
do {
b = data[offset + count] & 0xFF;
result |= (b & 0x7F) << shift;
shift += 7;
count++;
} while ((b & 0x80) != 0);
return new int[]{result, count};
}
// 根据字段 ID 获取字段名称
public static String getFieldName(byte[] dexData, int fieldId) {
int fieldIdsOff = readInt(dexData, 0x80); // 获取 field_ids 的偏移
int fieldIdOffset = readInt(dexData, fieldIdsOff + fieldId * 4);
return getStringById(dexData, fieldIdOffset);
}
// 根据方法 ID 获取方法名称
public static String getMethodName(byte[] dexData, int methodId) {
int methodIdsOff = readInt(dexData, 0x84); // 获取 method_ids 的偏移
int methodIdOffset = readInt(dexData, methodIdsOff + methodId * 4);
return getStringById(dexData, methodIdOffset);
}
// 从指定偏移读取 4 字节,按小端格式转换为 int
public static int readInt(byte[] data, int offset) {
return (data[offset] & 0xFF)
| ((data[offset + 1] & 0xFF) << 8)
| ((data[offset + 2] & 0xFF) << 16)
| ((data[offset + 3] & 0xFF) << 24);
}
// 根据 string_id 获取字符串内容
public static String getStringById(byte[] data, int stringId) {
if (stringId < 0) return "null";
int stringIdsOff = readInt(data, 0x3C); // string_ids 起始偏移,0x3C 位置
int stringDataOff = readInt(data, stringIdsOff + stringId * 4); // 获取该字符串的偏移
return readString(data, stringDataOff);
}
// 从 offset 开始读取一个以 0 结尾的字符串(UTF-8 编码)
public static String readString(byte[] data, int offset) {
int end = offset;
while (end < data.length && data[end] != 0) end++;
return new String(data, offset, end - offset, StandardCharsets.UTF_8);
}
}
继续回到 DexClassData 中来。header
部分定义了各种字段和方法的个数,后面跟着的分别就是 静态字段 、实例字段 、直接方法 、虚方法 的具体数据了。字段用 DexField
表示,方法用 DexMethod
表示。
DexField
struct DexField {
u4 fieldIdx; /* index to a field_id_item */
u4 accessFlags;
};
-
fieldIdx : 指向 field_ids ,表示字段信息 -
accessFlags :访问标识符
DexMethod
struct DexMethod {
u4 methodIdx; /* index to a method_id_item */
u4 accessFlags;
u4 codeOff; /* file offset to a code_item */
46};
method_idx
是指向 method_ids 的索引,表示方法信息。accessFlags
是该方法的访问标识符。codeOff
是结构体 DexCode
的偏移量
DexCode
struct DexCode {
u2 registersSize; // 寄存器个数
u2 insSize; // 参数的个数
u2 outsSize; // 调用其他方法时使用的寄存器个数
u2 triesSize; // try/catch 语句个数
u4 debugInfoOff; // debug 信息的偏移量
u4 insnsSize; // 指令集的个数
u2 insns[1]; // 指令集
/* followed by optional u2 padding */ // 2 字节,用于对齐
/* followed by try_item[triesSize] */
/* followed by uleb128 handlersSize */
/* followed by catch_handler_item[handlersSize] */
};
public class DexCodeParser {
public static void parseDexCode(byte[] dexData, int codeOffset) {
// 解析寄存器数量
int registersSize = readShort(dexData, codeOffset);
// 解析输入指令数量
int insSize = readShort(dexData, codeOffset + 2);
// 解析输出指令数量
int outsSize = readShort(dexData, codeOffset + 4);
// 解析异常处理块数量
int triesSize = readShort(dexData, codeOffset + 6);
// 解析调试信息的偏移
int debugInfoOffset = readInt(dexData, codeOffset + 8);
// 解析指令集大小
int insnsSize = readInt(dexData, codeOffset + 12);
// 解析字节码指令
byte[] insns = new byte[insnsSize];
System.arraycopy(dexData, codeOffset + 16, insns, 0, insnsSize);
// 打印解析的字节码信息
System.out.println("Registers Size: " + registersSize);
System.out.println("Ins Size: " + insSize);
System.out.println("Outs Size: " + outsSize);
System.out.println("Tries Size: " + triesSize);
System.out.println("Debug Info Offset: " + debugInfoOffset);
System.out.println("Insns Size: " + insnsSize);
// 解析字节码指令
for (int i = 0; i < insnsSize; i++) {
System.out.printf("0x%02X ", insns[i]);
if ((i + 1) % 16 == 0) {
System.out.println();
}
}
System.out.println();
}
// 读取 2 字节的小端数据
public static int readShort(byte[] data, int offset) {
return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8);
}
// 读取 4 字节的小端数据
public static int readInt(byte[] data, int offset) {
return (data[offset] & 0xFF)
| ((data[offset + 1] & 0xFF) << 8)
| ((data[offset + 2] & 0xFF) << 16)
| ((data[offset + 3] & 0xFF) << 24);
}
}
原文始发于微信公众号(土拨鼠的安全屋):Android系列基础学习小计-1
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论