JNI 跨线程调用时局部引用的陷阱_跨线程调用函数
myzbx 2025-09-01 09:52 47 浏览
一、背景
近期在项目中遇到一个困扰了2天的问题,虽然知道问题的原因,但始终不明所以,解决的方式也是囫囵吞枣。问题的起因是,项目中封装了一个统一的JNI 回调函数,将Native层C++ 调用Java 的函数统一接管,做过JNI 的都知道,Native调用Java 回调函数时需要考虑跨线程的情况,即在JNI 线程调用和不在JNI 线程调用的情况(有人喜欢把这类线程叫作子线程)。在JNI 线程时操作起来比较简单,直接调用即可。但不在JNI 线程时需要先对相关对象附在JNI 线程中,否则会闪退。
本文的问题在调用时已经考虑了线程的附加与分离,但依然报下述crash :
//(局部引用非法)
java_vm_ext.cc:598] JNI DETECTED ERROR IN APPLICATION: JNI ERROR (app bug): jobject is an invalid local reference: 0x7a38ba2035 (reference outside the table: 0x7a38ba2035) java_vm_ext.cc:598] JNI DETECTED ERROR IN APPLICATION: JNI ERROR (app bug): jobject is an invalid local reference: 0x7a38ba2035 (reference outside the table: 0x7a38ba2035)
java_vm_ext.cc:598] in call to GetObjectClass
2025-08-04 10:18:43.917 32203-7932 n.collaboration com.tran.collaboration A runtime.cc:708] Runtime aborting...
runtime.cc:708] Dumping all threads without mutator lock held
runtime.cc:708] All threads:
runtime.cc:708] DALVIK THREADS (26):
runtime.cc:708] "main" prio=10 tid=1 Native
runtime.cc:708] | group="" sCount=1 ucsCount=0 flags=1 obj=0x72adc9e8 self=0xb40000784b84df50
runtime.cc:708] | sysTid=32203 nice=-10 cgrp=top-app sched=0/0 handle=0x7a3a0844f8
runtime.cc:708] | state=S schedstat=( 807140063 37525466 241 ) utm=68 stm=12 core=6 HZ=100二、原因分析
分析这个问题之前,先将问题涉及的代码贴出来,这样好讲。
统一的回调函数:
JavaObjCtx.h
//
// Created by Rambo.liu on 2025/8/1.
// java object 管理器
#ifndef INPUTSHARE_JAVAOBJCTX_H
#define INPUTSHARE_JAVAOBJCTX_H
#include <jni.h>
class JavaObjCtx {
public:
JNIEnv *env;
//类对象
jclass clazz;
//构造器
jmethodID constructor;
//实例对象
jobject instanceObj;
//方法ID
jmethodID methodId;
JavaVM *jvm;
/**
* java 对象
* @param _jvm
* @param _env
* @param clzz 类 需要包名 如:com/tran/share/input/Injection
* @param methodName 方法名
* @param methodSign 方法签名
*/
JavaObjCtx(JavaVM *_jvm,JNIEnv *_env,const char* clzz,const char* methodName,const char* methodSign);
~JavaObjCtx();
/**
* 调用函数
* @param obj 实例对象,需要注意跨线程调用,
* 注意:obj 在跨线程调用时,当第一次在 JNI 线程调用时工作正常,但第二次在非 JNI 线程调用时,原始局部引用 jobject 已经失效。
* 解决方案为:在首次获取 Java 对象时,直接转换为全局引用
* 第二次调用时 obj 打印不为空但实际已失效,是 JNI 局部引用机制的一个典型陷阱
* 当 JNI 方法返回时,整个局部引用表会被自动清空,但 C++ 变量 obj 仍保留原地址值
* 类似「野指针」现象:指针地址非 NULL,但指向的内容已无效
* @param methodID
* @param ...
*/
void callVoidMethod(jobject obj, jmethodID methodID, ...);
bool isReferenceValid(JNIEnv* env, jobject obj);
};
#endif //INPUTSHARE_JAVAOBJCTX_HJavaObjCtx.cpp
//
// Created by Rambo.liu on 2025/8/1.
//
#include "JavaObjCtx.h"
#include "Log.h"
JavaObjCtx::JavaObjCtx(JavaVM *_jvm,JNIEnv *_env, const char *clzz, const char *methodName,
const char *methodSign) {
LOGI("class = %s methodId = %s methodSign = %s", clzz, methodName, methodSign);
env = _env;
jvm = _jvm;
//先找类对象
jclass injectionCls = env->FindClass(clzz);
if (injectionCls == nullptr) {
LOGE("找不到 %s 类对象", clzz);
return;
}
//找到类对象,先初始化,获取类对象实例,使用默认的构造方法
jmethodID _constructor = env->GetMethodID(injectionCls, "<init>", "()V");
if (nullptr == _constructor) {
LOGW("can't constructor injectionCls");
return;
}
//构造injectionCls 实例对象
jobject injectionClsObject = env->NewObject(injectionCls, _constructor);
if (nullptr == injectionClsObject) {
LOGW("can't new injectionClsObject");
env->DeleteLocalRef(injectionCls);
return;
}
//查找要调用的方法
jmethodID _methodId = env->GetMethodID(injectionCls, methodName,
methodSign);
if (_methodId == nullptr) {
LOGV("onStatus 未找到,直接释放类对象与实例对象");
env->DeleteLocalRef(injectionCls);
env->DeleteLocalRef(injectionClsObject);
return;
}
clazz = injectionCls;
instanceObj = injectionClsObject;
constructor = _constructor;
methodId = _methodId;
}
JavaObjCtx::~JavaObjCtx() {
LOGI("JavaObjCtx::~JavaObjCtx()析构");
if (env) {
if (clazz) {
env->DeleteLocalRef(clazz);
}
}
}
void JavaObjCtx::callVoidMethod(jobject obj, jmethodID methodID, ...) {
bool attached = false;
if (!env) {
LOGE("JNIEnv is null!");
return;
}
if (!obj) {
LOGE("Java object is null!");
return;
}
// 检查线程是否已附加
if (jvm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_EDETACHED) {
if (jvm->AttachCurrentThread(&env, nullptr) == JNI_OK) {
attached = true;
LOGI("运行在非主线程中,附加成功");
} else {
LOGE("此函数运行在非创建JNI线程中,附加JVM 失败!!");
return;
}
} else {
LOGI("函数已运行在创建JNI 线程中,无需附加");
}
LOGD("跨线程地址: %p", obj); // 地址相同
LOGD("是否有效: %d", isReferenceValid(env, obj)); // 必为false
va_list args;
va_start(args, methodID);
env->CallVoidMethodV(obj, methodID, args);
va_end(args);
// 检查调用是否产生异常
if (env->ExceptionCheck()) {
LOGD("Exception occurred while calling method");
env->ExceptionDescribe();
env->ExceptionClear();
}
if (attached) {
jvm->DetachCurrentThread();
}
}
bool JavaObjCtx::isReferenceValid(JNIEnv *env, jobject obj) {
if (!obj){
LOGI("obj 已经为空");
return false;
}
jclass objClass = env->GetObjectClass(obj); // 关键检查
if (env->ExceptionCheck() || !objClass) {
env->ExceptionClear();
LOGI("obj 已经为空");
return false;
}
return true;
}出现问题的场景为:
- Socket 初始时针对Socket初始化各种失败的场景将结果回调给java 层做业务交互。这类场景都在JNI 线程中调用 callVoidMethod。调用的代码如下:
//回调函数
std::function<void(CONNECTED_STATUS status)> _connectCallBack;
//状态改变时 触发回调
void setStatus(CONNECTED_STATUS status) {
_status = status;
LOGI("80----------- status = %d",status);
if(_connectCallBack) {
_connectCallBack(status);
}
}
void start(std::string &ip, const int port) {
if (getStatus() == CONNECTED_STATUS::CONNECTING) {
LOGI("正在连接中...");
return;
} else if (getStatus() == CONNECTED_STATUS::CONNECTED) {
LOGI("已连接成功,无需要重复连接!!!");
return;
}
setStatus(CONNECTED_STATUS::CONNECTING);
_ip = ip;
_port = port;
// 1. 若已有线程在运行,先终止并清理
if (_connect_thread && _connect_thread->joinable()) {
_connect_thread->join(); // 等待旧线程结束
_connect_thread.reset(); // 释放线程资源
}
_is_connected = false;
_stop = true;
//第二次创建线程调用socket connect
std::future<bool> connectFuture = commit(
std::bind(&SocketClient::connectServer, this, ip, port));
_is_connected = connectFuture.get();
if (_is_connected) {
_conn_cond.notify_one();
std::packaged_task<bool()> task([this, ip, port]() {
LOGI("连接成功,服务器地址:%s 端口:%d", ip.c_str(), port);
_reconnect_count = 1;
_stop = false;
setStatus(CONNECTED_STATUS::CONNECTED);
initEventLoop();
read();
return _is_connected;
});
_connect_thread = std::make_unique<std::thread>(std::move(task));
}
}- 第二次在Socket 连接服务器时调用,这次是开了一个线程来调用Socket 的connect,(非JNI 线程调用)callVoidMethod。(注 上述代码的红色的注释部分)
- 外部调用时两个调用对象为同一个,调用部分代码为:
std::shared_ptr<SocketClient> _socketClient;
std::unique_ptr<JavaObjCtx> _javaObjCtx;
extern "C"
JNIEXPORT void JNICALL
testSocket(JNIEnv *env, jclass thiz, jstring ip, jint port) {
jboolean isCopy;
const char * szIp = env->GetStringUTFChars(ip, &isCopy);
if(!_socketClient) {
_socketClient = std::make_shared<SocketClient>();
}
std::string s(szIp);
if(!_javaObjCtx) {
_javaObjCtx = std::make_unique<JavaObjCtx>(g_jvm,env,"com/tran/share/input/Injection","onStatus",
"(I)V");
}
//收到回调时由此统一向java 回吐
_socketClient->setConnectStatusCallBack([&env](CONNECTED_STATUS status)->void {
_javaObjCtx->callVoidMethod(_javaObjCtx->instanceObj, _javaObjCtx->methodId, status);
});
_socketClient->start(s , port);
env->ReleaseStringUTFChars(ip, szIp);
}Java 层的回调函数:
package com.tran.share.input;
import android.util.Log;
import java.io.DataOutputStream;
/**
* author rambo.liu
* date 2025/7/15 11:04
* Version: 1.0
* Description: 事件注入器
*/
public class Injection {
private final static String TAG = "Injection";
static {
System.loadLibrary("inputShare");
}
public void onStatus(int code) {
Log.i(TAG,"58--------收到来自Native 的回调 code = "+code);
}
}业务需求很简单,但问题却不小。在JNI 对象中明明考虑了跨线程的场景,而且对相关的对象都做判空处理,
callVoidMethod 中判空代码
void JavaObjCtx::callVoidMethod(jobject obj, jmethodID methodID, ...) {
bool attached = false;
//判空
if (!env) {
LOGE("JNIEnv is null!");
return;
}
//判空
if (!obj) {
LOGE("Java object is null!");
return;
}
// 检查线程是否已附加
if (jvm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_EDETACHED) {
if (jvm->AttachCurrentThread(&env, nullptr) == JNI_OK) {
attached = true;
LOGI("运行在非主线程中,附加成功");
} else {
LOGE("此函数运行在非创建JNI线程中,附加JVM 失败!!");
return;
}
} else {
LOGI("函数已运行在创建JNI 线程中,无需附加");
}
va_list args;
va_start(args, methodID);
env->CallVoidMethodV(obj, methodID, args);
va_end(args);
// 检查调用是否产生异常
if (env->ExceptionCheck()) {
LOGD("Exception occurred while calling method");
env->ExceptionDescribe();
env->ExceptionClear();
}
if (attached) {
jvm->DetachCurrentThread();
}
}为毛还报 "java_vm_ext.cc:598] JNI DETECTED ERROR IN APPLICATION: JNI ERROR (app bug): jobject is an invalid local reference: 0x7a38ba2035 (reference outside the table: 0x7a38ba2035) (局部引用非法)"
产生这个问题的根本原因:
当第一次在 JNI 线程调用时工作正常,但第二次在非 JNI 线程调用时,原始局部引用 jobject 已经失效。跨线程传递未保护的局部引用导致的。
- 第一次调用(JNI 线程)
- 局部引用 obj 有效,因为仍在原始 JNI 上下文生命周期内
- 第二次调用(非 JNI 线程)
- 局部引用 obj 已随第一次 JNI 调用结束被自动释放
- 直接使用会导致 invalid local reference 错误
这里还有一个问题需要搞清楚,不然没法理解:
既然已经失效了,按照常规理解,指针指向的内存应该也已经被释放了,callVoidMethod方法中判空应该会被阻断,但上述代码并非如此?何解?
第二次调用时callVoidMthod obj 打印不为空,其实是忽略了一个C/C++ 中的常见问题,即野指针问题。虽然指针指向的内存释放了,但指针并未指空,导致了该问题出现。这也是 JNI 局部引用机制的一个典型陷阱。以下是深度解析:
- 打印显示非空但实际无效
- C++ 层的 jobject 本质上是一个指针(类似 void*),打印时只是显示内存地址值
- 局部引用被释放后,指针地址不会自动置零,但 JVM 内部已将其标记为无效
- JNI 局部引用表机制
jobject obj = env->NewLocalRef(someObj); // 添加到当前线程的局部引用表
- 当 JNI 方法返回时,整个局部引用表会被自动清空,但 C++ 变量 obj 仍保留原地址值
- 类似「野指针」现象:指针地址非 NULL,但指向的内容已无效
为了验证上述分析,可以使用如下代码测试:
extern "C" JNIEXPORT void JNICALL
Java_com_example_Test_printRef(JNIEnv* env, jobject thiz, jobject javaObj) {
jobject localRef = env->NewLocalRef(javaObj);
LOGD("第一次地址: %p", localRef); // 输出类似 0x7a38ba2039
// 模拟跨线程传递(错误做法!)
std::thread([=]{
LOGD("第二次地址: %p", localRef); // 地址相同但已失效!
}).join();
}输出结果:
第一次地址: 0x7a38ba2039
第二次地址: 0x7a38ba2039 // 相同地址但实际已失效三、解决方案
既然局部引用已经 失效,但就转换为全局引用,将传递给callMethodId 方法的jobject 参数转换为实全局引用,调用的地方改为如下:
std::shared_ptr<SocketClient> _socketClient;
std::unique_ptr<JavaObjCtx> _javaObjCtx;
JavaVM *g_jvm = nullptr;
jobject g_globalObj = nullptr;
extern "C"
JNIEXPORT void JNICALL
testSocket(JNIEnv *env, jclass thiz, jstring ip, jint port) {
jboolean isCopy;
const char * szIp = env->GetStringUTFChars(ip, &isCopy);
if(!_socketClient) {
_socketClient = std::make_shared<SocketClient>();
}
std::string s(szIp);
if(!_javaObjCtx) {
_javaObjCtx = std::make_unique<JavaObjCtx>(g_jvm,env,"com/tran/share/input/Injection","onStatus",
"(I)V");
if (g_globalObj) env->DeleteGlobalRef(g_globalObj);
//全局引用
g_globalObj = env->NewGlobalRef(_javaObjCtx->instanceObj);
}
_socketClient->setConnectStatusCallBack([&env](CONNECTED_STATUS status)->void {
//jobject 改为全局引用
_javaObjCtx->callVoidMethod(g_globalObj, _javaObjCtx->methodId, status);
});
_socketClient->start(s , port);
env->ReleaseStringUTFChars(ip, szIp);
}
//不用的时候记得释放全局指针
JNIEXPORT void JNI_OnUnload(JavaVM *vm, void *reserved) {
LOGV("卸載---------如果有全局引用需要將其移除");
JNIEnv* env;
vm->GetEnv((void**)&env, JNI_VERSION_1_6);
if (g_globalObj) {
env->DeleteGlobalRef(g_globalObj);
g_globalObj = nullptr;
}
}除了jobject 这类参数有此问题之外,还有一个对象在跨线程使用的过程中也容易忽视-- JNIEnv *env。在跨线程使用过程中需要注意以下几点:
- 线程安全
JNIEnv* 是线程私有对象,不能跨线程传递。如果在新线程中使用 env,必须先附加到 JVM 并获取当前线程的 env:按如下方式获取当前线程的JNIEnv 指针
// 在 C++ 新线程中获取 JNIEnv* 的正确方式
JNIEnv* env = nullptr;
// 假设 vm 是全局的 JavaVM* 指针(通过 JNI_OnLoad 获取)
int ret = vm->AttachCurrentThread(&env, nullptr);
if (ret != JNI_OK || env == nullptr) {
LOGD("Failed to attach thread");
return;
}
// 使用 env 前检查有效性
if (env->ExceptionCheck()) { ... }
// 线程结束时分离
vm->DetachCurrentThread();- 生命周期:env 仅在当前 JNI 调用或附加的线程生命周期内有效,不要将其保存为全局变量长期使用。
在上述代码JavaObjCtx.h 中刚好把JNIEnv 当作成员变量了:
//
// Created by Rambo.liu on 2025/8/1.
// java object 管理器
#ifndef INPUTSHARE_JAVAOBJCTX_H
#define INPUTSHARE_JAVAOBJCTX_H
#include <jni.h>
class JavaObjCtx {
public:
//成员变量
JNIEnv *env;
//类对象
jclass clazz;
//构造器
jmethodID constructor;
//实例对象
jobject instanceObj;
//方法ID
jmethodID methodId;
JavaVM *jvm;
};
#endif //INPUTSHARE_JAVAOBJCTX_H源文件JavaObjCtx.cpp 中构造器中给JNIEnv* env赋值:
JavaObjCtx::JavaObjCtx(JavaVM *_jvm,JNIEnv *_env, const char *clzz, const char *methodName,
const char *methodSign) {
LOGI("class = %s methodId = %s methodSign = %s", clzz, methodName, methodSign);
//给成员变量赋值
env = _env;
jvm = _jvm;
//先找类对象
......
}这在跨线程调用函数:
void JavaObjCtx::callVoidMethod(jobject obj, jmethodID methodID, ...) {
bool attached = false;
if (!env) {
return;
}
if (!obj) {
return;
}
// 检查线程是否已附加
if (jvm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_EDETACHED) {
if (jvm->AttachCurrentThread(&env, nullptr) == JNI_OK) {
attached = true;
//LOGI("运行在非主线程中,附加成功");
} else {
//LOGE("此函数运行在非创建JNI线程中,附加JVM 失败!!");
return;
}
} else {
//LOGI("函数已运行在创建JNI 线程中,无需附加");
}
va_list args;
va_start(args, methodID);
env->CallVoidMethodV(obj, methodID, args);
va_end(args);
// 检查调用是否产生异常
if (env->ExceptionCheck()) {
LOGD("Exception occurred while calling method");
env->ExceptionDescribe();
env->ExceptionClear();
}
if (attached) {
jvm->DetachCurrentThread();
}
}上述代码中使用的 env 由于是类的成员变量(而非局部变量),但 JNIEnv* 是线程私有的(每个线程有自己独立的 JNIEnv*),不能作为类成员跨线程共享。理由如下:
- 当在新线程中调用此函数时,成员变量 env 可能仍是其他线程的无效值(甚至 nullptr)。
- 即使通过 GetEnv 或 AttachCurrentThread 获取了当前线程的 env,若未正确更新成员变量,后续使用的仍是旧的无效 env。
如果在跨线程调用时,env 为成员变量同时又没有随线程更新,就会出现下面的crash 情况:
JNI 中 2025-08-04 19:15:16.227 8925-10816 libc com.tran.collaboration A Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 10816 (Thread-2), pid 8925 (n.collaboration)Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 是 JNI 开发中常见的内存访问错误,本质是 C/C++ 代码尝试访问无效的内存地址(这里是 0x0,即空指针 NULL),导致操作系统触发段错误(Segmentation Fault)。
错误核心原因
SIGSEGV 是 Linux 系统的 “段错误” 信号,SEGV_MAPERR 表示程序尝试访问的内存地址未被映射到合法的内存空间,而 fault addr 0x0 明确指出:代码中存在对空指针(NULL)的解引用操作(比如访问 NULL->field 或 *(NULL))。
修复方案
核心原则:JNIEnv* 应作为局部变量使用,每次通过 JavaVM* 获取当前线程的有效实例,并严格检查所有指针的有效性。完整方案见文首的JavaObjCtx.h 代码
四、小结
场景 | 正确做法 |
需要跨线程使用 | 必须用 NewGlobalRef 转换 |
临时局部引用 | 确保不跨越 JNI 方法调用 |
调试检查 | 用 GetObjectClass 检测有效性 |
技术类比
JNI 概念 | 类似 C++ 概念 | 关键区别 |
局部引用 | 栈指针 | 自动管理生命周期 |
全局引用 | new 分配的对象 | 需手动释放 |
弱全局引用 | weak_ptr | 可能被 GC 回收 |
绝对禁忌
// 错误!跨线程传递局部引用
void dangerousCall(jobject obj) {
std::thread([=]{
// 即使 obj 地址非空,实际已失效
someJNICall(obj);
}).detach();
}记住:在 JNI 中,指针非空 ≠ 引用有效!必须通过 JVM 机制验证。
相关推荐
- 如何设计一个优秀的电子商务产品详情页
-
加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品电子商务网站的产品详情页面无疑是设计师和开发人员关注的最重要的网页之一。产品详情页面是客户作出“加入购物车”决定的页面...
- 怎么在JS中使用Ajax进行异步请求?
-
大家好,今天我来分享一项JavaScript的实战技巧,即如何在JS中使用Ajax进行异步请求,让你的网页速度瞬间提升。Ajax是一种在不刷新整个网页的情况下与服务器进行数据交互的技术,可以实现异步加...
- 中小企业如何组建,管理团队_中小企业应当如何开展组织结构设计变革
-
前言写了太多关于产品的东西觉得应该换换口味.从码农到架构师,从前端到平面再到UI、UE,最后走向了产品这条不归路,其实以前一直再给你们讲.产品经理跟项目经理区别没有特别大,两个岗位之间有很...
- 前端监控 SDK 开发分享_前端监控系统 开源
-
一、前言随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的...
- Ajax 会被 fetch 取代吗?Axios 怎么办?
-
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天给大家带来的主题是ajax、fetch...
- 前端面试题《AJAX》_前端面试ajax考点汇总
-
1.什么是ajax?ajax作用是什么?AJAX=异步JavaScript和XML。AJAX是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX可以使网页实...
- Ajax 详细介绍_ajax
-
1、ajax是什么?asynchronousjavascriptandxml:异步的javascript和xml。ajax是用来改善用户体验的一种技术,其本质是利用浏览器内置的一个特殊的...
- 6款可替代dreamweaver的工具_替代powerdesigner的工具
-
dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver...
- 我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊
-
接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...
- 福斯《死侍》发布新剧照 "小贱贱"韦德被改造前造型曝光
-
时光网讯福斯出品的科幻片《死侍》今天发布新剧照,其中一张是较为罕见的死侍在被改造之前的剧照,其余两张剧照都是死侍在执行任务中的状态。据外媒推测,片方此时发布剧照,预计是为了给不久之后影片发布首款正式预...
- 2021年超详细的java学习路线总结—纯干货分享
-
本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础重点知识点:数据类型、核心语法、面向对象...
- 不用海淘,真黑五来到你身边:亚马逊15件热卖爆款推荐!
-
Fujifilm富士instaxMini8小黄人拍立得相机(黄色/蓝色)扫二维码进入购物页面黑五是入手一个轻巧可爱的拍立得相机的好时机,此款是mini8的小黄人特别版,除了颜色涂装成小黄人...
- 2025 年 Python 爬虫四大前沿技术:从异步到 AI
-
作为互联网大厂的后端Python爬虫开发,你是否也曾遇到过这些痛点:面对海量目标URL,单线程爬虫爬取一周还没完成任务;动态渲染的SPA页面,requests库返回的全是空白代码;好不容易...
- 最贱超级英雄《死侍》来了!_死侍超燃
-
死侍Deadpool(2016)导演:蒂姆·米勒编剧:略特·里斯/保罗·沃尼克主演:瑞恩·雷诺兹/莫蕾娜·巴卡林/吉娜·卡拉诺/艾德·斯克林/T·J·米勒类型:动作/...
- 停止javascript的ajax请求,取消axios请求,取消reactfetch请求
-
一、Ajax原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。注意abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。停止javascript的ajax请求...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)
- JS Loop For (32)
