开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Android 15 新 API:内存追踪利器 Profil

发表于 2024-04-27

android15-base-profiling.png

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前言

我们都知道 Android Studio 里内置了 Profiler tool 供大家对 App 在 Memory、CPU、Network、Power 等角度进行 dump 和分析。

但如果一个内存相关的 bug 是运行时发生的,而且很难复现,那么后期就很难准确定位发生时的具体状况。

值得兴奋的是 Android 15 将直面这个痛点:引入了 ProfilingManager API,允许 app 对 Memory 进行动态的、随时随地的 dump。

生成的文件默认存在本地,也可以通过网络传递到 offboard,方便开发者事后回溯。

API 说明

ProfilingManager 主要提供了 3 个方法。

registerForAllProfilingResults

注册 profiling 请求的回调和执行的线程,需要如下两个参数:

Parameters Descriptions
executor Executor: 回调执行的线程池实例,不可为空
listener Consumer: 携带 profiling 结果的 listener 实例,不可为空

unregisterForAllProfilingResults

注销 profiling 请求的回调,如果没有指定 listener 参数的话,将移除所有 callback。

Parameters Descriptions
listener Consumer: 待移除的回调,null 的话移除所有

requestProfiling

请求进行一次 profiling 操作,有非常详细的参数可供设置:

Parameters Descriptions
profilingType int: profiling 操作的类型,主要包括 dump Java 堆的 PROFILING_TYPE_JAVA_HEAP_DUMP,dump 堆的 PROFILING_TYPE_HEAP_PROFILE, dump 栈的 PROFILING_TYPE_STACK_SAMPLING 和 dump 系统 trace 的PROFILING_TYPE_SYSTEM_TRACE,参数不能为空。
parameters Bundle: 携带请求额外的相关参数, 如果包含了未定义的参数类型,请求会失败,在 callback 当中以 ERROR_FAILED_INVALID_REQUEST 结果进行返回,参数可为空。
tag String: 回来识别 dump 输出的 tag 标签,其中的前 20 个字符将会以小写的形式拼接到 dump 文件名中,参数可为空
cancellationSignal CancellationSignal: 支持请求侧用来取消 dump 的 cancellation 实例,如果 dump 结果已出来的话,会被返回。参数可为空,此时将执行系统默认的超时时间,之后结束 dump。
executor Executor: 回调执行的线程池实例,参数可为空。但如果没有其他 executor 注册的话,该请求会被无视。
listener Consumer: 监听操作结果的实例,registerForAllProfilingResults() 注册的 callback 同样也会被回调,参数可为空。但如果没有其他 listener 注册的话,该请求会被无视。

需要说明的是:

  • 很多时候,并不推荐直接使用该 API 进行 dump,相反可以采用 androidx 中封装好的高层级接口进行请求。该接口内部会依据可用选项和简化的参数进行正确地请求。
  • 并非所有情况都会得到响应
  • 需要同时考虑 result 的监听和执行它的线程池两个参数,要么都设置,要么都不设置交给 registerForAllProfilingResults() 一起设置

ProfilingResult

在上述提到的 registerForAllProfilingResults() 里会回调 ProfilingResult 参数过来,它用来封装单次请求 profiling 操作的结果。

主要提供了这几个方法来获得信息:

  • getErrorCode():获取 profiling 请求失败的原因,如果成功的话,值为 ERROR_NONE 即 0,其他的还有 ERROR_FAILED_RATE_LIMIT_SYSTEM 等值
  • getErrorMessage():获取额外的失败信息
  • getTag():请求时传入的参数 tag,方便回溯
  • getResultFilePath():获取 profiling 结果文件的路径

实战

留意一下,需要将 Android 15 的 SDK 升级到 revision 3 才可以看到该 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
kotlin复制代码     class ProfilingActivity : AppCompatActivity() {
     private val singleThreadExecutor = Executors.newSingleThreadExecutor()
 
     private val profilingResultConsumer = Consumer<ProfilingResult> {
         Log.d(TAG_PROFILING, "accept profilingResult:${it.printProfilingResult()}")
    }
 
     private val profilingManager by lazy {
         getSystemService(ProfilingManager::class.java)
    }
 
     override fun onCreate(savedInstanceState: Bundle?) {
        ...
         binding.dump.setOnClickListener {
             Log.d(TAG_PROFILING, "button dump tapped")
 
             profilingManager.registerForAllProfilingResults(
                 singleThreadExecutor,
                 profilingResultConsumer
            )
        }
 
         binding.request.setOnClickListener {
             Log.d(TAG_PROFILING, "button request tapped")
 
             profilingManager.requestProfiling(
                 // ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE,
                 // ProfilingManager.PROFILING_TYPE_JAVA_HEAP_DUMP,
                 // ProfilingManager.PROFILING_TYPE_STACK_SAMPLING,
                 ProfilingManager.PROFILING_TYPE_HEAP_PROFILE,
                 null,
                 "TEST_FOR_PROFILING_MANAGER",
                 null,
                 singleThreadExecutor,
                 profilingResultConsumer
            )
        }
 
         binding.stop.setOnClickListener {
             Log.d(TAG_PROFILING, "button stop tapped")
 
             profilingManager.unregisterForAllProfilingResults(profilingResultConsumer)
        }
    }
 }

可以看到我们在代码里设置的 tag 为 *”TEST_FOR_PROFILING_MANAGER”* 。

转存失败,建议直接上传图片文件
我们点击 “register dump profile” button 开始注册回调,然后点击 “request dump profile” button 开始请求。

看下 Profiling 的 log 输出。

点击完 request 之后需要等待一段时间(系统默认的 dump 超时为 120s 左右)才会看到 dump 结果。

注意,不要重复点击,否则会收到如下错误的 callback。其中 3 对应的是 ERROR_FAILED_PROFILING_IN_PROGRESS,表示仍在 dump 中,该重复请求被拒绝。

ProfilingResult{errorCode:3 errorMessage:null resultFilePath:null tag:TEST_FOR_PROFILING_MANAGER}

下面我们以如下 4 种 dump 类型看下具体的 ProfilingResult 输出内容。

PROFILING_TYPE_JAVA_HEAP_DUMP

1
bash复制代码04-21 19:37:32.220  7184  7270 D Profiling: accept profilingResult:ProfilingResult{errorCode:0 errorMessage:null resultFilePath:/data/user/0/com.ellison.osvdemo/files/profiling/profile_testforprofilingmana_2024-04-21-19-37-26.perfetto-java-heap-dump tag:TEST_FOR_PROFILING_MANAGER}

可以看到成功输出了 Java heap 的 dump 文件,并且咱们的 tag 被拼接到了文件名中,而且将_删除并限制了 20 的长度。

所以咱们设置的 tag 最好不要包含字母以外的字符,并且不要过长,不然不方便定位 tag。

PROFILING_TYPE_HEAP_PROFILE

1
bash复制代码04-21 19:47:16.810  7474  7552 D Profiling: accept profilingResult:ProfilingResult{errorCode:0 errorMessage:null resultFilePath:/data/user/0/com.ellison.osvdemo/files/profiling/profile_testforprofilingmana_2024-04-21-19-45-11.perfetto-heap-profile tag:TEST_FOR_PROFILING_MANAGER}

和上面的 Java dump 一样,成功输出了 heap 全量的 dump 文件。

PROFILING_TYPE_STACK_SAMPLING

1
bash复制代码04-21 19:53:01.043  7704  7790 D Profiling: accept profilingResult:ProfilingResult{errorCode:0 errorMessage:null resultFilePath:/data/user/0/com.ellison.osvdemo/files/profiling/profile_testforprofilingmana_2024-04-21-19-51-55.perfetto-stack-sample tag:TEST_FOR_PROFILING_MANAGER}

dump stack 的记录也是一样,不再赘述。

PROFILING_TYPE_SYSTEM_TRACE

1
bash复制代码04-21 15:43:33.926  4730  4775 D Profiling: accept profilingResult:ProfilingResult{errorCode:7 errorMessage:Trace is not supported until redaction lands resultFilePath:null tag:null}

和前面 3 中 type 不同,这种 dump 系统 trace 的请求总是会直接失败。

ProfilingResult 结果对应的 errorCode 为 7,常量名称为 ERROR_FAILED_INVALID_REQUEST,表示请求失败了:这是一个不合法的 ProfilingRequest。

The request failed due to invalid ProfilingRequest.

单从这个 error 定义来看,压根不知道问题出在哪。

在 API 章节里我们提到过,如果调用 requestProfiling() 时传递的 Bundle 参数包含了不支持的 key-value,会造成 ERROR_FAILED_INVALID_REQUEST 错误。

但可以看到:实际上咱们的 DEMO 代码里啥 bundle 都没携带,所以应该不是这个原因。

好在,ProfilingRequest 结果还打携带了 errorMessage,其内容为:Trace is not supported until redaction lands。

笔者尝试依据该 message 找到蛛丝马迹,但无论在 Android 官网,还是在 Android 源码里,抑或是在 AOSP issue 的首页上,都没找到合相关的记录。

我猜测是 PROFILING_TYPE_SYSTEM_TRACE 需要某个 DOC 里没说明的 Bundle 参数,造成了失败。当然也有可能是 beta 版阶段的系统 bug。

后续再看。

perfetto 支持

以 PROFILING_TYPE_JAVA_HEAP_DUMP 的文件为例,从 data 目录里 pull 之后,在 ui.perfetto.dev 中打开,可以进行分析。

转存失败,建议直接上传图片文件
DEMO 源码


AndroidOSVDemo

结语

ProfilingManager API 允许 App 直接进行各种 Memory 数据的 dump,相信这能在一定程度上帮助开发者进行内存相关问题的回溯,不再像以前那样被动。

从上面的实战也能看到目前部分功能还存在些问题,期待正式版本能够修复。

感兴趣的朋友可以在 Android 15 稳定版的时候再行体验。

笔者猜测因为 Memory 是最高优的关注点,所以目前 ProfilingManager 只支持 dump Memory。但我相信随着该 API 的成熟和普及,以及确实有 dump 其他点强劲需求的话,肯定会逐步支持更多 dump 的内容。

参考资料

  • ProfilingManager
  • ProfilingResult

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JavaScript动手实现2FA动态认证码 前言 2FA认

发表于 2024-04-27

前言

好久没登陆Github了,发现Github需要经过2FA认证才能正常使用功能。Github推荐用可以用认证软件来扫描二维码来生成动态密码,或者利用secretKey专属密钥来生成动态密码。

我试了试用github推荐的软件,但是他们要么要付费,要么国内手机号无法注册,行不通呀根本行不通。

所以我就在百度里面搜免费2FA动态密码生成器,真的找到了好心人提供的生成器,输入动态密码后丝滑进入Github。

2FA认证是什么

2FA,2 Factor Authentication,双因子验证/双因素验证,是一种安全密码验证方式。区别于传统的密码验证,由于传统的密码验证是由一组静态信息组成,如:字符、图像、手势等,很容易被获取,相对不安全。2FA是基于时间、历史长度、实物(信用卡、SMS手机、令牌、指纹)等自然变量结合一定的加密算法组合出一组动态密码,一般每60秒刷新一次。不容易被获取和破解,相对安全。 ————以上来自百度百科

在Github这里,2FA采用的双因子一般是时间和个人密钥。Github会根据时间和账号的密钥比对我们填写的动态密码的一致性,一致才能通过验证。

PS:

  1. Github中动态密码长度为6位;
  2. Github中动态密码的有效期为30s,也就是说一次性密码每半分钟会变化。

TOTP和HOTP

TOTP(Time-Based One-Time Password Algorithm) 是基于时间的一次性密码算法,可以根据时间因子和密钥生成一次性密码。TOTP算法是基于HOTP算法的,HOTP算法是根据计数器(移动因子)和密钥生成一次性密码。

HOTP

算法公式:HOTP(K,C) = Trancate(HMAC-SHA-1(K,C))

  • K:密钥,两端之间共享,不同的用户密钥应该保证唯一且不同
  • C:计数器(移动因子),是一个8字节的值
  • Digit:一次性密码的位数

HOTP算法是根据计数器(移动因子)和密钥生成一次性密码。

在经过 HMAC-SHA-1算法用 密钥K 加密 计数器C 后,我们会得到20个字节的十六进制字符串。

Trancate算法会对加密得到的十六进制字符串进行处理。首先会选取最后1个字节的16进制串,将其转化为十进制数字offset; 然后会从offset开始选取4个字节的字符串,将其转化为十进制数字;最后会根据 Digit位数 对十进制数字取模,获取最后几位数字,如果位数不够就在前方补0。

image.png

TOTP

算法公式:TOTP(K,T) = HOTP(K,(T-T0)/X)

  • K:密钥,两端之间共享,不同的用户密钥应该保证唯一且不同
  • T:当前时间戳(以秒为单位)
  • T0: 初始时间戳,一般为0
  • X:时间步长,一般为30s、60s,这里是30s
  • Digit:一次性密码的位数

从TOTP算法公式可以看到,我们只需要获取时间因子,将时间因子替换HOTP中的计数器即可。

JavaScript实现代码

第一步:密钥解码

Github 2FA认证提供的是经过base32(RFC3548)编码后的secretKey,所以在这里我们需要对其进行解码,解码的encoding类型选择RFC3548。

