Android-音视频学习系列(一)-JNI-从入门到精通,android开发入门
/**
- 1. 加载 native 库
*/
static {
System.loadLibrary(“native-lib”);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.sample_text);
/*3.调用 native c++ 函数/
tv.setText(stringFromJNI());
}
/**
- 2. 定义 native 函数
*/
public native String stringFromJNI();
}
Native-lib.cpp 代码:
#include <jni.h>
#include
extern “C” JNIEXPORT jstring JNICALL
Java_com_devyk_ndk_1sample_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = “Hello from C++”;
return env->NewStringUTF(hello.c_str());
}
运行之后屏幕就会出现 “Hello from C++” 字符串,一个最简单的 native 项目就创建完成了。
JNI 入门学习
1. 数据类型和类型描述符
Java 中有两种数据类型:
- 基本数据类型: boolean 、char、byte、int、short、long、float、double。
- 引用数据类型: String、Object[]、Class、Object 及其它类。
1.1 基本数据类型
基本数据类型可以直接与 C/C++ 的相应基本数据类型映射,如下表所示。JNI 用类型定义使得这种映射对开发人员透明。
Java 类型 | JNI 类型 | C/C++ 类型 |
---|---|---|
boolean | jboolean | unsigned char (无符号 8 位整型) |
byte | jbyte | char (有符号 8 位整型) |
char | jchar | unsingned short (无符号 16 位整型) |
short | jshort | short (有符号 16 位整型) |
int | jint | int (有符号 32 位整型) |
long | jlong | long (有符号 64 位整型) |
float | jfloat | float (有符号 32 位浮点型) |
double | jdouble | double (有符号 64 位双精度型) |
1.2 引用类型:
与基本数据类型不同,引用类型对原生方法时不透明的,引用类型映射如下表所示。它们的内部数据结构并不直接向原生代码公开。
Java 类型 | 原生类型 |
---|---|
Java.lang.Class | jclass |
Java.lang.Throwable | jthrowable |
Java.lang.String | jstring |
Other object | jobject |
Java.lang.Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
Other arrays | jarray |
1.3 数据类型描述符
在 JVM 虚拟机中,存储数据类型的名称时,是使用指定的描述符来存储,而不是我们习惯的 int,float 等。
Java 类型 | 签名 (描述符) |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
其它引用类型 | L + 全类名 + ; |
type[] | [ |
method type | (参数)返回值 |
示例:
- 表示一个 String
Java 类型 : java.lang.String
JNI 描述符: Ljava/lang/String; (L + 类全名 + ;)
- 表示一个数组
Java 类型: String[] JNI 描述符: [Ljava/lang/String; Java 类型: int [] [] JNI 描述符: [[I
- 表示一个方法
Java 方法: long func(int n, String s, int[] arr); JNI 描述符: (ILjava/lang/String;[I)J
Java 方法: void func(); JNI 描述符: ()V
也可以使用命令 : javap -s 全路径 来获取方法签名
2. JNIEnv 和 JavaVm 介绍
2.1 JNIEnv :
JNIEnv 表示 Java 调用 native 语言的环境,是一个封装了几乎全部 JNI 方法的指针。
JNIEnv 只在创建它的线程生效,不能跨线程传递,不同线程的 JNIEnv 彼此独立。
native 环境中创建的线程,如果需要访问 JNI,必须要调用 AttachCurrentThread 关联,并使用 DetachCurrentThread 解除链接。
2.2 JavaVm :
JavaVM 是虚拟机在 JNI 层的代表,一个进程只有一个 JavaVM,所有的线程共用一个 JavaVM。
2.3 代码风格 (C/C++)
C: (*env)->NewStringUTF(env, “Hellow World!”);
C++: env->NewStringUTF(“Hellow World!”);
3. JNI API
参考官方 API 文档 或者 JNI 方法大全及使用示例
4. 对数据类型的操作
JNI 处理 Java 传递过来的数据
- 定义 native 函数
public class MainActivity extends AppCompatActivity {
/**
- 1. 加载 native 库
*/
static {
System.loadLibrary(“native-lib”);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/** 1. Java 数据传递给 native */
test1(true,
(byte) 1,
‘,’,
(short) 3,
4,
3.3f,
2.2d,
“DevYK”,
28,
new int[]{1, 2, 3, 4, 5, 6, 7},
new String[]{“1”, “2”, “4”},
new Person(“阳坤”),
new boolean[]{false, true}
);
}
/**
- Java 将数据传递到 native 中
*/
public native void test1(
boolean b,
byte b1,
char c,
short s,
long l,
float f,
double d,
String name,
int age,
int[] i,
String[] strs,
Person person,
boolean[] bArray
);
}
- jni 处理 Java 传递过来的数据
#include <jni.h>
#include
#include <android/log.h>
#include
#define TAG “native-lib”
// VA_ARGS 代表 …的可变参数
#define LOGD(…) __android_log_print(ANDROID_LOG_DEBUG, TAG, VA_ARGS);
#define LOGE(…) __android_log_print(ANDROID_LOG_ERROR, TAG, VA_ARGS);
#define LOGI(…) __android_log_print(ANDROID_LOG_INFO, TAG, VA_ARGS);
extern “C”//支持 C 语言代码
JNIEXPORT void JNICALL
Java_com_devyk_ndk_1sample_MainActivity_test1(JNIEnv *env, jobject instance,
jboolean jboolean1,
jbyte jbyte1,
jchar jchar1,
jshort jshort1,
jlong jlong1,
jfloat jfloat1,
jdouble jdouble1,
jstring name_,
jint age,
jintArray i_,
jobjectArray strs,
jobject person,
jbooleanArray bArray_
) {
//1. 接收 Java 传递过来的 boolean 值
unsigned char b_boolean = jboolean1;
LOGD(“boolean-> %d”, b_boolean);
//2. 接收 Java 传递过来的 boolean 值
char c_byte = jbyte1;
LOGD(“jbyte-> %d”, c_byte);
//3. 接收 Java 传递过来的 char 值
unsigned short c_char = jchar1;
LOGD(“char-> %d”, c_char);
//4. 接收 Java 传递过来的 short 值
short s_short = jshort1;
LOGD(“short-> %d”, s_short);
//5. 接收 Java 传递过来的 long 值
long l_long = jlong1;
LOGD(“long-> %d”, l_long);
//6. 接收 Java 传递过来的 float 值
float f_float = jfloat1;
LOGD(“float-> %f”, f_float);
//7. 接收 Java 传递过来的 double 值
double d_double = jdouble1;
LOGD(“double-> %f”, d_double);
//8. 接收 Java 传递过来的 String 值
const char *name_string = env->GetStringUTFChars(name_, 0);
LOGD(“string-> %s”, name_string);
//9. 接收 Java 传递过来的 int 值
int age_java = age;
LOGD(“int:%d”, age_java);
//10. 打印 Java 传递过来的 int []
jint *intArray = env->GetIntArrayElements(i_, NULL);
//拿到数组长度
jsize intArraySize = env->GetArrayLength(i_);
for (int i = 0; i < intArraySize; ++i) {
LOGD(“intArray->%d:”, intArray[i]);
}
//释放数组
env->ReleaseIntArrayElements(i_, intArray, 0);
//11. 打印 Java 传递过来的 String[]
jsize stringArrayLength = env->GetArrayLength(strs);
for (int i = 0; i < stringArrayLength; ++i) {
jobject jobject1 = env->GetObjectArrayElement(strs, i);
//强转 JNI String
jstring stringArrayData = static_cast(jobject1);
//转 C String
const char *itemStr = env->GetStringUTFChars(stringArrayData, NULL);
LOGD(“String[%d]: %s”, i, itemStr);
//回收 String[]
env->ReleaseStringUTFChars(stringArrayData, itemStr);
}
//12. 打印 Java 传递过来的 Object 对象
//12.1 获取字节码
const char *person_class_str = “com/devyk/ndk_sample/Person”;
//12.2 转 jni jclass
jclass person_class = env->FindClass(person_class_str);
//12.3 拿到方法签名 javap -a
const char *sig = “()Ljava/lang/String;”;
jmethodID jmethodID1 = env->GetMethodID(person_class, “getName”, sig);
jobject obj_string = env->CallObjectMethod(person, jmethodID1);
jstring perStr = static_cast(obj_string);
const char *itemStr2 = env->GetStringUTFChars(perStr, NULL);
LOGD(“Person: %s”, itemStr2);
env->DeleteLocalRef(person_class); // 回收
env->DeleteLocalRef(person); // 回收
//13. 打印 Java 传递过来的 booleanArray
jsize booArrayLength = env->GetArrayLength(bArray_);
jboolean *bArray = env->GetBooleanArrayElements(bArray_, NULL);
for (int i = 0; i < booArrayLength; ++i) {
bool b = bArray[i];
jboolean b2 = bArray[i];
LOGD(“boolean:%d”,b)
LOGD(“jboolean:%d”,b2)
}
//回收
env->ReleaseBooleanArrayElements(bArray_, bArray, 0);
}
输出:
输出:
native-lib: boolean-> 1
native-lib: jbyte-> 1
native-lib: char-> 44
native-lib: short-> 3
native-lib: long-> 4
native-lib: float-> 3.300000
native-lib: double-> 2.200000
native-lib: string-> DevYK
native-lib: int:28
native-lib: intArray->1:
native-lib: intArray->2:
native-lib: intArray->3:
native-lib: intArray->4:
native-lib: intArray->5:
native-lib: intArray->6:
native-lib: intArray->7:
native-lib: String[0]: 1
native-lib: String[1]: 2
native-lib: String[2]: 4
native-lib: Person: 阳坤
native-lib: boolean:0
native-lib: jboolean:0
native-lib: boolean:1
native-lib: jboolean:1
JNI 处理 Java 对象
- 定义一个 Java 对象
public class Person {
private String name;
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
@Override
public String toString() {
return “Person{” +
“name=’” + name + ‘’’ +
“, age=” + age +
‘}’;
}
}
- 定义 native 接口
public class MainActivity extends AppCompatActivity {
private String TAG = this.getClass().getSimpleName();
/**
- 1. 加载 native 库
*/
static {
System.loadLibrary(“native-lib”);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView text = findViewById(R.id.sample_text);
/*处理 Java 对象/
String str = getPerson().toString();
text.setText(str);
}
public native Person getPerson();
}
根据上面代码我们知道,如果获取成功,手机屏幕上肯定会打印会显示数据。
- JNI 的处理
extern “C”
JNIEXPORT jobject JNICALL
Java_com_devyk_ndk_1sample_MainActivity_getPerson(JNIEnv *env, jobject instance) {
//1. 拿到 Java 类的全路径
const char *person_java = “com/devyk/ndk_sample/Person”;
const char *method = “”; // Java构造方法的标识
//2. 找到需要处理的 Java 对象 class
jclass j_person_class = env->FindClass(person_java);
//3. 拿到空参构造方法
jmethodID person_constructor = env->GetMethodID(j_person_class, method, “()V”);
//4. 创建对象
jobject person_obj = env->NewObject(j_person_class, person_constructor);
//5. 拿到 setName 方法的签名,并拿到对应的 setName 方法
const char *nameSig = “(Ljava/lang/String;)V”;
jmethodID nameMethodId = env->GetMethodID(j_person_class, “setName”, nameSig);
//6. 拿到 setAge 方法的签名,并拿到 setAge 方法
const char *ageSig = “(I)V”;
jmethodID ageMethodId = env->GetMethodID(j_person_class, “setAge”, ageSig);
//7. 正在调用 Java 对象函数
const char *name = “DevYK”;
jstring newStringName = env->NewStringUTF(name);
env->CallVoidMethod(person_obj, nameMethodId, newStringName);
env->CallVoidMethod(person_obj, ageMethodId, 28);
const char *sig = “()Ljava/lang/String;”;
jmethodID jtoString = env->GetMethodID(j_person_class, “toString”, sig);
jobject obj_string = env->CallObjectMethod(person_obj, jtoString);
jstring perStr = static_cast(obj_string);
const char *itemStr2 = env->GetStringUTFChars(perStr, NULL);
LOGD(“Person: %s”, itemStr2);
return person_obj;
}
输出:
可以看到 native 返回数据给 Java 了。
5. JNI 动态注册
前面咱们学习的都是静态注册,静态注册虽然简单方便,但是也面临一个较大的问题,如果当前类定义的 native 方法名称改变或者包名改变,那么这一改也就面临在 cpp 中实现的也将改动,如果将要面临这种情况你可以试试 JNI 动态注册,如下代码所示:
public class MainActivity extends AppCompatActivity {
private String TAG = this.getClass().getSimpleName();
/**
- 1. 加载 native 库
*/
static {
System.loadLibrary(“native-lib”);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView text = findViewById(R.id.sample_text);
/**动态注册的 native */
dynamicRegister(“我是动态注册的”);
}
/**
- 动态注册
*/
public native void dynamicRegister(String name);
}
复制代码
cpp:
#include <jni.h>
#include
#include <android/log.h>
#include
#define TAG “native-lib”
// VA_ARGS 代表 …的可变参数
#define LOGD(…) __android_log_print(ANDROID_LOG_DEBUG, TAG, VA_ARGS);
#define LOGE(…) __android_log_print(ANDROID_LOG_ERROR, TAG, VA_ARGS);
#define LOGI(…) __android_log_print(ANDROID_LOG_INFO, TAG, VA_ARGS);
/**
- TODO 动态注册
*/
/**
- 对应java类的全路径名,.用/代替
*/
const char *classPathName = “com/devyk/ndk_sample/MainActivity”;
extern “C” //支持 C 语言
JNIEXPORT void JNICALL //告诉虚拟机,这是jni函数
native_dynamicRegister(JNIEnv *env, jobject instance, jstring name) {
const char *j_name = env->GetStringUTFChars(name, NULL);
LOGD(“动态注册: %s”, j_name)
//释放
env->ReleaseStringUTFChars(name, j_name);
}
/* 源码结构体
- typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
*/
static const JNINativeMethod jniNativeMethod[] = {
{“dynamicRegister”, “(Ljava/lang/String;)V”, (void *) (native_dynamicRegister)}
};
/**
- 该函数定义在jni.h头文件中,System.loadLibrary()时会调用JNI_OnLoad()函数
*/
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *javaVm, void *pVoid) {
//通过虚拟机 创建爱你全新的 evn
JNIEnv *jniEnv = nullptr;
jint result = javaVm->GetEnv(reinterpret_cast<void **>(&jniEnv), JNI_VERSION_1_6);
if (result != JNI_OK) {
return JNI_ERR; // 主动报错
}
jclass mainActivityClass = jniEnv->FindClass(classPathName);
jniEnv->RegisterNatives(mainActivityClass, jniNativeMethod,
sizeof(jniNativeMethod) / sizeof(JNINativeMethod));//动态注册的数量
return JNI_VERSION_1_6;
}
复制代码
输出:
动态注册: 我是动态注册的
6. 异常处理
异常处理是 Java 程序设计语言的重要功能, JNI 中的异常行为与 Java 中的有所不同,在 Java 中,当抛出一个异常时,虚拟机停止执行代码块并进入调用栈反向检查能处理特定类型异常的异常处理程序代码块,这也叫捕获异常。虚拟机清除异常并将控制权交给异常处理程序。相比之下, JNI 要求开发人员在异常发生后显式地实现异常处理流。
捕获异常:
JNIEvn 接口提供了一组与异常相关的函数集,在运行过程中可以使用 Java 类查看这些函数,比如代码如下:
public native void dynamicRegister2(String name);
/**
- 测试抛出异常
- @throws NullPointerException
*/
private void testException() throws NullPointerException {
throw new NullPointerException(“MainActivity testException NullPointerException”);
}
当调用 testException 方法时,dynamicRegister2 该原生方法需要显式的处理异常信息,JNI 提供了 ExceptionOccurred 函数查询虚拟机中是否有挂起的异常。在使用完之后,异常处理程序需要用 ExceptionClear 函数显式的清除异常,如下代码:
jthrowable exc = env->ExceptionOccurred(); // 检测是否发生异常
if (exc) {//如果发生异常
env->ExceptionDescribe(); // 打印异常信息
env->ExceptionClear(); // 清除掉发生的异常
}
抛出异常:
JNI 也允许原生代码抛出异常。因为异常是 Java 类,应该先用 FindClass 函数找到异常类,用 ThrowNew 函数可以使用化且抛出新的异常,如下代码所示:
jthrowable exc = env->ExceptionOccurred(); // 检测是否发生异常
if (exc) {//如果发生异常
jclass newExcCls = env->FindClass(“java/lang/IllegalArgumentException”);
env->ThrowNew(newExcCls, “JNI 中发生了一个异常信息”); // 返回一个新的异常到 Java
}
因为原生函数的代码执行不受虚拟机的控制,因此抛出异常并不会停止原生函数的执行并把控制权交给异常处理程序。到抛出异常时,原生函数应该释放所有已分配的原生资源,例如内存及合适的返回值等。通过 JNIEvn 接口获得的引用是局部引用且一旦返回原生函数,它们自动地被虚拟机释放。
示例代码:
public class MainActivity extends AppCompatActivity {
private String TAG = this.getClass().getSimpleName();
/**
- 1. 加载 native 库
*/
static {
System.loadLibrary(“native-lib”);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dynamicRegister2(“测试异常处理”);
}
public native void dynamicRegister2(String name);
/**
- 测试抛出异常
- @throws NullPointerException
*/
private void testException() throws NullPointerException {
throw new NullPointerException(“MainActivity testException NullPointerException”);
}
}
native-lib.cpp 文件
#include <jni.h>
#include
#include <android/log.h>
#include
#define TAG “native-lib”
// VA_ARGS 代表 …的可变参数
#define LOGD(…) __android_log_print(ANDROID_LOG_DEBUG, TAG, VA_ARGS);
#define LOGE(…) __android_log_print(ANDROID_LOG_ERROR, TAG, VA_ARGS);
#define LOGI(…) __android_log_print(ANDROID_LOG_INFO, TAG, VA_ARGS);
/**
- TODO 动态注册
*/
…
…
extern “C” //支持 C 语言
JNIEXPORT void JNICALL //告诉虚拟机,这是jni函数
native_dynamicRegister2(JNIEnv *env, jobject instance, jstring name) {
const char *j_name = env->GetStringUTFChars(name, NULL);
LOGD(“动态注册: %s”, j_name)
jclass clazz = env->GetObjectClass(instance);//拿到当前类的class
jmethodID mid =env->GetMethodID(clazz, “testException”, “()V”);//执行 Java 测试抛出异常的代码
env->CallVoidMethod(instance, mid); // 执行会抛出一个异常
jthrowable exc = env->ExceptionOccurred(); // 检测是否发生异常
if (exc) {//如果发生异常
env->ExceptionDescribe(); // 打印异常信息
env->ExceptionClear(); // 清除掉发生的异常
jclass newExcCls = env->FindClass(“java/lang/IllegalArgumentException”);
env->ThrowNew(newExcCls, “JNI 中发生了一个异常信息”); // 返回一个新的异常到 Java
}
//释放
extern “C” //支持 C 语言
JNIEXPORT void JNICALL //告诉虚拟机,这是jni函数
native_dynamicRegister2(JNIEnv *env, jobject instance, jstring name) {
const char *j_name = env->GetStringUTFChars(name, NULL);
LOGD(“动态注册: %s”, j_name)
jclass clazz = env->GetObjectClass(instance);//拿到当前类的class
jmethodID mid =env->GetMethodID(clazz, “testException”, “()V”);//执行 Java 测试抛出异常的代码
env->CallVoidMethod(instance, mid); // 执行会抛出一个异常
jthrowable exc = env->ExceptionOccurred(); // 检测是否发生异常
if (exc) {//如果发生异常
env->ExceptionDescribe(); // 打印异常信息
env->ExceptionClear(); // 清除掉发生的异常
jclass newExcCls = env->FindClass(“java/lang/IllegalArgumentException”);
env->ThrowNew(newExcCls, “JNI 中发生了一个异常信息”); // 返回一个新的异常到 Java
}
//释放
Android-音视频学习系列(一)-JNI-从入门到精通,android开发入门相关推荐
- Android音视频学习系列(五) — 掌握音频基础知识并使用AudioTrack、OpenSL ES渲染PCM数据
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- Android音视频学习系列(七) — 从0~1开发一款Android端播放器(支持多协议网络拉流本地文件)
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- Android音视频学习系列(八) — 基于Nginx搭建(rtmp、http)直播服务器
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- Android音视频学习系列(九) — Android端实现rtmp推流
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- Android音视频学习系列(六) — 掌握视频基础知识并使用OpenGL ES 2.0渲染YUV数据
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- Android音视频学习系列(十) — 基于FFmpeg + OpenSL ES实现音频万能播放器
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- android 键编译,Android 音视频学习系列 (四) 一键编译 32/64 位 FFmpeg 4.2.2
前言 2020/5/20 增加了硬件解码编译脚本 编译环境 Centos + NDK20b + FFmpeg4.2.2 + Android-21/16 2020/4/26 更新了编译 64 位脚本 编 ...
- Android音视频 - 学习路线概览
PS 我们上一个系列 - OpenGL ES 暂告一段落,如果你对相机滤镜感兴趣,可以参看之前的文章. 从本篇开始呢,开始记录Android音视频的相关知识. 学习路线概览 Android音视频的基础 ...
- Android音视频学习 之C语言入门 (一)
前言 作为一个Android开发,想要学习音视频方向知识和NDK技术.就得具备C/C++ 语言基础,下面我们就先来学习 C语言基础. 简介 C 语言是一种通用的高级语言,最初是由丹尼斯·里奇在贝尔实验 ...
- android 音乐播放器mv播放功能,Android 音视频学习基础Android最简单的音频播放器| 神农笔记...
/* *最简单的基于FFmpeg的音频播放器 *Simplest FFmpeg Audio Player *本程序实现了音频的解码和播放. * */ #include #include extern ...
最新文章
- python封装方法有几种_Python中的封装有什么作用?
- bzoj 3875: [Ahoi2014Jsoi2014]骑士游戏【dp+spfa】
- torch_geometric笔记:nn. graclus (图点分类)
- html5 div css 页签,div css 实现tabs标签的思路及示例代码
- Ubuntu+Django+Nginx+uWSGI+Mysql搭建Python Web服务器
- 二项分布的期望方差证明_关于二项分布
- parseInt原来是这样用的
- android中webview的实现
- 百战学堂python教学文档_尚学堂百战程序员:python对文件的操作
- Python的EEMD实现
- 推荐系统三十六式:矩阵分解 总结
- matlab如何进行四维拟合,matlab四维插值拟合
- word把选择答案弄到题目里_将Word解答中的答案项批量填入题干
- 颜色转换助手RGB888-565
- 如何区别计算机体系结构与计算机组成这两个概念?
- 四维图新总经理孙玉国谈导航
- 有哪些好用的视频录制软件?快进来学习一波
- 未来几年的IT发展方向-目前的趋势已经非常明朗
- PHP 源码加密学习
- 教育部:不得将研究生当作廉价劳动力!