0. python 获得 android 屏幕图片
以前使用vysor获得视频流,来获得android屏幕图片,但是经常会断流,而且手机端一直运行 vysor 服务器也会导致手机发烫严重。
使用python airtest 库可以比较流畅的实时获得 android 屏幕,基本能达到每秒 5 帧。
from airtest.core.api import *
from airtest.core.android.minicap import *
import cv2
import numpy as np
device = connect_device("Android://127.0.0.1:5037/cd9faa7f")
cap = Minicap(device.adb, [1080, 1920]) # 分辨率可以适当修改,提高速度
index = 0
while True:
image_bytes = cap.get_frame_from_stream()
img = cv2.imdecode(np.frombuffer(image_bytes, np.uint8), cv2.IMREAD_COLOR)
img = cv2.resize(img, None, None, 0.5, 0.5)
cv2.imshow("window", img)
cv2.waitKey(1)
print(index)
index += 1
在liunx上 需要手动给 adb 文件添加可执行权限
库安装命令
pip install -U airtest
1.native的char指针和JAVA的String相互转换
首先确保(C/C++)源文件的字符编码是UTF-8与JAVA的class文件字符编码保持一致。如果(C/C++)源码含有中文,那么编译出来的so中的中文字符串也保存为UTF-8编码,这样的程序不会产生乱码。 JNI提供了jstring来引用JAVA的String类型变量,如果native函数需要返回 String或者接受String类型参数就必须使用到jstring。而(C/C++)用char引用字符串起始地址,当native函数接到jstring后要转换为char所指向的字符串才能处理。当我们处理完char*所指向的字符串又要转换为jstring才能返回给JAVA代码。下面给出转换的方法(下面均是 C++ 代码)。
jstring转换为char使用JNIEnv的const char GetStringUTFChars(JNIEnv*, jstring, jboolean*)
JNIEnv env=//传入参数 ;
jstring name=//传入参数 ;
const char *nameStr=env->GetStringUTFChars(env,name,NULL);
//调用完GetStringUTFChars后必须调用JNIEnv的void ReleaseStringUTFChars(JNIEnv*, jstring, const char*)释放新建的字符串。
env-> ReleaseStringUTFChars(env,name, nameStr);
char转换为jstring使用JNIEnv的jstring NewStringUTF(JNIEnv, const char*);
jstring newArgName=env->NewStringUTF(env, nameStr);
//调用完NewStringUTF后必须调用JNIEnv的void DeleteLocalRef(JNIEnv*, jobject);释放新建的jstring。
env-> DeleteLocalRef(env, newArgName);
2. c代码中打log
在cmake中依赖log库
find_library(log-lib log)
target_link_libraries(native-lib ${log-lib})
在c/c++代码中添加
#include <android/log.h>
#define 是C的宏定义 可以给 字符串 和函数起别名
#define LOG_TAG "System.out"
//给__android_log_print函数起别名第一参数 有限及 第二个参数打log的TAG
//LOGD 就是打印优先级是DEBUG的LOG 用法跟printf一样
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
3. c++ 调用java层代码
c对java函数的调用 本质:是通过java反射调用java函数 实现步骤:
1.通过类的路径获得字节码文件 (注意:获取的是反射方法所在的类的字节码) jclass (FindClass)(JNIEnv, const char*);
2.获取函数的对象 (注意:此时获取的是需反射方法的函数对象) jmethodID (FromReflectedMethod)(JNIEnv, jobject);
3.获得对象,仅本地方法与反射方法不再一个类里的时候调用 注意:获取的是反射方法所在的类的对象,本地方法的类已自动传入,即参数中的jobject) //jmethodID (GetMethodID)(JNIEnv, jclass, const char*, const char*); //第3个参数为方法名,第4个参数为方法签名;方法签名对象是具体的反射函数,而不是本地函数
4.处理c文件中自定义的数据为java类型 注意:在c文件中创建的变量,都是默认为c格式的;如果反射的java函数需要用到这些变量,就需要将其转变为java格式 例如:c中的char* 表示字符串,java函数需要用到char*的字符串时,需要将其转变为jstring类型。
例子
反射类部分
public class Jui {
static{
System.loadLibrary("getFlection");
}
//注意:如果需要在反射中调用到需要context的android函数,
//此时需要将context作为参数传给c文件反射的java类
public Context context;
public Jui(Context context){
this.context = context;
}
//定义本地方法
//注意:对于本身就是对象的参数(例如:string,数组 等),不需要返回操作后的变量,因为操作时就是操作该参数本身。
//对于int等类型的数据,如果需要返回,可以定义返回语句。
public native int callOperaInt(int i);
public native void callOperaString(String str);
public native void callOperaArray(int [] array);
//定义被本地方法反射的java方法
//注意:本地方法与反射方法通常在一个类中,这样可以避免在反射时new新对象。
public int operaInt(int i){
operaToast(i+":"+(i+5));
return i+5;
}
public void operaString(String str){
byte[] bytes = str.getBytes();
for (int i=0;i<bytes.length;i++){
bytes[i] += 1;
}
operaToast(str+":"+(new String(bytes)));
}
public void operaArray(int [] array){
StringBuffer stringBuffer = new StringBuffer();
for (int i=0;i<array.length;i++){
array[i] += 1;
stringBuffer.append(array[i]);
}
operaToast(stringBuffer.toString());
}
public void operaToast(String str){
Toast.makeText(context, str, Toast.LENGTH_SHORT).show();
}
}
C文件部分
JNIEXPORT jint JNICALL Java_com_day02_reflection_Jui_callOperaInt(JNIEnv * env, jobject obj, jint i){
//1.获取类的字节码文件
//jclass FindClass(const char* name)
jclass clazz = (*env)->FindClass(env,"com/day02/reflection/Jui");
//2.获取到方法对象
//jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
//第3个参数为方法名,第4个参数为方法签名
jmethodID methodID = (*env)->GetMethodID(env, clazz, "operaInt","(I)I" );
//3.通过字节码对象获取到类的对象
//jobject (*AllocObject)(JNIEnv*, jclass);
//4.调用方法
//(*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
return (*env)->CallIntMethod(env,obj,methodID,i);
};
JNIEXPORT void JNICALL Java_com_day02_reflection_Jui_callOperaString(JNIEnv * env, jobject obj, jstring str){
jclass clazz = (*env)->FindClass(env,"com/day02/reflection/Jui");
jmethodID methodID = (*env)->GetMethodID(env, clazz, "operaString","(Ljava/lang/String;)V" );
//注意:在c中定义的字符串都默认为c类型的char字符数组,反射调用java方法时,应传递jstring类型的java字符串
//此时,需要将c的字符串转为jstring类型
jstring jstr = (*env)->NewStringUTF(env,"hello");
(*env)->CallVoidMethod(env,obj,methodID,jstr);
};
/*
* Class: com_day02_reflection_Jui
* Method: callOperaArray
* Signature: ([I)V
*/
JNIEXPORT void JNICALL Java_com_day02_reflection_Jui_callOperaArray(JNIEnv * env, jobject obj, jintArray jarray){
jclass clazz = (*env)->FindClass(env,"com/day02/reflection/Jui");
jmethodID methodID = (*env)->GetMethodID(env,clazz,"operaArray","([I)V");
(*env)->CallVoidMethod(env,obj,methodID,jarray);
};
函数与属性签名
在GetMethodID( )和GetFieldID( )这两个函数中,最后一个参数都是签名字符串,用来标识java函数和成员的唯一性。
由于Java中存在重载函数,所以一个函数名不足以唯一指定一个函数,这时候就需要签名字符串来指定函数的参数列表和返回值类型了。
函数签名是一个字符串:”(M)N”
括号中的内容是函数的参数类型,括号后面表示函数的返回值。
例如:
(I)V 带一个int 类型的参数,返回值类型为void
( )D 没有参数,返回double
JNI 类型签名
“(M)N”,这里的M和N指的是该函数的输入和输出参数的类型签名(Type Signature)。
具体的每一个字符的对应关系如下:
字符 | Java类型 | C类型 |
---|---|---|
V | void | void |
Z | jboolean | boolean |
I | jint | int |
J | jlong | long |
D | jdouble | double |
F | jfloat | float |
B | jbyte | byte |
C | jchar | char |
S | jshort | short |
数组则以”[“开始,用两个字符表示
数组 | 本地类型 | java类型 |
---|---|---|
[I | jintArray | int[] |
[F | jfloatArray | float[] |
[B | jbyteArray | byte[] |
[C | jcharArray | char[] |
[S | jshortArray | short[] |
[D | jdoubleArray | double[] |
[J | jlongArray | long[] |
[Z | jbooleanArray | boolean[] |
如果Java函数的参数是class,则以”L”开头,以”;”结尾,中间是用”/” 隔开的包及类名。而其对应的C函数名的参数则为jobject
举个例子:
String类,其对应的类为jstring,其形式为Ljava/lang/String; String jstring
Socket类的形式为Ljava/net/Socket; Socket jobject
如果Java函数位于一个嵌入类中,则可用$作为类名间的分隔符。
例如:(Ljava/lang/String;Landroid/os/FileUtils$FileStatus;)Z
4. c++运行库介绍
默认情况下,Android NDK会使用一个非常迷你的C运行环境,称做“libstdc”。这个运行环境几乎什么都没有,不支持异常和RTTI(RunTime Type Information,即运行时类型识别),甚至连C++标准库也没有。
不过除了这个缺省的C++ 运行环境之外,Android NDK还带了一些其它的C++ 运行环境,它们各有各的特长,在使用的时候也经常分不清到底最好要使用哪个。本文下面的部分, 将对它们各自的特点做一个详细的说明。
但在正式介绍之前,先说几个基本概念。
首先是所谓的C++ 标准库。C++ 标准除了定义了语法之外,还包含了一个包罗万象的标准库,方便程序的开发者调用,从而使用简单代码就可以实现复杂的功能。最简单的,比如string就是属于C++ 标准库的,C++ 自己的语法中并没有string关键字,只有char。这个标准库非常庞大,包含的功能如下图所示:
接着,是所谓的C++ 运行时类型信息(RTTI)。它有点类似Java语言中反射的概念,就是可以在程序运行时,可以动态的判断某一个对象所属的类是什么。
然后是C++ 异常机制,这个简单,就是try和catch嘛。不过要想实现C++ 的异常捕获机制,需要C++ 编译器和运行时库一起配合才行,两者缺一不可。运行时库支持,但是C++ 编译器不支持也不行,反之亦然。
最后是所谓的动态库和静态库的概念。所谓动态库是程序运行时动态加载进进程内的,它的好处是如果程序中有很多独立的模块都需要使用同样的一个动态库,那么只需要在内存中加载一次就可以了。而静态库,是在模块编译的时候,在链接的过程中,将程序库中所需要的代码“拷贝”到模块内部实现的,它的坏处就是同样的代码会在各个模块中都存在,浪费内存和磁盘存储空间,甚至不同模块间的库函数代码会相互干扰,出现诡异的行为。
除去默认的那个C++ 运行时库,Android NDK中还带了其它的四个不同的C运行时库,它们所支持的C 特性有可能都不一样,而且每个运行时库都分有动态库和静态库。下面这张表,总结了一下所有C++ 运行时库的特点:
名字 | 支持C++的特性 | 文件名 | 动态或静态 |
---|---|---|---|
libstdc++ | 无 | libstdc++.so | 动态 |
gabi++_static | 异常和RTTI | libgabi++static.a | 静态 |
gabi++_shared | 异常和RTTI | libgabi++shared.so | 动态 |
stlport_static | 异常、RTTI和标准库 | libstlport++static.a | 静态 |
stlport_shared | 异常、RTTI和标准库 | libstlport++shared.so | 动态 |
gnustl_static | 异常、RTTI和标准库 | libgnustl++static.a | 静态 |
gnustl_shared | 异常、RTTI和标准库 | libgnustl++shared.so | 动态 |
c++_static | 异常、RTTI和标准库 | libc++static.a | 静态 |
c++_shared | 异常、RTTI和标准库 | libc++shared.so | 动态 |
下面简单介绍一下各个运行时库的特点:
1)libstdc++(默认运行时库)
前面也提到了,这个系统默认的C++ 运行时库功能非常的弱,基本上都不支持所有的C++ 高级特性。
不过,在真实的设备上,只自带了缺省的C++ 运行时库(一般该文件位于/system/lib/libstdc++.so),在Android NDK中只包含了这个运行时库所需要的一些头文件,并没有真实的静态库和动态库。所以,也就是说,Android系统中默认的那个C++运行时库是没有静态库版本的,而且由于系统自带了,所以在你的程序中也不需要再包含这个库的.so文件了,可以减小安装文件的大小。
2)gabi++
这个库也不支持C++标准库,但它加入了异常和RTTI的支持。
在Android NDK中,包含有这个库的静态和动态两个预先编译好的版本,位于<NDK Folder>/sources/cxx-stl/gabi++/目录下。
3)stlport
这个库提供了对C++所有特性的完整支持。它是开源项目STLPort(http://www.stlport.org)的一个Android移植版本。
在Android NDK中,也包含有这个库的静态和动态两个版本,位于<NDK Folder>/sources/cxx-stl/stlport/目录下。
同时,Android NDK中还包含了这个C++运行时库的所有源码,你可以在Application.mk文件中,加上下面的赋值语句来强制Android NDK通过源码编译这个库,而不要用预编译的版本:
STLPORT_FORCE_REBUILD := true
4)gnustl
这就是GNU标准C++ 运行时库(libstdc++-v3)。同样,它支持C++ 的所有特性,且在Android NDK中包括了静态和动态两个预编译的版本,位于 <NDK Folder>/sources/cxx-stl/gnu-libstdc++ /目录下。
5)llvm-libc++
最后一个就是llvm-libc++,它是LLVM libc++(http://libcxx.llvm.org/)的一个Android移植版本。也支持C++ 的所有特性,且也有静态和动态的两个预编译的版本,位于<NDK Folder>/sources/cxx-stl/llvm-libc++/目录下。
这两个预编译的版本都是使用Clang 3.4编译的。不过,和stlport一样,在Android NDK中也带有它的实现源码,可以在Application.mk文件中,加上下面的赋值语句来强制Android NDK通过源码编译这个库,而不要用预编译的版本:
LIBCXX_FORCE_REBUILD := true
那么多个C++ 运行时库,程序怎么知道到底用哪个呢?刚才说了,默认情况下,也就是不做任何特殊设置的时候,是使用libstdc++。如果想用别的C++ 运行时库,需要在你程序中的Application.mk文件中,对变量APP_STL进行赋值,想用哪个C++ 运行时库,就直接将运行时库的名字赋值给这个变量,而运行时库的名字就是上表第一列中的字符串。例如,如果想换用共享的gnustl库的话,需要在Application.mk文件中,加上下面的语句:
APP_STL := gnustl_static
还要特别说明的是,默认情况下,Android NDK的编译器的选项是不包含对C++ 异常和RTTI的支持的。刚才也提到过了,对异常和RTTI的支持,必须是编译器和C++ 运行时库相互配合才能完成的。如果编译器本身不支持,即使运行时库支持也没用。
所以,如果想让你的程序支持异常和RTTI,必须先让编译器支持,且选择合适的运行时库才行。
那么怎么让编译器支持异常和RTTI呢?如果你是想让你程序中的所有模块都支持,只需要在Application.mk文件中,对APP_CXXFLAGS变量赋值即可。例如,如果想全部程序加入对异常的支持,可以这样赋值:
APP_CPPFLAGS += -fexceptions
cmake gradle配置
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
arguments '-DANDROID_TOOLCHAIN=clang', '-DANDROID_STL=c++_shared'
cppFlags "-std=c++11 -frtti -fexceptions"
}
}
ndk {
abiFilters 'armeabi','armeabi-v7a'
}
}
sourceSets{
jni.assets{"../jniLib"}
}
...
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
5.一些源码位置
opengl native源码在/framework/native/opengl/文件夹下