因为Node crypto中的加密的参数需要传递binaryLike形式的值,所以解码的内容我们要用Buffer来进行接收。

1
2
js复制代码const secret = 'Github 2FA认证提供的经过base32(RFC3548)编码后的secretKey'; // 这是你的秘钥,需要保密
const secretKey = Buffer.from(base32Decode(secret, 'RFC3548'));

第二步:实现HOTP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码// 生成HMAC-based One-Time Password (HOTP)
function generateHOTP(key, counter, digits = 6) {
// 将计数器转换为8字节的Buffer
const counterBuffer = Buffer.alloc(8);
counterBuffer.writeBigInt64BE(BigInt(counter), 0);

// 使用HMAC-SHA1算法计算HMAC
const hmac = crypto.createHmac('sha1', key).update(counterBuffer).digest();

// 获取HMAC的最后一个字节的低四位作为偏移量
const offset = hmac[hmac.length - 1] & 0x0F;

// 将动态密码的部分转换为整数
let dynamicPassword = (hmac[offset] & 0x7F)<< 24
| (hmac[offset + 1] & 0xFF) << 16
| (hmac[offset + 2] & 0xFF) << 8
| (hmac[offset + 3] & 0xFF);

// 限制密码长度
dynamicPassword = dynamicPassword % Math.pow(10, digits);

// 根据指定的位数格式化密码
return dynamicPassword.toString().padStart(digits, '0');
}

第三步:获取和处理时间戳

1
2
3
4
5
js复制代码// 获取当前时间的时间戳,并以时间步长划分为计数器
function getCounter(timeStep = 30) {
const currentTime = Math.floor(Date.now() / 1000); // 获取当前时间戳(单位:秒)
return Math.floor(currentTime / timeStep);
}

第四步:得到TOTP动态密码

1
2
3
4
5
6
js复制代码// 生成 TOTP
function generateTOTP(key, timeStep = 30, digits = 6) {
const counter = getCounter(timeStep);
console.log(counter)
return generateHOTP(key, counter, digits);
}

全部代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
js复制代码const crypto = require('crypto');
const base32Decode = require('base32-decode')
// 生成HMAC-based One-Time Password (HOTP)
function generateHOTP(key, counter, digits = 6) {
// 将计数器转换为8字节的Buffer
const counterBuffer = Buffer.alloc(8);
counterBuffer.writeBigInt64BE(BigInt(counter), 0);

// 使用HMAC-SHA1算法计算HMAC
const hmac = crypto.createHmac('sha1', key).update(counterBuffer).digest();

// 获取HMAC的最后一个字节的低四位作为偏移量
const offset = hmac[hmac.length - 1] & 0x0F;

// 将动态密码的部分转换为整数
let dynamicPassword = (hmac[offset] & 0x7F)<< 24
| (hmac[offset + 1] & 0xFF) << 16
| (hmac[offset + 2] & 0xFF) << 8
| (hmac[offset + 3] & 0xFF);

// 限制密码长度
dynamicPassword = dynamicPassword % Math.pow(10, digits);

// 根据指定的位数格式化密码
return dynamicPassword.toString().padStart(digits, '0');
}

// 获取当前时间的时间戳,并以时间步长划分为计数器
function getCounter(timeStep = 30) {
const currentTime = Math.floor(Date.now() / 1000); // 获取当前时间戳(单位:秒)
return Math.floor(currentTime / timeStep);
}

// 生成 TOTP
function generateTOTP(key, timeStep = 30, digits = 6) {
const counter = getCounter(timeStep);
console.log(counter)
return generateHOTP(key, counter, digits);
}

// 示例
const secret = 'Github 2FA认证提供的经过base32(RFC3548)编码后的secretKey'; // 这是你的秘钥,需要保密
const secretKey = Buffer.from(base32Decode(secret, 'RFC3548'));
const totp = generateTOTP(secretKey);
console.log('Generated TOTP:', totp);

参考文档

  1. blog.csdn.net/weixin_4279…
  2. 2FA百度百科

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android Native线程找不到class的原因

发表于 2024-04-27

在native创建线程,想调用java层时,通常会去获取到java层class,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码// native线程
void ThreadTest::callJava(void *data) {
   ThreadTest *threadTest = (ThreadTest *) data;
   JNIEnv *env= nullptr;
   threadTest->vm->AttachCurrentThread(&env, nullptr);
   // 查找class
   jclass clazz = env->FindClass("com/hyc/jni_demo/NativeCall");
                                         className);
   jmethodID methodId = env->GetStaticMethodID(clazz, "callStatic", "()I");
   jint result = env->CallStaticIntMethod(clazz, methodId);
   LOGD("result1: %d", result);
   jmethodID methodId2 = env->GetMethodID(clazz, "callNormal", "()I");

   jfieldID field = env->GetStaticFieldID(clazz, "INSTANCE", "Lcom/hyc/jni_demo/NativeCall;");
   jobject nativeCall = env->GetStaticObjectField(clazz, field);
   jint result2 = env->CallIntMethod(nativeCall, methodId2, nullptr);
   LOGD("result2: %d", result2);
   threadTest->vm->DetachCurrentThread();

}

但是却发生了如下崩溃,报Didn’t find class “com.hyc.jni_demo.NativeCall” ,下面我们来探究一下出现这个问题的原因。

1
2
3
4
typescript复制代码Abort message: 'JNI DETECTED ERROR IN APPLICATION: JNI GetStaticMethodID called with pending exception java.lang.ClassNotFoundException: Didn't find class "com.hyc.jni_demo.NativeCall" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64, /system/lib64, /system_ext/lib64]]
                                                                                                     at java.lang.Class dalvik.system.BaseDexClassLoader.findClass(java.lang.String) (BaseDexClassLoader.java:259)
                                                                                                     at java.lang.Class java.lang.ClassLoader.loadClass(java.lang.String, boolean) (ClassLoader.java:379)
                                                                                                     at java.lang.Class java.lang.ClassLoader.loadClass(java.lang.String) (ClassLoader.java:312)

在这之前我们还需要了解一下什么是JavaVM和JNIEnv。

JavaVM和JNIEnv

JNIEnv可以单纯的理解为java层和native层之间的桥梁,每个java线程都有一个自己的JNIEnv。而JavaVM是管理JNIEnv的,它它可以创建新的JNIEnv,获取当前线程的JNIEnv,以及销毁JNIEnv。

JavaVM创建

JavaVM在虚拟机里面只有一个实例,JavaVM在虚拟机启动的时候创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
arduino复制代码// 调用栈
// art/runtime/runtime.cc Runtime::Init
// art/runtime/runtime.cc Runtime::Create
// art/runtime/jni/java_vm_ext.cc JNI_CreateJavaVM
// frameworks/base/core/jni/AndroidRuntime.cpp AndroidRuntime::startVm
// frameworks/base/core/jni/AndroidRuntime.cpp AndroidRuntime::start
// frameworks/base/cmds/app_process/app_main.cpp main


bool Runtime::Init(RuntimeArgumentMap&& runtime_options_in) {
........
 java_vm_ = JavaVMExt::Create(this, runtime_options, &error_msg);
........  
}

std::unique_ptr<JavaVMExt> JavaVMExt::Create(Runtime* runtime,
                                            const RuntimeArgumentMap& runtime_options,
                                            std::string* error_msg) NO_THREAD_SAFETY_ANALYSIS {
 std::unique_ptr<JavaVMExt> java_vm(new JavaVMExt(runtime, runtime_options, error_msg));
 if (java_vm && java_vm->globals_.IsValid() && java_vm->weak_globals_.IsValid()) {
   return java_vm;
}
 return nullptr;
}

JavaVMExt::JavaVMExt(Runtime* runtime,
                    const RuntimeArgumentMap& runtime_options,
                    std::string* error_msg)
  : runtime_(runtime),
    ..................
     // 配置接口    
     unchecked_functions_(&gJniInvokeInterface),
    ..................
     old_allocation_tracking_state_(false) {
 functions = unchecked_functions_;
 SetCheckJniEnabled(runtime_options.Exists(RuntimeArgumentMap::CheckJni) || kIsDebugBuild);
}

const JNIInvokeInterface gJniInvokeInterface = {
 nullptr,  // reserved0
 nullptr,  // reserved1
 nullptr,  // reserved2
 JII::DestroyJavaVM,
 JII::AttachCurrentThread,
 JII::DetachCurrentThread,
 JII::GetEnv,
 JII::AttachCurrentThreadAsDaemon
};

// App层可以调用到的接口
struct _JavaVM {
   const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
   jint DestroyJavaVM()
  { return functions->DestroyJavaVM(this); }
   jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
  { return functions->AttachCurrentThread(this, p_env, thr_args); }
   jint DetachCurrentThread()
  { return functions->DetachCurrentThread(this); }
   jint GetEnv(void** env, jint version)
  { return functions->GetEnv(this, env, version); }
   jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
  { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};
SystemClassLoader的创建

在创建JavaVM后,会创建SystemClassLoader,并设置给JavaVM,这个就是在native线程,我们能拿到的默认ClassLoader。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
scss复制代码// 调用栈
// art/runtime/runtime.cc Runtime::Start
// art/runtime/jni/java_vm_ext.cc JNI_CreateJavaVM
// frameworks/base/core/jni/AndroidRuntime.cpp AndroidRuntime::startVm
// frameworks/base/core/jni/AndroidRuntime.cpp AndroidRuntime::start
// frameworks/base/cmds/app_process/app_main.cpp main

bool Runtime::Start() {
  .............
   system_class_loader_ = CreateSystemClassLoader(this);
  .............
}


static jobject CreateSystemClassLoader(Runtime* runtime) {
 if (runtime->IsAotCompiler() && !runtime->GetCompilerCallbacks()->IsBootImage()) {
   return nullptr;
}

 ScopedObjectAccess soa(Thread::Current());
 ClassLinker* cl = Runtime::Current()->GetClassLinker();
 auto pointer_size = cl->GetImagePointerSize();

 StackHandleScope<2> hs(soa.Self());
 Handle<mirror::Class> class_loader_class(
     hs.NewHandle(soa.Decode<mirror::Class>(WellKnownClasses::java_lang_ClassLoader)));
 CHECK(cl->EnsureInitialized(soa.Self(), class_loader_class, true, true));
 // 获取到ClassLoader的getSystemClassLoader方法
 ArtMethod* getSystemClassLoader = class_loader_class->FindClassMethod(
     "getSystemClassLoader", "()Ljava/lang/ClassLoader;", pointer_size);
 CHECK(getSystemClassLoader != nullptr);
 CHECK(getSystemClassLoader->IsStatic());
 // 执行getSystemClassLoader方法
 JValue result = InvokeWithJValues(soa,
                                   nullptr,
                                   getSystemClassLoader,
                                   nullptr);
 JNIEnv* env = soa.Self()->GetJniEnv();
 // 获取到local Ref
 ScopedLocalRef<jobject> system_class_loader(env, soa.AddLocalReference<jobject>(result.GetL()));
 CHECK(system_class_loader.get() != nullptr);
 // 保存
 soa.Self()->SetClassLoaderOverride(system_class_loader.get());

 Handle<mirror::Class> thread_class(
     hs.NewHandle(soa.Decode<mirror::Class>(WellKnownClasses::java_lang_Thread)));
 CHECK(cl->EnsureInitialized(soa.Self(), thread_class, true, true));

 ArtField* contextClassLoader =
     thread_class->FindDeclaredInstanceField("contextClassLoader", "Ljava/lang/ClassLoader;");
 CHECK(contextClassLoader != nullptr);

 // We can't run in a transaction yet.
 contextClassLoader->SetObject<false>(
     soa.Self()->GetPeer(),
     soa.Decode<mirror::ClassLoader>(system_class_loader.get()).Ptr());
 // 返回global ref
 return env->NewGlobalRef(system_class_loader.get());
}

java层逻辑,根据启动jvm传入参数创建PathClassLoader。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
typescript复制代码    public static ClassLoader getSystemClassLoader() {
       return SystemClassLoader.loader;
  }
   static private class SystemClassLoader {
       public static ClassLoader loader = ClassLoader.createSystemClassLoader();
  }

  private static ClassLoader createSystemClassLoader() {
       String classPath = System.getProperty("java.class.path", ".");
       String librarySearchPath = System.getProperty("java.library.path", "");

       // String[] paths = classPath.split(":");
       // URL[] urls = new URL[paths.length];
       // for (int i = 0; i < paths.length; i++) {
       // try {
       // urls[i] = new URL("file://" + paths[i]);
       // }
       // catch (Exception ex) {
       // ex.printStackTrace();
       // }
       // }
       //
       // return new java.net.URLClassLoader(urls, null);

       // TODO Make this a java.net.URLClassLoader once we have those?
       return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
  }
查找class

在查找Class时,先去获取当前的ClassLoader:

查找当前调用的java方法,如果能拿到方法,那么返回方法所在的Class的ClassLoader,如果为空,那么使用虚拟机创建时创建的SystemClassLoader。此时是native线程刚绑定jvm虚拟机,所以方法为空,返回SystemClassLoader。

而这个ClassLoader时在Zygote进程创建时,传入虚拟机配置参数路径创建的PathClassLoader,只会包含系统相关路径,不会有上层App的dex,所以我们就不能通过这个ClassLoader获取到我们自己的Class,理所当然出现上面那个崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
scss复制代码  static jclass FindClass(JNIEnv* env, const char* name) {
   CHECK_NON_NULL_ARGUMENT(name);
   Runtime* runtime = Runtime::Current();
   ClassLinker* class_linker = runtime->GetClassLinker();
   std::string descriptor(NormalizeJniClassDescriptor(name));
   ScopedObjectAccess soa(env);
   ObjPtr<mirror::Class> c = nullptr;
   if (runtime->IsStarted()) {
     StackHandleScope<1> hs(soa.Self());
     // 查找当前class loader  
     Handle<mirror::ClassLoader> class_loader(hs.NewHandle(GetClassLoader<kEnableIndexIds>(soa)));
     c = class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader);
  } else {
     c = class_linker->FindSystemClass(soa.Self(), descriptor.c_str());
  }
   return soa.AddLocalReference<jclass>(c);
}

