概述
在很多场景下,需要进行分析字节数据,但是我们存起来的字节数据一般都是二进制的,这时候就需要我们将其转成16进制的方式方便分析。比如在做音视频的时候,需要看下我们传输的视频h264数据中是否有对应的I帧或者B帧等数据,做ASM插桩的时候,可以使用输出类结构的16进制辅助分析了解问题。测试投屏的时候尤其有用,比如说投屏到电视上后,发现没有画面,或者是画面很卡顿,这时候就需要对我们传输的视频数据做分析,所以我们将视频的数据转成16进制的形式,并且以一定的格式输出,可以很方便的帮助我们定位问题。本文主要介绍如何使用Java将字节数组格式化成16进制的格式并输出。
输出效果展示
上图是以一个class字节码文件的16进制的格式输出,下面就介绍如何将我们的字节数组输出成16进制的格式
代码实现
首先我们定义一个类,用于生成一个class文件,作为我们格式化的对象。读者使用的时候可以是其他数据,只要是字节数组的方式提供就行了,这里仅仅作为演示
public class ASMDemoEntity {
private int intNum = 10;
private static final String staticString = "hello world";
public void fun() {
System.out.println("I am fun");
}
public int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
System.out.println("a+b = " + new ASMDemoEntity().add(1, 2));
new ASMDemoEntity().fun();
}
}
然后定义一个枚举类,定义我们格式化后的16进制数据的输出样式以及分隔符,如下所示:
public enum HexFormat {
// 无分隔符分别展示0,8,16,32列
FORMAT_HEX_0("", 0),
FORMAT_HEX_8("", 8),
FORMAT_HEX_16("", 16),
FORMAT_HEX_32("", 32),
// 带空格分隔符分别展示0,8,16,32列
FORMAT_HEX_SPACE__0(" ", 0),
FORMAT_HEX_SPACE_8(" ", 8),
FORMAT_HEX_SPACE_16(" ", 16),
FORMAT_HEX_SPACE_32(" ", 32);
public final String separator; // 分隔符
public final int column; // 展示几列
HexFormat(String separator, int column) {
this.separator = separator;
this.column = column;
}
}
如上所示:FORMAT_HEX_0就表示展示0列,无分隔符,用一行展示完所有的16进制数据,而FORMAT_HEX_SPACE_32 表示以一个空格做分隔符,展示32列,就如我们本文展示的效果图一样。
接着我们使用一个FileUtil类去读我们生成的.class文件:
public class FileUtil {
public static String getFilePath(String relativePath){
URL resource = FileUtil.class.getResource("/");
String dir = resource == null? "" : resource.getPath();
return dir + relativePath;
}
public static byte[] readBytes(String filePath){
File file = new File(filePath);
if(!file.exists()){
throw new IllegalArgumentException(filePath + "not exist");
}
InputStream in = null;
try {
in = Files.newInputStream(file.toPath());
in = new BufferedInputStream(in);
ByteArrayOutputStream bao = new ByteArrayOutputStream();
IOUtil.copy(in,bao);
return bao.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}finally {
IOUtil.closeIO(in);
}
return null;
}
}
使用一个IOUtil类做复制字节数组和关闭IO流
public class IOUtil {
private static final int EOF = -1;
private static final int BUFFER_SIZE = 1024 * 4;
public static long copy(final InputStream input,
final OutputStream output) throws IOException {
long count = 0;
int n;
byte[] buffer = new byte[BUFFER_SIZE];
while (EOF != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
public static void closeIO(final Closeable closeable) {
if(closeable != null){
try {
closeable.close();
} catch (IOException ignored) {
}
}
}
}
最后使用格式化工具类将字节数组格式化成16进制的样式并按照指定的格式输出
public class HexUtil {
public static String hexFormat(byte[] bytes,HexFormat format){
String separator = format.separator;
int column = format.column;
return hexFormat(bytes,separator,column);
}
private static String hexFormat(byte[] bytes, String separator, int column) {
if(bytes == null || bytes.length < 1) {
return "";
}
StringBuilder sb = new StringBuilder();
Formatter fm = new Formatter(sb);
int length = bytes.length;
for (int i = 0; i < length; i++) {
int val = bytes[i] & 0xFF;
fm.format("%02X",val);
if(column > 0 && (i+1) % column == 0){
fm.format("%n");
}else{
fm.format("%s",separator);
}
}
return sb.toString();
}
}
在上面代码中的代码是Formatter.format()方法,它的作用是格式化我们的字节数组,我们传入的格式中带有%…X…时表示输出16进制数据,具体的定义如下:
%X: 正常输出16进制数
%NX: 十六进制数,输出N位,如果本身大于N位,正常输出,比如format("%2X",val);
表示输出2位16进制数,若本身大于2位,正常输出
%NBX: 十六进制数,输出N位,不足N位就补B,若本身大于N位,就正常输出,比如format("%02X",val);
代表输出2位的16进制数,如果不足2位就补0,如果本身大于2位,就正常输出
演示将一个class文件的二进制数据转成16进制数据并格式化后输出:
public class HexFormatMain {
public static void main(String[] args) {
String relativePath = "org/example/entity/ASMDemoEntity.class";
String filePath = FileUtil.getFilePath(relativePath);
System.out.println("file path: " + filePath);
byte[] bytes = FileUtil.readBytes(filePath);
String hex = HexUtil.hexFormat(bytes, HexFormat.FORMAT_HEX_SPACE_32);
System.out.println("class文件的16进制: ");
System.out.println(hex);
}
}