template<bool kEnableIndexIds>
static ObjPtr<mirror::ClassLoader> GetClassLoader(const ScopedObjectAccess& soa)
   REQUIRES_SHARED(Locks::mutator_lock_) {
 // 查找当前调用的java方法,此时是native线程刚绑定jvm虚拟机,所以为空
 ArtMethod* method = soa.Self()->GetCurrentMethod(nullptr);
 // If we are running Runtime.nativeLoad, use the overriding ClassLoader it set.
 if (method ==
     jni::DecodeArtMethod<kEnableIndexIds>(WellKnownClasses::java_lang_Runtime_nativeLoad)) {
   return soa.Decode<mirror::ClassLoader>(soa.Self()->GetClassLoaderOverride());
}
 // If we have a method, use its ClassLoader for context.
 // 如果不为空,那么获取方法所在的class的class loader  
 if (method != nullptr) {
   return method->GetDeclaringClass()->GetClassLoader();
}
 // 如果为空,那么获取SystemClassLoader
 ObjPtr<mirror::ClassLoader> class_loader =
     soa.Decode<mirror::ClassLoader>(Runtime::Current()->GetSystemClassLoader());
 if (class_loader != nullptr) {
   return class_loader;
}
 // See if the override ClassLoader is set for gtests.
 class_loader = soa.Decode<mirror::ClassLoader>(soa.Self()->GetClassLoaderOverride());
 if (class_loader != nullptr) {
   // If so, CommonCompilerTest should have marked the runtime as a compiler not compiling an
   // image.
   CHECK(Runtime::Current()->IsAotCompiler());
   CHECK(!Runtime::Current()->IsCompilingBootImage());
   return class_loader;
}
 // Use the BOOTCLASSPATH.
 return nullptr;
}

结合上面的代码,我们验证一下加载系统的Class是否可行,发现时可以的,没有报错。

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码void ThreadTest::callJava(void *data) {
   ThreadTest *threadTest = (ThreadTest *) data;
   JNIEnv *env= nullptr;
   threadTest->vm->AttachCurrentThread(&env, nullptr);
   jclass clazz = env->FindClass("java/lang/Object");
   jmethodID methodId = env->GetMethodID(clazz, "<init>", "()V");
   jobject obj = env->NewObject(clazz, methodId);
   jmethodID methodId2 = env->GetMethodID(clazz, "toString", "()Ljava/lang/String;");
   jstring result = (jstring)env->CallObjectMethod(obj, methodId2);
   const char *str = env->GetStringUTFChars(result, nullptr);
}
// 打印 result: java.lang.Object@e7b9a76
如何解决?

知道出现的原因后就很好解决了,我们不能在一个线程调用另一个线程的JNIEnv,所以就不能缓存有正确ClassLoader的JNIEnv,然后调用其FindClass方法。我们在java线程中初始化时就去获取出相应的jclass,进行缓存。这种方法不是很通用,我们可以在正确的线程下对ClassLoader进行缓存,然后再在另一个线程调用这个ClassLoader的loadClass方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
ini复制代码extern "C"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
   // 此时运行在一个java线程中(真正绑定了jvm环境的线程),其ClassLoader是调用loadLibrary所在的Class对应的ClassLoader,在这里就是加载MainActivity的ClassLoader
   JNIEnv* env = nullptr;
   if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
       return JNI_ERR;
  }
   threadTest = new ThreadTest(vm);
   threadTest->initClassLoader(env);
   return JNI_VERSION_1_6;
}

void ThreadTest::initClassLoader(JNIEnv *env) {
   jclass clazz = env->FindClass("com/hyc/jni_demo/TestClassLoader");
   jmethodID methodId = env->GetStaticMethodID(clazz, "getClassLoader", "()Ljava/lang/ClassLoader;");
   jobject loader = env->CallStaticObjectMethod(clazz, methodId);
   classLoader = env->NewGlobalRef(loader);
}

void ThreadTest::callJava(void *data) {
   ThreadTest *threadTest = (ThreadTest *) data;
   JNIEnv *env= nullptr;
   threadTest->vm->AttachCurrentThread(&env, nullptr);

   jclass classLoaderClass = env->GetObjectClass(threadTest->classLoader);
   jstring className = env->NewStringUTF("com.hyc.jni_demo.NativeCall");
   jmethodID loadClassMethod = env->GetMethodID(classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
   jclass clazz = (jclass)env->CallObjectMethod(threadTest->classLoader, loadClassMethod,
                                                className);
   jmethodID methodId = env->GetStaticMethodID(clazz, "callStatic", "()I");
   jint result = env->CallStaticIntMethod(clazz, methodId);
   LOGD("result1: %d", result);
   jmethodID methodId2 = env->GetMethodID(clazz, "callNormal", "()I");

   jfieldID field = env->GetStaticFieldID(clazz, "INSTANCE", "Lcom/hyc/jni_demo/NativeCall;");
   jobject nativeCall = env->GetStaticObjectField(clazz, field);
   jint result2 = env->CallIntMethod(nativeCall, methodId2, nullptr);
   LOGD("result2: %d", result2);
   threadTest->vm->DetachCurrentThread();

}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

window 安装大模型 chatglm-6b 一、 前言

发表于 2024-04-27

你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益:

  1. 了解大厂经验
  2. 拥有和大厂相匹配的技术等

希望看什么,评论或者私信告诉我!

一、 前言

有大模型以来一直想尝试通过本地安装大模型,主要的原因是GPT众所众知的原因没有办法通过 API 访问,而国内的所谓的开发平台一方面要么不兼容 openai 的 api 要么就是价格不够友好,要么两者兼有,另外的话,开发平台也有所谓的隐私问题。另外公司内部虽然有已经部署好的开源大模型,但相应的服务中的 Temperautre 或者 Top 都已经被固定了,用起来特别没意思。

所以想自己搞一套,喜欢自己说了算。

二、准备工作

2.1 电脑

在这里插入图片描述
这是我工作使用的电脑配置,16G内存,Intel(R) 集成显卡。这里要吐槽一下,程序员千万不要使用 window,随便装点什么东西就很麻烦,用不了 mac 就用 linux,我这是公司电脑,后悔没早点装 unbantu 系统

2.2 组件安装

VS studio 2022
在这里插入图片描述

cmake

TDM-GCC,注意,安装的时候直接选择全部安装就好。安装完在cmd中运行”gcc -v”测试是否成功即可( 我的电脑需要重启后才能执行 gcc -v )

2.3 开始安装

因为公司电脑性能不行,所以我选择了 ChatGLM-6B,另外ChatGLM-6B完整版本需要13GB显存做推理,但是INT4量化版本只需要6GB显存即可运行,这里选择 INT4量化版

2.3.1下载官方代码,安装Python依赖的库

首先,我们需要从GitHub上下载ChatGLM的requirements.txt来帮助我们安装依赖的库。大家只需要在GitHub上下载requirements.txt即可。下载地址:github.com/THUDM/ChatG…
在这里插入图片描述这个文件记录了ChatGLM-6B依赖的Python库及版本,大家点击右上角Code里面有Download ZIP,下载到本地解压之后就能获取这个文件。然后执行如下命令即可:

1
python复制代码  pip install -r requirements.txt

注意,这是从cmd进入到requirements.txt文件所在的目录执行的结果,这部分属于Python基础,就不赘述了。

需要注意的是,ChatGLM依赖HuggingFace的transformers库,尽管官方说:

1
复制代码使用 pip 安装依赖:pip install -r requirements.txt,其中 transformers 库版本推荐为 4.27.1,但理论上不低于 4.23.1 即可。

但是实际上,必须是4.27.1及以上的版本才可以,更低版本的transformers会出现如下错误:

AttributeError: ‘Logger’ object has no attribute “‘warning_once’”

所以,一定要查看自己的transformers版本是否正确。

另外,ChatGLM-6B依赖torch,如果你有GPU,且高于6G内存,那么建议部署GPU版本,但是需要下载支持cuda的torch,而不是默认的CPU版本的torch。具体可参考 :
关于 AssertionError: Torch not compiled with CUDA enabled 问题

2.3.2 下载INT4量化后的预训练结果文件

在上述的依赖环境安装完毕之后,大家接下来就要下载预训练结果。

INT4量化的预训练文件下载地址:huggingface.co/THUDM/chatg…,需要魔法,如果没有魔法,可去 modelscope 搜索合适的版本

需要注意的是,在GitHub上,官方提供了模型在清华云上的下载地址,但是那个只包含预训练结果文件,即bin文件,但实际上ChatGLM-6B的运行需要模型的配置文件,即config.json等,如下图所示:

因此建议大家全部从HuggingFace上下载所有文件到本地。上述文件全部下载之后保存到本地的一个目录下即可,比如:

D:\LLM

2.3.3 Windows+CPU部署方案

我的机器不支持CUDA,所以我们直接来看CPU方式

运行部署CPU版本的INT4量化的ChatGLM-6B模型

CPU版本量化模型的代码与GPU版本稍微有点差异,代码如下:

1
2
3
4
5
6
python复制代码from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("D:\LLM\chatglm-6b-int4", trust_remote_code=True, revision="")
model = AutoModel.from_pretrained("D:\LLM\chatglm-6b-int4",trust_remote_code=True, revision="").float()
model = model.eval()
response, history = model.chat(tokenizer, "你好", history=[])
print(response)

一般都会报错

在运行中遇到了如下错误提示:

1
2
3
4
yaml复制代码No compiled kernel found.
Compiling kernels : C:\Users\在、xxx\.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\quantization_kernels_parallel.c
Compiling gcc -O3 -fPIC -pthread -fopenmp -std=c99 C:\Users\xxx\.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\quantization_kernels_parallel.c -shared -o C:\Users\xxx\.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\quantization_kernels_parallel.so
Kernels compiled : C:\Users\xxx\.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\quantization_kernels_parallel.so

CPU版本的ChatGLM-6B部署比GPU版本稍微麻烦一点,主要涉及到一个kernel的编译问题。

在安装之前,除了上面需要安装好requirements.txt中所有的Python依赖外,torch需要安装好正常的CPU版本即可。

安装这个主要是为了编译之前下载的文件中的quantization_kernels.c和quantization_kernels_parallel.c这两个文件。如果大家

那么就是这两个文件编译出问题了。那么就需要我们手动去编译这两个文件:

在C:\Users\xxx.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\本地目录下进入cmd,运行如下两个编译命令:

1
2
ini复制代码gcc -fPIC -pthread -fopenmp -std=c99 quantization_kernels.c -shared -o quantization_kernels.so
gcc -fPIC -pthread -fopenmp -std=c99 quantization_kernels_parallel.c -shared -o quantization_kernels_parallel.so

如下图所示即为运行成功

然后就可以在C:\Users\xxx.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\目录下看到下面两个新的文件:quantization_kernels_parallel.so和quantization_kernels.so。说明编译成功,后面我们手动载入即可。

在原来代码的基础上添加

1
python复制代码model = model.quantize(bits=4, kernel_file="C:\Users\xxx\.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\\quantization_kernels.so")

一行手动加载的内容。

接下来你就可以看到如下界面:
我这里把 quantization_kernels.so 放在了 D:\LLM\chatglm2-6b-int4 下面了

输出结果

你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。

也就是CPU版本的ChatGLM-6B运行成功了!但很慢,所以可以考虑 CPP 加速!待后续输出!

三、总结

通过本文,读者可以了解到如何在个人电脑上部署ChatGLM-6B的INT4量化版本,同时作者也分享了他在安装过程中遇到的问题和解决方法,帮助读者顺利完成安装并运行大模型

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【AI大模型应用开发】【RAG评估】0 综述:一文了解RA

发表于 2024-04-27

大家好,我是同学小张,日常分享AI知识和实战案例,欢迎 点赞 + 关注 👏,持续学习,持续干货输出。


前面我们学习了RAG的基本框架并进行了实践,我们也知道使用它的目的是为了改善大模型在一些方面的不足:如训练数据不全、无垂直领域数据、容易出现幻觉等。那么如何评估RAG的效果呢?本文我们来了解一下。

推荐前置阅读

  • 【AI大模型应用开发】3. RAG初探 - 动手实现一个最简单的RAG应用
  • 【AI大模型应用开发】3.2 RAG实战 - RAG应用+UI实现加载本地文件并对话
  1. RAG效果评估的必要性

  • 评估出RAG对大模型能力改善的程度
  • RAG优化过程,通过评估可以知道改善的方向和参数调整的程度
  1. RAG评估方法

1.1 人工评估

最Low的方式是进行人工评估:邀请专家或人工评估员对RAG生成的结果进行评估。他们可以根据预先定义的标准对生成的答案进行质量评估,如准确性、连贯性、相关性等。这种评估方法可以提供高质量的反馈,但可能会消耗大量的时间和人力资源。

1.2 自动化评估

自动化评估肯定是RAG评估的主流和发展方向。

1.2.1.1 LangSmith

在我的这篇文章中 【AI大模型应用开发】【LangSmith: 生产级AI应用维护平台】1. 快速上手数据集与测试评估过程 介绍了如何使用LangSmith平台进行效果评估。

  • 需要准备测试数据集
  • 不仅可以评估RAG效果,对于LangChain中的Prompt模板等步骤都可进行测试评估。

在这里插入图片描述

1.2.1.2 Langfuse

Langfuse作为LangSmith的平替,也具有自动化评估的功能。在我的这篇文章中 【AI大模型应用开发】【LangFuse: LangSmith平替,生产级AI应用维护平台】0. 快速上手 - 基本功能全面介绍与实践(附代码) 介绍了如何使用Langfuse平台进行效果评估。

  • 需要准备测试数据集
  • 不仅可以评估RAG效果,对于LangChain中的Prompt模板等步骤都可进行测试评估。

在这里插入图片描述
以上两个平台对RAG的评估,都可以自定义自己的评估函数。当然其也支持一些内置的评估函数。

1.2.1.3 Trulens

TruLens是一款旨在评估和改进 LLM 应用的软件工具,它相对独立,可以集成 LangChain 或 LlamaIndex 等 LLM 开发框架。它使用反馈功能来客观地衡量 LLM 应用的质量和效果。这包括分析相关性、适用性和有害性等方面。TruLens 提供程序化反馈,支持 LLM 应用的快速迭代,这比人工反馈更快速、更可扩展。

  • 开源链接:github.com/truera/trul…
  • 使用手册:www.trulens.org/trulens_eva…

在这里插入图片描述
使用的步骤:

(1)创建LLM应用

(2)将LLM应用与TruLens连接,记录日志并上传

(3)添加 feedback functions到日志中,并评估LLM应用的质量

(4)在TruLens的看板中可视化查看日志、评估结果等

(5)迭代和优化LLM应用,选择最优的版本

其对于RAG的评估主要有三个指标:

  • 上下文相关性(context relevance):衡量用户提问与查询到的参考上下文之间的相关性
  • 忠实性(groundedness ):衡量大模型生成的回复有多少是来自于参考上下文中的内容
  • 答案相关性(answer relevance):衡量用户提问与大模型回复之间的相关性

在这里插入图片描述

其对RAG的评估不需要有提前收集的测试数据集和相应的答案。

1.2.4 RAGAS

考虑标准的RAG设置,即给定一个问题q,系统首先检索一些上下文c(q),然后使用检索到的上下文生成答案as(q)。在构建RAG系统时,通常无法访问人工标注的数据集或参考答案,因此该工作将重点放在完全独立且无参考的度量指标上。

四个指标,与Trulens的评估指标有些类似:

  • 评估检索质量:
    • context_relevancy(上下文相关性,也叫 context_precision)
    • context_recall(召回性,越高表示检索出来的内容与正确答案越相关)
  • 评估生成质量:
    • faithfulness(忠实性,越高表示答案的生成使用了越多的参考文档(检索出来的内容))
    • answer_relevancy(答案的相关性)

在这里插入图片描述

  1. 常用评估指标

在上文评估方法中已经介绍了几种常用的评估指标:

2.1 Trulens 的RAG三元组指标

  • 上下文相关性(context relevance):衡量用户提问与查询到的参考上下文之间的相关性
  • 忠实性(groundedness ):衡量大模型生成的回复有多少是来自于参考上下文中的内容
  • 答案相关性(answer relevance):衡量用户提问与大模型回复之间的相关性

2.2 RAGAS的四个指标

四个指标,与Trulens的评估指标有些类似:

  • 评估检索质量:
    • context_relevancy(上下文相关性,也叫 context_precision)
    • context_recall(召回性,越高表示检索出来的内容与正确答案越相关)
  • 评估生成质量:
    • faithfulness(忠实性,越高表示答案的生成使用了越多的参考文档(检索出来的内容))
    • answer_relevancy(答案的相关性)

2.3 其它指标

参考论文:arxiv.org/pdf/2309.01…

(1)噪声鲁棒性(Noise Robustness)

衡量从噪声文档中提取有用的信息能力。在现实世界中,存在大量的噪声信息,例如假新闻,这给语言模型带来了挑战。

(2)否定拒绝(Negative Rejection)

当检索到的文档不足以支撑回答用户的问题时,模型应拒绝回答问题,发出”信息不足”或其他拒绝信号。

(3)信息整合(information integration)

评估模型能否回答需要整合多个文档信息的复杂问题,即,当一个问题需要查找多个文档,综合信息之后才能回答时,模型的表现。

(4)反事实鲁棒性(CounterfactualRobustness)

模型能否识别检索文档中已知事实错误的能力,即当索引的文档信息原本就是与事实相背时,大模型能否识别出不对。

在这里插入图片描述

  1. 总结

本文主要总结了当前比较流行的评估方法和指标。当前AI技术的快速发展,RAG和RAG评估是当前比较有前景的发展方向,不断有新的评估工具和理论被提出,让我们持续跟进,了解这些工具和理论,从而在使用时知道如何选择。

参考

  • mp.weixin.qq.com/s/Si8rb0L1u…
  • mp.weixin.qq.com/s/z18J2l_b-…
  • mp.weixin.qq.com/s/YFji1s2yT…
  • mp.weixin.qq.com/s/TrXWXkQIY…
  • maimai.cn/article/det…

如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~


  • 大家好,我是同学小张,日常分享AI知识和实战案例
  • 欢迎 点赞 + 关注 👏,持续学习,持续干货输出。
  • +v: jasper_8017 一起交流💬,一起进步💪。
  • 微信公众号也可搜【同学小张】 🙏

本站文章一览:

在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

没有前端能抵抗住的酷炫效果,带你用Threejs Shad

发表于 2024-04-27

前言

上一篇文章「手撸一个星系,送给心爱的姑娘!(Three.js Shader 粒子系统实现)- 牛衣古柳 - 20240417」里古柳带大家用 Three.js Shader 粒子系统实现了这个非常漂亮的星系效果。文章在上周三发布后上了掘金热榜,并且截至目前点赞数、收藏数双双破百,成为本 Shader 系列教程里阅读量最高的一篇(马上破万)。果然大家更喜欢看这种实现完整、实际、酷炫效果的文章,这倒是和我设想的一样。

  • 链接:actium.co.jp/
  • 链接:codepen.io/GuLiu/pen/W…

正好上篇讲到粒子系统,我想不妨继续趁热打铁讲解下这个带我入坑 Shader 的效果 Pepyaka,想来这应该也是很多前端、程序员梦寐以求想要实现的酷炫效果吧。下图是本文最终实现的效果,GIF 不够清晰,大家可以去 Codepen 查看源码和效果,代码后续也会同步到 GitHub。

  • 链接:codepen.io/GuLiu/pen/L…
  • 链接:github.com/DesertsX/th…

Pepyaka 这个效果本来出自于 Grant Yi 的个人网站,其实我一直不知道这个词到底啥意思,但就当成该效果的代名词这么叫着。可惜原网站换成了这个效果,虽然仍是 Shader 实现、同样酷炫,但看不到 Pepyaka 还是可惜。

  • 链接:www.grantyi.com/

在「断更19个月,携 Three.js Shader 归来!(上)- 牛衣古柳 - 20230416」一文里我提过,当初入门 Three.js 后因为对粒子系统感兴趣,于是在油管搜教程,然后看到 Yuri Artiukh 复现 Pepyaka 的教程——「#s3e6 ALL YOUR HTML, Making Pepyaka with Three.js - 20191201」——在视频里残存的片段里窥见到 Pepyaka 如此丝滑、酷炫、漂亮的效果,于是入坑 Shader,再然后有了现在输出 Shader 教程这桩事。

时隔两年古柳终于可以在原视频的基础上融入自己会的一些 Shader 效果,更近一步实现出更贴近原作的各种效果,并通过文章教给大家,让大家也能上手实现这样酷炫的效果。

中心球体

闲言少叙,进入正题。让我们同样从线框模式下的白色球体开始讲起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
js复制代码import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

let w = window.innerWidth;
let h = window.innerHeight;

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(75, w / h, 0.01, 1000);
camera.position.set(0, 0, 4);
camera.lookAt(new THREE.Vector3());

const renderer = new THREE.WebGLRenderer({
antialias: true,
// alpha: true,
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(w, h);
renderer.setClearColor(0x0a0a0f, 1);
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);

const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);

const vertexShader = /* GLSL */ `
uniform float uTime;

void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

const fragmentShader = /* GLSL */ `
void main() {
gl_FragColor = vec4(vec3(1.0), 1.0);
}
`;

const sphereMaterial = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTime: { value: 0 },
},
// wireframe: true,
});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphere);

const clock = new THREE.Clock();
function render() {
const time = clock.getElapsedTime();
sphereMaterial.uniforms.uTime.value = time;
sphere.rotation.y = time;
renderer.render(scene, camera);
requestAnimationFrame(render);
}

render();

顶点偏移

接着我们用 noise 噪声函数对顶点位置进行偏移从而改变球体形状、并用 noise 值控制 HSV 颜色模式里的 Hue 色相值,这些内容在「手把手带你入门 Three.js Shader 系列(六) - 牛衣古柳 - 20231220」、「手把手带你入门 Three.js Shader 系列(七) - 牛衣古柳 - 20240206」两篇文章里已经详细地讲过,大家可以去自行学习。

这里简单过一遍,我们谷歌搜索 GLSL noise function,从这里拷贝 Simplex 4D Noise 函数,接着对每个顶点产生一个 noise 数值(不同 noise 函数返回的值范围可能是0-1,也可能是-1-1,大家可以用第七篇里讲的方法去查看,这里不太重要也就不带大家看了),将该数值乘上法线 normal 作为在该偏移方向上偏移的程度,再加上原始 position 就是偏移后的顶点坐标。

  • 链接:gist.github.com/patriciogon…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
C#复制代码// vertex shader
uniform float uTime;

// Simplex 4D Noise
// by Ian McEwan, Ashima Arts
vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
float permute(float x){return floor(mod(((x*34.0)+1.0)*x, 289.0));}
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
float taylorInvSqrt(float r){return 1.79284291400159 - 0.85373472095314 * r;}

vec4 grad4(float j, vec4 ip){
const vec4 ones = vec4(1.0, 1.0, 1.0, -1.0);
vec4 p,s;

p.xyz = floor( fract (vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0;
p.w = 1.5 - dot(abs(p.xyz), ones.xyz);
s = vec4(lessThan(p, vec4(0.0)));
p.xyz = p.xyz + (s.xyz*2.0 - 1.0) * s.www;

return p;
}

float snoise(vec4 v){
const vec2 C = vec2( 0.138196601125010504, // (5 - sqrt(5))/20 G4
0.309016994374947451); // (sqrt(5) - 1)/4 F4
// First corner
vec4 i = floor(v + dot(v, C.yyyy) );
vec4 x0 = v - i + dot(i, C.xxxx);

// Other corners

// Rank sorting originally contributed by Bill Licea-Kane, AMD (formerly ATI)
vec4 i0;

vec3 isX = step( x0.yzw, x0.xxx );
vec3 isYZ = step( x0.zww, x0.yyz );
// i0.x = dot( isX, vec3( 1.0 ) );
i0.x = isX.x + isX.y + isX.z;
i0.yzw = 1.0 - isX;

// i0.y += dot( isYZ.xy, vec2( 1.0 ) );
i0.y += isYZ.x + isYZ.y;
i0.zw += 1.0 - isYZ.xy;

i0.z += isYZ.z;
i0.w += 1.0 - isYZ.z;

// i0 now contains the unique values 0,1,2,3 in each channel
vec4 i3 = clamp( i0, 0.0, 1.0 );
vec4 i2 = clamp( i0-1.0, 0.0, 1.0 );
vec4 i1 = clamp( i0-2.0, 0.0, 1.0 );

// x0 = x0 - 0.0 + 0.0 * C
vec4 x1 = x0 - i1 + 1.0 * C.xxxx;
vec4 x2 = x0 - i2 + 2.0 * C.xxxx;
vec4 x3 = x0 - i3 + 3.0 * C.xxxx;
vec4 x4 = x0 - 1.0 + 4.0 * C.xxxx;

// Permutations
i = mod(i, 289.0);
float j0 = permute( permute( permute( permute(i.w) + i.z) + i.y) + i.x);
vec4 j1 = permute( permute( permute( permute (
i.w + vec4(i1.w, i2.w, i3.w, 1.0 ))
+ i.z + vec4(i1.z, i2.z, i3.z, 1.0 ))
+ i.y + vec4(i1.y, i2.y, i3.y, 1.0 ))
+ i.x + vec4(i1.x, i2.x, i3.x, 1.0 ));
// Gradients
// ( 7*7*6 points uniformly over a cube, mapped onto a 4-octahedron.)
// 7*7*6 = 294, which is close to the ring size 17*17 = 289.

vec4 ip = vec4(1.0/294.0, 1.0/49.0, 1.0/7.0, 0.0) ;

vec4 p0 = grad4(j0, ip);
vec4 p1 = grad4(j1.x, ip);
vec4 p2 = grad4(j1.y, ip);
vec4 p3 = grad4(j1.z, ip);
vec4 p4 = grad4(j1.w, ip);

// Normalise gradients
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
p4 *= taylorInvSqrt(dot(p4,p4));

// Mix contributions from the five corners
vec3 m0 = max(0.6 - vec3(dot(x0,x0), dot(x1,x1), dot(x2,x2)), 0.0);
vec2 m1 = max(0.6 - vec2(dot(x3,x3), dot(x4,x4) ), 0.0);
m0 = m0 * m0;
m1 = m1 * m1;
return 49.0 * ( dot(m0*m0, vec3( dot( p0, x0 ), dot( p1, x1 ), dot( p2, x2 )))
+ dot(m1*m1, vec2( dot( p3, x3 ), dot( p4, x4 ) ) ) ) ;
}

void main() {
float noise = snoise(vec4(position, 0.0));
vec3 newPos = position + 0.8 * normal * noise;
// gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}

这里用接收 vec4 的 snoise 函数方便后续把 uTime 作为第4个参数,让形状动起来。

用法线 normal 作为颜色方便看变形后的形状。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
C#复制代码// vertex shader
uniform float uTime;
varying vec3 vNormal;

float snoise(vec4 c){
// ...
}

void main() {
vNormal = normal;

float noise = snoise(vec4(position, 0.0));
vec3 newPos = position + 0.8 * normal * noise;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}

// fragment shader
varying vec3 vNormal;

void main() {
// gl_FragColor = vec4(vec3(1.0), 1.0);
gl_FragColor = vec4(vNormal, 1.0);
}

给 position 乘不同值,改变传给 noise 函数的顶点相邻程度,使形状变化更加“剧烈”。

1
2
3
C#复制代码// float noise = snoise(vec4(position * 1.0, 0.0));
// float noise = snoise(vec4(position * 0.3, 0.0));
float noise = snoise(vec4(position * 10.0, 0.0));

增加球体细分数,使球体上有更多顶点可以被用来偏移位置。

1
2
js复制代码// const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
const sphereGeometry = new THREE.SphereGeometry(1, 200, 200);

将 uTime 作为第4个参数使形状实时发生变化。

1
C#复制代码float noise = snoise(vec4(position * 10.0, uTime * 0.2));

noise 值作为颜色

noise 数值除了用来偏移顶点坐标,还能用来设置颜色。将 vec3(noise) 灰度值颜色作为 vColor 传到片元着色器进行使用,那么 noise 值越大偏移高度越高、颜色越白;反之值越小高度越低、颜色越黑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
C#复制代码// vertex shader
uniform float uTime;
varying vec3 vNormal;
varying vec3 vColor;

void main() {
vNormal = normal;

float noise = snoise(vec4(position * 10.0, uTime * 0.2));
vColor = vec3(noise);
vec3 newPos = position + 0.8 * normal * noise;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}

// fragment shader
varying vec3 vNormal;
varying vec3 vColor;

void main() {
// gl_FragColor = vec4(vec3(1.0), 1.0);
// gl_FragColor = vec4(vNormal, 1.0);
gl_FragColor = vec4(vColor, 1.0);
}

也可以设置成 rgb 里的 red 值,就是红黑效果。

1
C#复制代码vColor = vec3(noise, 0.0, 0.0);

还可以把 noise 值设置到 HSV 模式里的 hue 色相值(第七篇里都讲过,不过那里用的 HSL 这里用的 HSV,需要注意 HSV=HSB!=HSL),然后转换回 rgb 模式。谷歌搜索 glsl hsv2rgb function 找到现成的实现,拷贝后就能使用,直接把 noise 作为 hue 会是五彩斑斓的效果,因为 noise 值0-1的话会把所有色相值覆盖到。虽然也挺好看,但不是我们这里想要的。

  • 链接:gist.github.com/983/e170a24…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
C#复制代码vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

void main() {
vNormal = normal;

float noise = snoise(vec4(position * 10.0, uTime * 0.2));
// vColor = vec3(noise);
// vColor = vec3(noise, 0.0, 0.0);
vColor = hsv2rgb(vec3(noise, 1.0, 1.0));
vec3 newPos = position + 0.8 * normal * noise;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}

缩小 noise 范围,使颜色变化不那么剧烈。

1
C#复制代码vColor = hsv2rgb(vec3(vNoise * 0.1, 1.0, 1.0));

最后微调 hue,改下饱和度,至效果满意即可。

1
C#复制代码vColor = hsv2rgb(vec3(noise * 0.1 + 0.04, 0.8, 1.0));

球形粒子系统

中心的效果完成后,接着在外面加一层球形的粒子,之前有群友在学第七篇时就有问到,这次终于可以讲解下(欢迎加我「xiaoaizhj」,备注「可视化加群」,一起交流)。

上一篇文章「手撸一个星系,送给心爱的姑娘!(Three.js Shader 粒子系统实现)- 牛衣古柳 - 20240417」对粒子系统已经做过介绍,这里就不再重复。

我们通过 BufferGeometry() 设置粒子的 position,使 radius 稍大于中心球体,这样能包裹着球体。这里想要粒子在球体上均匀分布,从网上找现成的公式即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码const particleGeometry = new THREE.BufferGeometry();

const N = 4000;
const positions = new Float32Array(N * 3);

const inc = Math.PI * (3 - Math.sqrt(5));
const off = 2 / N;
const radius = 2;

for (let i = 0; i < N; i++) {
const y = i * off - 1 + off / 2;
const r = Math.sqrt(1 - y * y);
const phi = i * inc;

positions[3 * i] = radius * Math.cos(phi) * r;
positions[3 * i + 1] = radius * y;
positions[3 * i + 2] = radius * Math.sin(phi) * r;
}

particleGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);

视频里用的是某博客文章里的公式,由于文章已看不到,这里放截图方便感兴趣的小伙伴看眼,总之把这里 python 代码改成上面 JS 代码即可。

我们也可以搜 evenly distribute points on a sphere、fibonacci spiral sphere 等关键词,能找到其他大同小异的实现方式,下面是另一种方案,作为参考,可以看到效果都差不多。后续演示仍沿用第一种方案。

  • 链接:medium.com/@vagnerseib…
1
2
3
4
5
6
7
8
9
10
11
js复制代码// 另一种生成球体上均匀粒子坐标的方式
for (let i = 0; i < N; i++) {
const k = i + 0.5;
const phi = Math.acos(1 - (2 * k) / N);
const theta = Math.PI * (1 + Math.sqrt(5)) * k;
const x = Math.cos(theta) * Math.sin(phi) * radius;
const y = Math.sin(theta) * Math.sin(phi) * radius;
const z = Math.cos(phi) * radius;

positions.set([x, y, z], i * 3);
}

材质用 ShaderMaterial,粒子颜色设置了透明度,然后和 particleGeometry 一起丢给 Points 就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
js复制代码const particleVertex = /* GLSL */ `
uniform float uTime;

void main() {
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = 6.0 / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}
`;

const particleFragment = /* GLSL */ `
void main() {
// gl_FragColor = vec4(vec3(1.0), 1.0);
gl_FragColor = vec4(vec3(1.0), 0.6);
}
`;

const particleMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
},
vertexShader: particleVertex,
fragmentShader: particleFragment,
transparent: true,
blending: THREE.AdditiveBlending,
});

const particles = new THREE.Points(particleGeometry, particleMaterial);
scene.add(particles);

function render() {
// ...
sphereMaterial.uniforms.uTime.value = time;
particleMaterial.uniforms.uTime.value = time;
}

不设置透明度的话效果如下。还是设置透明度、弱化粒子视觉效果后看着更舒服。

1
C#复制代码gl_FragColor = vec4(vec3(1.0), 1.0);

粒子上下波动

接着让粒子运动起来,可以通过对 y 坐标取 sin 值再加回到 y 上,这样同一高度的粒子会一起随 sin 波浪上下偏移,整体上就是波浪的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
C#复制代码// particleVertex
uniform float uTime;

void main() {

vec3 newPos = position;
newPos.y += 0.1 * sin(newPos.y * 6.0 + uTime);
// newPos.z += 0.05 * sin(newPos.y * 10.0 + uTime);

// vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);
gl_PointSize = 6.0 / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}

还可以对 z 坐标进行一定偏移,虽然这里可能效果不明显,但常常会组合的对 xyz 进行 sin/cos 等操作,以后大家也会碰到(实际上后文就多次重复用到),所以这里先演示下。大家也可以自由发挥、随意尝试,不必局限本文所讲到的方法。

1
C#复制代码newPos.z += 0.05 * sin(newPos.y * 10.0 + uTime);

效果已经很漂亮了,大家可以休息下,喝杯奶茶或咖啡,好好欣赏享受下自己的成果。

背景用随机粒子进行点缀

上面都是油管教程里涉及到的内容,休息结束后,这次让我们加个餐、更进一步把原作里其他一些效果也简单实现下。

首先可以看到背景有点空,我们可以用随机粒子进行点缀,丰富画面效果。

通过 r 使得粒子在中心球体和球形粒子之外的范围,xyz 坐标随机在立方体空间内分布,这里都是简单的设置,所以怎么方便怎么来;sizes 控制每个粒子的随机大小并且会用作粒子移动速度,为了使值不为0,这样粒子速度不为0就不会停着不动,所以加了0.4。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码const firefliesGeometry = new THREE.BufferGeometry();
const firefliesCount = 300;
const positions1 = new Float32Array(firefliesCount * 3);
const sizes = new Float32Array(firefliesCount);

for (let i = 0; i < firefliesCount; i++) {
const r = Math.random() * 5 + 5;
positions1[i * 3 + 0] = (Math.random() - 0.5) * r;
positions1[i * 3 + 1] = (Math.random() - 0.5) * r;
positions1[i * 3 + 2] = (Math.random() - 0.5) * r;

sizes[i] = Math.random() + 0.4;
}

firefliesGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions1, 3)
);
firefliesGeometry.setAttribute("aSize", new THREE.BufferAttribute(sizes, 1));

顶点着色器里 gl_PointSize 乘上 aSize 改变大小,片元着色器里用每个粒子离中心的距离通过一个反比例函数 0.05/d-0.05*2.0 使得靠近中心为1,往外逐渐变成0,再设置成透明度值,从而实现出光斑、模糊圆形的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
js复制代码const firefliesVertexShader = /* GLSL */ `
uniform float uTime;
attribute float aSize;

void main() {
vec3 newPos = position;
// newPos.y += sin(uTime * 0.5 + newPos.x * 100.0) * aSize * 0.2;
// newPos.x += sin(uTime * 0.5 + newPos.x * 200.0) * aSize * 0.1;
vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);
gl_PointSize = 70.0 * aSize / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}
`;

const firefliesFragmentShader = /* GLSL */ `
void main() {
float d = length(gl_PointCoord - vec2(0.5));
float strength = clamp(0.05 / d - 0.05 * 2.0, 0.0, 1.0);
gl_FragColor = vec4(vec3(1.0), strength);
}
`;

const firefliesMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
},
vertexShader: firefliesVertexShader,
fragmentShader: firefliesFragmentShader,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
});

const fireflies = new THREE.Points(firefliesGeometry, firefliesMaterial);
scene.add(fireflies);

通过曲线图看看透明度 strength 计算方式,当距离d=0.5时数值为0也就是圆圈边缘;d>0.5时数值为负数,通过clamp会变成0,完全透明;d=0.05/1.1=0.045时数值为1;d<0.045时数值大于1会被clamp取1,完全不透明,距离从0.045到0.5透明度快速降为0,从而实现模糊效果。

1
2
3
C#复制代码float d = length(gl_PointCoord - vec2(0.5));
float strength = clamp(0.05 / d - 0.05 * 2.0, 0.0, 1.0);
gl_FragColor = vec4(vec3(1.0), strength);

这种粒子实现方式还蛮常见。记得2022年4-5月份刚接触 shader 那会,碰上浙大125周年校庆,发现当年很特别官方搞了个求是星海的网站效果,就是粒子系统为主,里面粒子颜色透明度就是用这种反比例函数实现。

通过 Chrome 浏览器的 Spector.js 插件就能看到这里粒子的着色器里的 starIntensity() 函数,就是如此实现的。

在该网站输入校友专业和名字、认证通过后会生成每个校友专属的由“灿若繁星”的浙大人姓名的粒子系统组成的效果。网页还在,但需要输入信息才能生成,所以非校友的话看不到,只能看这里截图。

  • 链接:125.zju.edu.cn/universe

扯回来,和上面球形粒子一样这里用 sin 函数使背景随机粒子也运动起来,参数可以任意修改看效果。

1
2
3
C#复制代码vec3 newPos = position;
newPos.y += sin(uTime * 0.5 + newPos.x * 100.0) * aSize * 0.2;
newPos.x += sin(uTime * 0.5 + newPos.x * 200.0) * aSize * 0.1;

显示文字

接着模仿原作在中心球体和球形粒子之间放上文字。这里通过在长度2宽度1的 Plane 上显示纹理图片实现,准备一张白色文字、背景透明、1024x512 的图片。下面是截图的效果,而不是原图,不要保存这张图去直接使用。原图已传到 postimg 这个网站,下面是链接直接用就行。如果后续想将本文的效果修改后放自己的个人网站,那么这里就可以和原作一样换成自己的名字。

  • 链接:i.postimg.cc/nrSTmrZk/te…

将图片通过 TextureLoader().load() 加载后作为 uTexture 传给 shader,然后通过 uv 采样纹理图,再搭配 textMaterial 设置 transparent 即可显示文字。text mesh 通过设置 position.z=1.7 移动到所需位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
js复制代码const textGeometry = new THREE.PlaneGeometry(2, 1, 100, 100);

const textVertex = /* GLSL */ `
uniform float uTime;
varying vec2 vUv;

void main() {
vUv = uv;

vec3 newPos = position;
// newPos.y += 0.06 * sin(newPos.x + uTime);
// newPos.x += 0.1 * sin(newPos.x * 2.0 + uTime);
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}
`;

const textFragment = /* GLSL */ `
uniform sampler2D uTexture;
varying vec2 vUv;

void main() {
vec4 color = texture2D(uTexture, vUv);
gl_FragColor = color;
}
`;

const textMaterial = new THREE.ShaderMaterial({
vertexShader: textVertex,
fragmentShader: textFragment,
uniforms: {
uTime: { value: 0 },
uTexture: {
value: new THREE.TextureLoader().load("https://i.postimg.cc/nrSTmrZk/text.png"), // './assets/text.png',
},
},
transparent: true,
});

const text = new THREE.Mesh(textGeometry, textMaterial);
text.position.z = 1.7;
scene.add(text);

function render () {
// ...
sphereMaterial.uniforms.uTime.value = time;
particleMaterial.uniforms.uTime.value = time;
firefliesMaterial.uniforms.uTime.value = time;
textMaterial.uniforms.uTime.value = time;
}

接着再一次应用 sin 函数改变顶点位置使 plane 发生扭曲,顶点移动后 uv 也随之移动,用 uv 采样纹理后就会有文字飘浮的效果。

1
2
3
C#复制代码vec3 newPos = position;
newPos.y += 0.06 * sin(newPos.x + uTime);
newPos.x += 0.1 * sin(newPos.x * 2.0 + uTime);

上方光线

最后古柳观察到原作顶部有光线效果,于是也顺带实现了下,不过因为文章篇幅已经不短,加上这部分解释起来比前几个效果复杂些,就暂时不在本文讲了,后续其他文章有机会再讲。大家这里看眼效果即可,感兴趣可自行实现。

小结

至此,古柳带大家把这个自己入坑 shader 的酷炫效果“简单”复现了下,中间偷懒多次用了 sin 函数,所以其实并没有大家想的那么复杂。

虽然和原作比起来还是很粗陋,各种参数、粒子动画、颜色搭配都有很大优化空间,效果远远不如原作漂亮,但是作为一篇教程里的例子,或许已足够让大家学到些东西,其他的优化就留给大家去自由发挥了。

还是可惜原作早就看不到了,只能从油管视频里瞥见一些片段效果,无法自己去交互、去体验、去学习。但两年前初见 Pepyaka、初识 shader 时的那份惊艳却伴随古柳至今,希望更多人看到本文后也能感受到那份惊艳,并能借本教程之力让那份惊艳不再局限于观赏,而是可以自己去一步步实现、一步步靠近,相信大家跟着古柳的系列文章一点点学下来,就不会再觉得这样的效果是自己无法理解、无法实现的了吧。

最后完整源码可见 Codepen。

  • 链接:codepen.io/GuLiu/pen/L…

相关阅读

「手把手带你入门 Three.js Shader 系列」目录如下:

  • 「断更19个月,携 Three.js Shader 归来!(上)- 牛衣古柳 - 20230416」
  • 「断更19个月,携 Three.js Shader 归来!(下)- 牛衣古柳 - 20230421」
  • 「手撸一个星系,送给心爱的姑娘!(Three.js Shader 粒子系统实现)- 牛衣古柳 - 20240417」
  • 「手把手带你入门 Three.js Shader 系列(八)- 牛衣古柳 - 20240229」
  • 「手把手带你入门 Three.js Shader 系列(七)- 牛衣古柳 - 20230206」
  • 「手把手带你入门 Three.js Shader 系列(六)- 牛衣古柳 - 20231220」
  • 「手把手带你入门 Three.js Shader 系列(五)- 牛衣古柳 - 20231126」
  • 「手把手带你入门 Three.js Shader 系列(四)- 牛衣古柳 - 20231121」
  • 「手把手带你入门 Three.js Shader 系列(三)- 牛衣古柳 - 20230725」
  • 「手把手带你入门 Three.js Shader 系列(二)- 牛衣古柳 - 20230716」
  • 「手把手带你入门 Three.js Shader 系列(一)- 牛衣古柳 - 20230515」

照例

如果你喜欢本文内容,欢迎以各种方式支持,这也是对古柳输出教程的一种正向鼓励!

最后欢迎加入「可视化交流群」,进群多多交流,对本文任何地方有疑惑的可以群里提问。加古柳微信:xiaoaizhj,备注「可视化加群」即可。

欢迎关注古柳的公众号「牛衣古柳」,并设置星标,以便第一时间收到更新。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

多线程(75)乐观读策略

发表于 2024-04-27

乐观读策略是一种常见的并发控制方法,尤其在读多写少的场景中表现出色。这种策略基于一个假设:冲突发生的概率很低,所以在大多数情况下,无需在读操作时加锁。乐观读策略通常通过版本号或时间戳来实现,每次写操作会改变这个版本号或时间戳,而读操作会检查这个版本号或时间戳以确保数据的一致性。

下面以一个简单的使用版本号实现的乐观读策略为例,通过Java代码来演示这一概念。

示例代码

假设有一个共享资源Resource,这个资源有一个数据字段和一个版本号字段。每次写操作都会更新数据和版本号,而读操作则需要检验版本号以确认在读取数据过程中数据没有被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
java复制代码public class OptimisticReadExample {
private static class Resource {
private volatile int data = 0; // 共享资源数据
private volatile int version = 0; // 版本号

// 写操作
public synchronized void write(int newData) {
data = newData;
version++; // 写操作更新版本号
}

// 乐观读操作
public int read() {
int currentVersion = version;
int result = data; // 读取数据
// 检查在读取数据之后版本号是否发生了变化
if (currentVersion != version) {
throw new IllegalStateException("Data was modified during reading");
}
return result;
}
}

public static void main(String[] args) {
final Resource resource = new Resource();

// 模拟写操作
new Thread(() -> {
for (int i = 0; i < 5; i++) {
resource.write(i);
System.out.println("Write: " + i);
try {
Thread.sleep(100); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();

// 模拟读操作
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
System.out.println("Read: " + resource.read());
} catch (IllegalStateException e) {
System.out.println("Read failed due to modification");
}
try {
Thread.sleep(50); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
}

在这个例子中,如果在读取数据的过程中数据被另一个线程修改(即版本号发生变化),则抛出IllegalStateException。这就是乐观读策略的核心:它假设在大多数情况下不会发生冲突,所以不加锁,但通过版本号检查来确保数据的一致性。

特点与考虑

  • 性能优势:乐观读在读多写少的场景中可以显著提高性能,因为它减少了锁的使用。
  • 冲突处理:当检测到版本冲突时,需要有相应的策略来处理,比如重试读操作、回滚操作等。
  • 适用场景:乐观读策略特别适用于读操作远远多于写操作的场景。

实际应用

在实际应用中,许多高性能并发库和框架使用了乐观读策略,例如Java的java.util.concurrent包中的StampedLock就提供了一种基于时间戳的锁机制,支持乐观读锁。

乐观读策略是理解现代并发控制技术的一个重要环节。通过合理使用乐观读策略,可以在保证数据一致性的同时,显著提高应用程序的并发性能。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux平台Unity下RTMP RTSP低延迟播放器技术

发表于 2024-04-27

技术背景

国产操作系统对于确保信息安全、促进技术创新、满足特定需求以及推动经济发展等方面都具有重要意义,多以Linux为基础二次开发。2014年4月8日起,美国微软公司停止了对Windows XP SP3操作系统提供支持,这引起了社会和广大用户的广泛关注和对信息安全的担忧。

国产操作系统具有诸多好处:

  1. 信息安全与可控性提升:国产操作系统在设计和开发过程中,可以更加注重国内的信息安全标准和需求,通过自主研发,能够避免对外部系统的过度依赖,降低数据泄露和信息安全风险。此外,由于源代码掌握在自己手中,国家可以更好地控制操作系统的安全漏洞和后门问题,从而提高整个信息系统的安全可控性。
  2. 促进技术创新与产业发展:发展国产操作系统有助于推动国内软件技术的自主创新,提升整个软件产业的核心竞争力。通过自主研发,可以打破国外操作系统的技术垄断,掌握核心技术,为国内软件产业的发展提供有力支撑。同时,这也将促进相关产业链的发展,包括硬件、中间件、应用软件等,形成良性的产业生态。
  3. 满足特定需求与提升用户体验:国产操作系统可以根据国内用户的特定需求进行定制开发,提供更加符合国内使用习惯的服务和功能。这不仅可以提高用户的满意度和忠诚度,还可以为政府、企业等用户提供更加安全、高效、便捷的信息化解决方案。
  4. 培育新的经济增长点:随着数字经济的快速发展,操作系统作为数字基础设施的重要组成部分,具有巨大的市场潜力和商业价值。发展国产操作系统有助于培育新的经济增长点,推动国内软件产业的快速发展,为国家经济发展注入新的动力。
  5. 增强国家信息安全战略地位:在信息化时代,信息安全已成为国家安全的重要组成部分。发展国产操作系统有助于提升国家信息安全战略地位,增强国家在网络空间的话语权和影响力。

在发布国产操作系统|Linux平台的RTMP|RTSP直播播放SDK之前,大牛直播SDK(官方)在Windows、Android、iOS平台已经有了非常成熟的技术积累,功能齐全、稳定性高、超低延迟、超低资源占用,推进到Linux平台是顺理成章的。国产操作系统和Linux上的RTMP|RTSP直播播放模块,视频绘制使用XLib相关库实现, 音频输出使用PulseAudio和Alsa Lib,除了常规功能如实时静音、快照、buffer time设定、网络自动重连等,RTMP支持扩展H265播放, RTSP也支持H265播放。

Linux原生的RTSP、RTMP播放模块这里我们不做赘述,本文主要讲的是如何在Linux平台构建Unity下的RTSP和RTMP低延迟直播播放。

技术实现

国产操作系统和Linux平台下,Unity环境的播放器,和Windows、Android、iOS平台基础流程并无大的差异,简单来说,通过调用原生的播放模块,回调解码后的YUV或RGB数据,投递到Unity侧,在Unity下完成绘制,这里就需要原生的RTMP、RTSP播放模块,拉流解码延迟非常低,数据投递效率非常高,无图无真相:

Linux平台,我们是回调的YUV的数据,也就是 NT_SP_E_VIDEO_FRAME_FROMAT_I420:

1
2
3
4
5
6
7
csharp复制代码        /*定义视频帧图像格式*/
public enum NT_SP_E_VIDEO_FRAME_FORMAT : uint
{
NT_SP_E_VIDEO_FRAME_FORMAT_RGB32 = 1, // 32位的rgb格式, r, g, b各占8, 另外一个字节保留, 内存字节格式为: bb gg rr xx, 主要是和windows位图匹配, 在小端模式下,按DWORD类型操作,最高位是xx, 依次是rr, gg, bb
NT_SP_E_VIDEO_FRAME_FORMAT_ARGB = 2, // 32位的argb格式,内存字节格式是: bb gg rr aa 这种类型,和windows位图匹配
NT_SP_E_VIDEO_FRAME_FROMAT_I420 = 3, // YUV420格式, 三个分量保存在三个面上
}

开始播放之前,把回调设置下去:

1
2
3
4
5
6
ini复制代码//video frame callback (YUV/RGB)
videoctrl[sel].sdk_video_frame_call_back_ = new VideoControl.SetVideoFrameCallBack(SDKVideoFrameCallBack);
videoctrl[sel].video_frame_call_back_ = new SP_SDKVideoFrameCallBack(NT_SP_SetVideoFrameCallBack);
NTSmartPlayerSDK.NT_SP_SetVideoFrameCallBack(videoctrl[sel].player_handle_, (Int32)NT.NTSmartPlayerDefine.NT_SP_E_VIDEO_FRAME_FORMAT.NT_SP_E_VIDEO_FRAME_FROMAT_I420, window_handle_, videoctrl[sel].video_frame_call_back_);

UInt32 flag = NTSmartPlayerSDK.NT_SP_StartPlay(videoctrl[sel].player_handle_);

视频帧结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
csharp复制代码    /*定义视频帧结构.*/
[StructLayoutAttribute(LayoutKind.Sequential)]
public struct NT_SP_VideoFrame
{
public Int32 format_; // 图像格式, 请参考NT_SP_E_VIDEO_FRAME_FORMAT
public Int32 width_; // 图像宽
public Int32 height_; // 图像高

public Int64 timestamp_; // 时间戳, 一般是0,不使用, 以ms为单位的

//具体的图像数据, argb和rgb32只用第一个, I420用前三个
public IntPtr plane0_;
public IntPtr plane1_;
public IntPtr plane2_;
public IntPtr plane3_;

// 每一个平面的每一行的字节数,对于argb和rgb32,为了保持和windows位图兼容,必须是width_*4
// 对于I420, stride0_ 是y的步长, stride1_ 是u的步长, stride2_ 是v的步长,
public Int32 stride0_;
public Int32 stride1_;
public Int32 stride2_;
public Int32 stride3_;
}

开始播放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
ini复制代码/*
* SmartPlayerLinuxMono.cs
* Author: daniusdk.com
*/
public void StartPlayer(int sel)
{
Debug.Log("StartPlayer++, sel: " + sel);

if (videoctrl[sel].is_playing_)
{
Debug.Log("StartPlayer, already started.. sel: " + sel);
return;
}

lock (videoctrl[sel].frame_lock_)
{
videoctrl[sel].cur_video_frame_ = null;
}

if (!OpenPlayerHandle(sel))
{
Debug.LogError("call OpenPlayerHandle failed, sel:" + sel);
return;
}

//video frame callback (YUV/RGB)
videoctrl[sel].sdk_video_frame_call_back_ = new VideoControl.SetVideoFrameCallBack(SDKVideoFrameCallBack);
videoctrl[sel].video_frame_call_back_ = new SP_SDKVideoFrameCallBack(NT_SP_SetVideoFrameCallBack);
NTSmartPlayerSDK.NT_SP_SetVideoFrameCallBack(videoctrl[sel].player_handle_, (Int32)NT.NTSmartPlayerDefine.NT_SP_E_VIDEO_FRAME_FORMAT.NT_SP_E_VIDEO_FRAME_FROMAT_I420, window_handle_, videoctrl[sel].video_frame_call_back_);

UInt32 flag = NTSmartPlayerSDK.NT_SP_StartPlay(videoctrl[sel].player_handle_);

if (flag == DANIULIVE_RETURN_OK)
{
videoctrl[sel].is_need_get_frame_ = true;
Debug.Log("NT_SP_StartPlay succeed, sel:" + sel);
}
else
{
NTSmartPlayerSDK.NT_SP_Close(videoctrl[sel].player_handle_);
videoctrl[sel].player_handle_ = IntPtr.Zero;

videoctrl[sel].is_need_get_frame_ = false;
Debug.LogError("NT_SP_StartPlay failed, sel:" + sel);
}

videoctrl[sel].is_playing_ = true;
}

其中,调用的OpenPlayerHandle()实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
ini复制代码/*
* SmartPlayerLinuxMono.cs
* Author: daniusdk.com
*/
private bool OpenPlayerHandle(int sel)
{
if (videoctrl[sel].player_handle_ != IntPtr.Zero)
return true;

window_handle_ = IntPtr.Zero;

videoctrl[sel].player_handle_ = new IntPtr();
UInt32 ret_open = NTSmartPlayerSDK.NT_SP_Open(out videoctrl[sel].player_handle_, window_handle_, 0, IntPtr.Zero);
if (ret_open != 0)
{
Debug.LogError("call NT_SP_Open failed, sel: " + sel);
return false;
}

if (IntPtr.Zero == videoctrl[sel].player_handle_)
return false;

videoctrl[sel].event_call_back_ = new SP_SDKEventCallBack(NT_SP_SDKEventCallBack);
NTSmartPlayerSDK.NT_SP_SetEventCallBack(videoctrl[sel].player_handle_, window_handle_, videoctrl[sel].event_call_back_);
videoctrl[sel].sdk_event_call_back_ = new VideoControl.SetEventCallBack(SDKEventCallBack);


/* ++ 播放前参数配置可加在此处 ++ */

int play_buffer_time_ = 0;
NTSmartPlayerSDK.NT_SP_SetBuffer(videoctrl[sel].player_handle_, play_buffer_time_); //设置buffer time

//int is_using_tcp = 1; //TCP模式
//NTSmartPlayerSDK.NT_SP_SetRTSPTcpMode(videoctrl[sel].player_handle_, is_using_tcp);

int timeout = 10;
NTSmartPlayerSDK.NT_SP_SetRtspTimeout(videoctrl[sel].player_handle_, timeout);

int is_auto_switch_tcp_udp = 1;
NTSmartPlayerSDK.NT_SP_SetRtspAutoSwitchTcpUdp(videoctrl[sel].player_handle_, is_auto_switch_tcp_udp);

Boolean is_mute_ = false;
NTSmartPlayerSDK.NT_SP_SetMute(videoctrl[sel].player_handle_, is_mute_ ? 1 : 0); //是否启动播放的时候静音

int is_fast_startup = 1;
NTSmartPlayerSDK.NT_SP_SetFastStartup(videoctrl[sel].player_handle_, is_fast_startup); //设置快速启动模式

Boolean is_low_latency_ = false;
NTSmartPlayerSDK.NT_SP_SetLowLatencyMode(videoctrl[sel].player_handle_, is_low_latency_ ? 1 : 0); //设置是否启用低延迟模式

//设置旋转角度(设置0, 90, 180, 270度有效,其他值无效)
int rotate_degrees = 0;
NTSmartPlayerSDK.NT_SP_SetRotation(videoctrl[sel].player_handle_, rotate_degrees);

int volume = 100;
NTSmartPlayerSDK.NT_SP_SetAudioVolume(videoctrl[sel].player_handle_, volume); //设置播放音量, 范围是[0, 100], 0是静音,100是最大音量, 默认是100

// 设置上传下载报速度
int is_report = 0;
int report_interval = 2;
NTSmartPlayerSDK.NT_SP_SetReportDownloadSpeed(videoctrl[sel].player_handle_, is_report, report_interval);

//设置播放URL
NTSmartPlayerSDK.NT_SP_SetURL(videoctrl[sel].player_handle_, videoctrl[sel].playback_url_);
/* -- 播放前参数配置可加在此处 -- */

return true;
}

停止播放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ini复制代码/*
* SmartPlayerLinuxMono.cs
* Author: daniusdk.com
*/
private void StopPlayer(int sel)
{
Debug.Log("StopPlayer++, sel: " + sel);

videoctrl[sel].is_need_get_frame_ = false;
videoctrl[sel].is_need_init_texture_ = false;

if (videoctrl[sel].player_handle_ == IntPtr.Zero)
{
return;
}

UInt32 flag = NTSmartPlayerSDK.NT_SP_StopPlay(videoctrl[sel].player_handle_);
if (flag == DANIULIVE_RETURN_OK)
{
Debug.Log("call NT_SP_StopPlay succeed, sel: " + sel);
}
else
{
Debug.LogError("call NT_SP_StopPlay failed, sel: " + sel);
}

NTSmartPlayerSDK.NT_SP_Close(videoctrl[sel].player_handle_);
videoctrl[sel].player_handle_ = IntPtr.Zero;

videoctrl[sel].is_playing_ = false;
}

具体回调处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
ini复制代码    private void SDKVideoFrameCallBack(UInt32 status, IntPtr frame, int sel)
{
//这里拿到回调frame,进行相关操作
NT_SP_VideoFrame video_frame = (NT_SP_VideoFrame)Marshal.PtrToStructure(frame, typeof(NT_SP_VideoFrame));

VideoFrame u3d_frame = new VideoFrame();

u3d_frame.width_ = video_frame.width_;
u3d_frame.height_ = video_frame.height_;

u3d_frame.timestamp_ = (UInt64)video_frame.timestamp_;

int d_y_stride = video_frame.width_;
int d_u_stride = (video_frame.width_ + 1) / 2;
int d_v_stride = d_u_stride;

int d_y_size = d_y_stride * video_frame.height_;
int d_u_size = d_u_stride * ((video_frame.height_ + 1) / 2);
int d_v_size = d_u_size;

int u_v_height = ((u3d_frame.height_ + 1) / 2);

u3d_frame.y_stride_ = d_y_stride;
u3d_frame.u_stride_ = d_u_stride;
u3d_frame.v_stride_ = d_v_stride;

u3d_frame.y_data_ = new byte[d_y_size];
u3d_frame.u_data_ = new byte[d_u_size];
u3d_frame.v_data_ = new byte[d_v_size];


CopyFramePlane(u3d_frame.y_data_, d_y_stride,
video_frame.plane0_, video_frame.stride0_, u3d_frame.height_);

CopyFramePlane(u3d_frame.u_data_, d_u_stride,
video_frame.plane1_, video_frame.stride1_, u_v_height);

CopyFramePlane(u3d_frame.v_data_, d_v_stride,
video_frame.plane2_, video_frame.stride2_, u_v_height);

lock (videoctrl[sel].frame_lock_ )
{
videoctrl[sel].cur_video_frame_ = u3d_frame;
//Debug.LogError("sel: " + sel + " w:" + u3d_frame.width_ + "h:" + u3d_frame.height_);
}
}

Unity层拿到video frame后,刷新即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
ini复制代码    private void UpdateProc(int sel)
{
VideoFrame video_frame = null;

lock (videoctrl[sel].frame_lock_)
{
video_frame = videoctrl[sel].cur_video_frame_;

videoctrl[sel].cur_video_frame_ = null;
}

if ( video_frame == null )
return;

if (!videoctrl[sel].is_need_get_frame_)
return;

if (videoctrl[sel].player_handle_ == IntPtr.Zero )
return;

if ( !videoctrl[sel].is_need_init_texture_)
{
if ( video_frame.width_ != videoctrl[sel].video_width_
|| video_frame.height_ != videoctrl[sel].video_height_
|| video_frame.y_stride_ != videoctrl[sel].y_row_bytes_
|| video_frame.u_stride_ != videoctrl[sel].u_row_bytes_
|| video_frame.v_stride_ != videoctrl[sel].v_row_bytes_ )
{
videoctrl[sel].is_need_init_texture_ = true;
}
}

if (videoctrl[sel].is_need_init_texture_)
{
if (InitYUVTexture(video_frame, sel))
{
videoctrl[sel].is_need_init_texture_ = false;
}
}

UpdateYUVTexture(video_frame, sel);
}

UpdateYUVTexture相关实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ini复制代码    private void UpdateYUVTexture(VideoFrame video_frame, int sel)
{
if (video_frame.y_data_ == null || video_frame.u_data_ == null || video_frame.v_data_ == null)
{
Debug.Log("video frame with null..");
return;
}

if (videoctrl[sel].yTexture_ != null)
{
videoctrl[sel].yTexture_.LoadRawTextureData(video_frame.y_data_);
videoctrl[sel].yTexture_.Apply();
}

if (videoctrl[sel].uTexture_ != null)
{
videoctrl[sel].uTexture_.LoadRawTextureData(video_frame.u_data_);
videoctrl[sel].uTexture_.Apply();
}

if (videoctrl[sel].vTexture_ != null)
{
videoctrl[sel].vTexture_.LoadRawTextureData(video_frame.v_data_);
videoctrl[sel].vTexture_.Apply();
}
}

技术总结

以上是Linux平台下Unity RTMP、RTSP直播播放器大概的实现参考,随着国产操作系统的推进,Linux下RTMP、RTSP高质量的播放器需求越来越大,Unity下,可以实现和Windows、Android等平台统一开发管理,非常方便。感兴趣的公司或开发者,可以单独跟我沟通探讨。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

CTF 数表 乘积

发表于 2024-04-27

截屏2024-04-27 00.54.26.png

截屏2024-04-27 00.54.46.png

你拿到了一个15*15的数表,请你求得数表中在一条线上(行、列、两个对角线)连续5个数的乘积的最大值,并将这5个数的10个数字按数字从小到大从左到右排列形成密码串,并分别提交。

file.jpg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
python复制代码import pytesseract
from PIL import Image, ImageEnhance

# 打开图像文件
img = Image.open("file.jpg")

# 增强图像对比度,使文本更清晰
enhancer = ImageEnhance.Contrast(img)
img_enhanced = enhancer.enhance(1.5) # 调整对比度的级别

# 调整图像大小并应用阈值过滤器进行二值化处理
img_resized = img_enhanced.resize((1024, 768))
img_binarized = img_resized.convert("L").point(lambda x: 0 if x < 128 else 255, "1")

# 配置 Tesseract 以将图像视为单一块文本
custom_config = r"--oem 3 --psm 6"
text = pytesseract.image_to_string(img_binarized, config=custom_config)

# 替换识别结果中的度数符号为一个空格
text = text.replace("°", " ")
# print(text)


# 处理识别的文本,将其转换为二维数组
def convert_text_to_matrix(text):
# 根据换行符分割文本为多行
lines = text.strip().split("\n")

# 初始化二维数组
matrix = []
for line in lines:
# 过滤掉非数字字符,并以空格分割每一行中的数字
numbers = line.strip().split()
# 将字符串中的数字转换为整数列表
number_list = [int(num) for num in numbers if num.isdigit()]
# 如果列表非空,则添加到二维数组中
if number_list:
matrix.append(number_list)

return matrix


# 调用函数,将文本转换为二维数组
matrix = convert_text_to_matrix(text)

# 打印转换后的二维数组
print("矩阵:")
for row in matrix:
print(row)


# 辅助函数:计算列表中连续五个元素的乘积
def product_of_five(numbers):
from functools import reduce
from operator import mul

return reduce(mul, numbers)


# 定义函数计算最大乘积
def max_product(matrix):
n = len(matrix)
max_prod = 0 # 初始化最大乘积为 0
max_numbers = [] # 初始化最大乘积对应的数字序列

# 遍历矩阵的行和列
for i in range(n):
for j in range(n - 4):
# 计算每行的连续五个数字的乘积
row_prod = product_of_five(matrix[i][j : j + 5])
# 更新最大乘积和数字序列
if row_prod > max_prod:
max_prod = row_prod
max_numbers = matrix[i][j : j + 5]

# 计算每列的连续五个数字的乘积
col_prod = product_of_five([matrix[j + k][i] for k in range(5)])
if col_prod > max_prod:
max_prod = col_prod
max_numbers = [matrix[j + k][i] for k in range(5)]

# 遍历矩阵的对角线
for i in range(n - 4):
for j in range(n - 4):
# 计算主对角线上连续五个数字的乘积
diag1_prod = product_of_five([matrix[i + k][j + k] for k in range(5)])
if diag1_prod > max_prod:
max_prod = diag1_prod
max_numbers = [matrix[i + k][j + k] for k in range(5)]

# 计算副对角线上连续五个数字的乘积
diag2_prod = product_of_five([matrix[i + k][j + 4 - k] for k in range(5)])
if diag2_prod > max_prod:
max_prod = diag2_prod
max_numbers = [matrix[i + k][j + 4 - k] for k in range(5)]

# 对最大乘积对应的数字序列中的每一位数字进行排序
password = "".join(sorted("".join(map(str, max_numbers))))
return max_prod, password # 返回最大乘积和排序后的密码字符串


# 调用函数计算最大乘积和密码串
max_prod, password = max_product(matrix)
print("MAX SUM:", max_prod) # 打印最大乘积
print("PassWord:", password) # 打印排序后的密码串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码矩阵:
[71, 51, 67, 63, 89, 41, 92, 36, 54, 22, 40, 40, 28, 66, 33]
[60, 99, 13, 45, 22, 44, 75, 33, 53, 78, 36, 84, 20, 35, 17]
[28, 64, 23, 67, 10, 26, 38, 40, 67, 59, 54, 70, 66, 18, 38]
[68, 32, 62, 12, 20, 95, 63, 94, 39, 63, 68, 40, 91, 66, 49]
[25, 66, 73, 99, 26, 97, 17, 78, 78, 96, 83, 14, 88, 34, 89]
[89, 75, 10, 76, 44, 20, 45, 35, 14, 30, 61, 33, 97, 34, 31]
[28, 22, 75, 31, 67, 15, 94, 63, 80, 44, 62, 16, 14, 79, 53]
[42, 96, 35, 31, 47, 55, 58, 88, 24, 70, 17, 54, 24, 36, 29]
[48, 35, 71, 89, 67, 95, 54, 61, 37, 44, 60, 21, 58, 51, 54]
[68, 15, 64, 47, 69, 28, 73, 92, 13, 86, 52, 17, 77, 34, 89]
[83, 90, 35, 90, 16, 87, 97, 57, 32, 16, 26, 26, 79, 33, 27]
[87, 57, 62, 20, 72, 23, 46, 33, 67, 46, 55, 12, 32, 63, 93]
[73, 38, 25, 39, 11, 24, 94, 72, 18, 58, 46, 29, 32, 40, 62]
[41, 72, 30, 23, 88, 34, 62, 92, 69, 82, 67, 59, 85, 74, 84]
[29, 78, 31, 90, 31, 74, 31, 49, 71, 48, 86, 81, 16, 23, 57]
MAX SUM: 2171903490
PassWord: 1455677899

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Nodejs 第二十八章 邮件服务 Nodejs 第二十八章

发表于 2024-04-27

Nodejs 第二十八章 邮件服务

邮件服务在工作中扮演着多种关键角色,帮助提高团队效率和沟通流畅性。下面详细解释邮件服务在不同工作场景中的应用:

  1. 任务分配与跟踪:
* **用途**:邮件服务常用于分配具体任务、指派工作责任,并追踪项目的进展。
* **功能**:通过发送包含任务清单、工作指示和进度更新的邮件,可以确保每个团队成员都明确自己的职责和任务要求。这种通信方式能够有效帮助团队领导或项目管理者持续监控工作的完成情况。
  1. 错误报告和故障排除:
* **用途**:当软件或系统出现错误时,开发人员可以通过邮件向团队或相关人员报告问题。
* **功能**:邮件中可以包含详细的错误信息、重现问题的步骤以及问题发生的环境。团队成员就能够快速了解问题所在,并协助进行故障排除和修复。详细信息可能包括错误日志、堆栈跟踪以及可能的修复建议,使团队能更有效地响应和处理问题。
  1. 自动化构建和持续集成:
* **用途**:在持续集成(CI)和自动化构建的环境中,邮件服务用于通知团队成员关于构建状态的重要更新。
* **功能**:邮件通知可以包括构建成功或失败的信息、单元测试结果、代码覆盖率等。如果构建过程中出现错误或警告,系统会自动发送邮件给相关人员,他们就能及时了解并采取必要的措施。

第三方库使用

js-yaml

  • 作用:js-yaml 是一个用于解析和生成 YAML(一种常用于配置文件的人类友好的数据序列化标准)的 JavaScript 库。
  • 用途:它允许开发者在 Node.js 应用中轻松读取、写入和处理 YAML 文件。这对于配置管理特别有用,因为 YAML 格式的可读性比 JSON 更好,常用于存储配置设置。

nodemailer

  • 作用:nodemailer 是一个在 Node.js 中发送电子邮件的模块。
  • 用途:它支持各种邮件发送选项,包括但不限于文本内容、HTML内容、附件等。nodemailer可以与SMTP(简单邮件传输协议)服务配合使用,也支持直接传输和其他传输方法(如 Sendmail),使得它在应用程序中实现自动邮件通知、用户验证邮件和其他邮件发送功能变得非常简单。

yaml文件格式

  • 我们邮件的账号(密码| 授权码)不可能明文写到代码中,一般存放在yaml文件或者环境变量里面
+ pass就是password(密码),user则是用户邮箱账号
+ 这里需要注意在`pass:`或者`user:`后面继续填写内容的时候,要加上空格
1
2
3
yaml复制代码 # js-yaml 解析yaml文件
 pass: 授权码 | 密码
 user: xxxxx@qq.com 邮箱账号

代码实现

初始化邮件服务

  • 首先,是肯定需要先将两个需要用到的第三方库下载下来
+ mailer就是邮寄的意思,而nodemailer就是node-mailer=>使用node实现的邮件服务
1
2
复制代码 npm install js-yaml
 npm install nodemailer
  • 然后创建一个mail.http文件用来模拟发送请求(前面章节就有讲到了),一个mail.yaml文件用来存储内容,以及一个index.js用来写我们的代码啦
1
2
3
4
5
6
7
8
9
10
11
12
13
php复制代码 import nodemailer from "nodemailer"
 
 // 初始化邮件服务
 const transport = nodemailer.createTransport({
   service:'qq',
   host:"smtp.qq.com",
   port:465,//具体含义后面说
   secure:true,//是否要使用https
   auth:{
     user:"",//邮箱账号
     pass:""//邮箱密码/授权码
  }
 })
  • 这时候,我们可能就要疑惑了,这些参数要具体填写什么,我要去哪里看,比如说port端口参数,为什么要填465,有什么含义吗?
+ 很显然,这是有的,我们需要看**QQ邮件服务文档**,地址:[SMTP/IMAP服务 (qq.com)](https://wx.mail.qq.com/list/readtemplate?name=app_intro.html#/agreement/authorizationCode)
+ 那在这里面,我们就能够知道port为什么要填写465了,因为QQ的文档里面已经有写明如何使用了

image-20240426234830911

POP3/SMTP 设置方法

用户名/帐户: 你的QQ邮箱完整的地址

密码: 生成的授权码

电子邮件地址: 你的QQ邮箱的完整邮件地址

接收邮件服务器: pop.qq.com,使用SSL,端口号995

发送邮件服务器: smtp.qq.com,使用SSL,端口号465或587

授权码生成

  • 如何拿到授权码,就需要通过我们前面写出来的QQ开发文档链接跳转进去找到下图中的内容

image-20240426235044392

  • 然后选择生成授权码就OK了

image-20240426235428786

  • 成功生成界面如下:

image-20240426235606065

  • 从设备管理中,可以看到我们目前管理的授权码,然后将其存进mail.yaml文件里面
+ yaml存放的格式已经写在上面了,照着填进去就OK了
+ user填写QQ邮箱,pass填写我们刚刚拿到的授权码

image-20240426235921752

实现发送邮件功能

  • yaml文件设置好了之后,我们只需要在index.js中,将其读取出来,然后放入auth参数中。这里之所以绕一个弯路,不直接把内容存放进去的原因,就是为了防止隐私泄露(不用明文形式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
php复制代码 import nodemailer from "nodemailer"
 import yaml from "js-yaml"
 import fs from "node:fs"
 
 const mailConfig = yaml.load(fs.readFileSync('./mail.yaml', 'utf-8'))
 
 // 初始化邮件服务
 const transport = nodemailer.createTransport({
   service: 'qq',
   host: "smtp.qq.com",
   port: 465,
   secure: true,//是否要使用https
   auth: {
     user: mailConfig.user,//邮箱账号
     pass: mailConfig.pass//邮箱密码/授权码
  }
 })
  • 接下来就要起一个服务,然后实现发送邮件的功能。没错,现在才要开始实现,我们前面的内容是配置发送邮件的前置信息
+ 在下面这段代码中,我们在发送内容之前,必须要判断一下,是否是我们发送邮箱的地址,比如我们这里是以`/send/mail`进行发送的,这和我们区分动静分离的方式是有点像的,我们这里区分出来的是专门为了发送邮箱的接口
+ 其中to、subject、text都是不确定的参数,只有from是确定的。所以只有一个from可以写死(就是我们用来发送信息的那个邮箱账号)。而其他的三样内容都是要根据发送的请求携带参数来填入,且我们携带的这些参数作为邮件来说,必然是私密性质的,所以需要判断请求是否为POST请求才行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javascript复制代码 const server = http.createServer((req, res) => {
   const { pathname } = url.prase(req.url)
   const { method } = req
   if(method === 'POST' && pathname === '/send/mail'){
     // 发送邮件
     transport.sendMail( {
       to:'收件人',
       from:'发件人',
       subject:'主题',
       text:'内容'
    })
  }
 })
 
 server.listen(3000, () => {
   console.log("3000端口已经启动");
 })
  • 但上面的代码是没办法动态做到将请求的对应内容填入的,我们需要进行一定的修改
+ 我们先通过req.on中的data方法来监听到内容,再根据完整的数据进行JSON转化提取其中需要用到的数据传入我们的to、subject、text中
+ 其中这里发送邮件运用到ES6之后的写法,`to:to`类型的写法可以省略为`to`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
javascript复制代码 import nodemailer from "nodemailer"
 import yaml from "js-yaml"
 import fs from "node:fs"
 import url from "node:url"
 import http from "node:http"
 
 const mailConfig = yaml.load(fs.readFileSync('./mail.yaml', 'utf-8'))
 
 // 初始化邮件服务
 const transport = nodemailer.createTransport({
   service: 'qq',
   host: "smtp.qq.com",
   port: 465,
   secure: true,//是否要使用https
   auth: {
     user: mailConfig.user,//邮箱账号
     pass: mailConfig.pass//邮箱密码/授权码
  }
 })
 
 const server = http.createServer((req, res) => {
   const { pathname } = url.parse(req.url)
   const { method } = req
   if (method === 'POST' && pathname === '/send/mail') {
     // 发送邮件
     let data = '';
     // 回调返回的是一个个片段,我们将其拼接起来为整体
     req.on("data", (chunk) => {
       data += chunk
    })
     req.on('end', () => {
       const { to, subject, text } = JSON.parse(data)
       transport.sendMail({
         to,
         from: mailConfig.user,
         subject,
         text
      })
       res.end("邮件发送成功")
    })
     // transport.sendMail( {
     //   to:'收件人',
     //   from:'发件人',
     //   subject:'主题',
     //   text:'内容'
     // })
  }
 })
 
 server.listen(3000, () => {
   console.log("3000端口已经启动");
 })

测试邮件服务

  • 那这样我们也是成功写完了,现在,就让我们来编写一个http请求吧,看下是否能够实现发送邮件的功能
+ 然后记得启动一下我们写在index.js中的服务
1
2
3
4
5
6
7
8
9
bash复制代码 //mail.http文件
 POST http://localhost:3000/send/mail HTTP/1.1
 Content-Type: application/json
 
 {
  "to":"1045098807@qq.com",//这里就发给我自己进行测试了,这里的注释记得删掉
  "subject":"小余测试邮件发送功能",
  "text":"我是小余,我为自己代言,我的微信为:XiaoYu2002-AI"
 }

image-20240427004209645

  • 能够看到成功发送内容到我们写的邮件服务接口上了
+ 通过打开QQ邮箱,我们也确实成功看到了内容成功发送

image-20240427004311335

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…789…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%