目录

基础库常见问题

解析大文件xml发生内存溢出(Out of Memory)

由于ArkTS侧提供的XML解析接口暂不支持流式解析模式,建议通过Native工程调用第三方C/C++库来实现。推荐使用libxml2库,该库具有成熟稳定、性能优越的特点,能够支持SAX等流式解析方式,有效降低内存占用。

具体实施步骤如下:

  1. 创建Native工程:在HarmonyOS项目中创建C++模块。
  2. 集成libxml2:下载并配置libxml2库源码或预编译库,在CMakeLists.txt中进行引用。
  3. 编写解析代码:使用libxml2提供的API实现流式解析逻辑。
  4. XML对象处理:当XML文件大小超过100MB时,建议在Native侧处理。

关于如何在ArkTS侧引用编译生成的三方so库,请参考文档:如何在ArkTS侧引用其他三方so库。

libxml2库支持的回调函数主要如下所示:

回调函数指针 触发时机 用途
startDocument 文档开始时 初始化环境,分配资源。
endDocument 文档结束时 释放资源,打印统计信息。
startElement 读到开始标签(如<tag>) 获取标签名及其属性。
endElement 读到结束标签(如</tag>) 处理标签结束逻辑,如出栈。
characters 读到标签间的文本内容 处理文本数据(注意可能被多次调用)。

代码示例:

// 用户自定义数据
ParseContext context;

// 初始化SAX Handler结构体
xmlSAXHandler SAXHandler = { 0 };

// 绑定回调函数,用于在解析过程中处理XML数据
SAXHandler.startDocument = startDocument;
SAXHandler.endDocument = endDocument;
SAXHandler.startElement = startElement;
SAXHandler.endElement = endElement;
SAXHandler.characters = characters;

// 解析文件
// 用户自定义数据指针
int ret = xmlSAXUserParseFile(&SAXHandler, &context, xmlFileName);

if (ret != 0) {
    printf("Failed to parse XML file.\n");
    return 1;
}

// 清理libxml2全局状态
xmlCleanupParser();

定时器被误删除

由于定时器ID为进程共享,是从0开始的,开发者误操作容易导致定时器被删除。

例如以下场景:

export class testClass {
    // 初始值设置为0
    private timeoutId: number = 0;
    private intervalId: number = 0;

    // 在某些情况下没有调用setTimeout设置定时器就调用了clearAnimation函数删除了定时器,就会导致timeoutId为0的定时器被删除
    clearAnimation(): void {
        clearInterval(this.intervalId);
        clearTimeout(this.timeoutId);
    }
}

可以通过以下方法快速定位:

重写globalThis.clearTimeout函数,实现在调用clearTimeout函数时打印调用栈,快速定位定时器是在哪里被删除的。

调用顺序为先调用clearTimeout.ts文件中的test()函数,再调用TimerTest.ets文件中testClass类的clearAnimation()函数。

示例代码:

// 自定义TS文件clearTimeout.ts

// test函数需要在程序调用clearTimeout函数之前调用
export function test() {
    // 完全兼容原始 clearTimeout 类型
    const origClear = globalThis.clearTimeout;
    globalThis.clearTimeout = (...args: any[]) => {
        const timeoutId = args[0];

        // 检查所有可能的 timerId = 0 的情况
        if (timeoutId === 0 || timeoutId === "0") {
            console.info("清除 timerId = 0 !", new Error().stack);
            // 触发断点
            debugger;
        }

        // 使用 apply 确保正确传递所有参数
        return origClear.apply(this, args);
    }
}
// 自定义ets文件TimerTest.ets

export class testClass {
    // 初始值设置为0
    private timeoutId: number = 0;
    private intervalId: number = 0;

    // 在某些情况下没有调用setTimeout设置定时器就调用了clearAnimation函数删除了定时器,就会导致timeoutId为0的定时器被删除
    clearAnimation(): void {
        clearInterval(this.intervalId);
        clearTimeout(this.timeoutId);
    }
}
import { test } from './clearTimeout';
import { testClass } from './TimerTest';

@Entry
@Component
struct Index {
    @State message: string = 'Hello World';

    build() {
      Row() {
        Column() {
          Text(this.message)
            .fontSize(50)
            .fontWeight(FontWeight.Bold)
            .onClick(() => {
                test();
                let testCase = new testClass();
                testCase.clearAnimation();
                this.message = 'success';
            })
        }
        .width('100%')
      }
      .height('100%')
    }
}

Base64编码规则

Base64编码使用一组特定的64个字符来表示二进制数据。这64个字符包括大写字母A-Z、小写字母a-z、数字0-9,以及符号"+"和"/"。在某些情况下,还会使用"="作为填充字符。Base64编码将原始数据按每3个字节分组,每组3个字节转换为4个Base64字符。

Base64编码表如下:

索引 字符 索引 字符 索引 字符 索引 字符
0 A 16 Q 32 g 48 w
1 B 17 R 33 h 49 x
2 C 18 S 34 i 50 y
3 D 19 T 35 j 51 z
4 E 20 U 36 k 52 0
5 F 21 V 37 l 53 1
6 G 22 W 38 m 54 2
7 H 23 X 39 n 55 3
8 I 24 Y 40 o 56 4
9 J 25 Z 41 p 57 5
10 K 26 a 42 q 58 6
11 L 27 b 43 r 59 7
12 M 28 c 44 s 60 8
13 N 29 d 45 t 61 9
14 O 30 e 46 u 62 +
15 P 31 f 47 v 63 /

由此可见Base64是基于64个可打印字符来表示二进制数据的编解码方式。将二进制数据转为字符串(ASCII码),方便数据传输和加解密保护。

它的编码实现原理为:

将需要编码的字符串转换成二进制序列,然后按每6个二进制位为一组,分成若干组,如果不足6位,则低位补0。每6位组成一个新的字节,高位补00,构成一个新的二进制序列,最后根据Base64索引表中的值找到对应的字符。

例如字符串“ABC”,我们进行Base64编码,最后结果会是QUJD:

原始字符 A B C -
ASCII编码 65 66 67 -
二进制位 01000001 01000010 01000011 -
编码转换 010000 010100 001001 000011
base64索引值 16 20 9 3
base64字符 Q U J D

若原始字符串长度不是3的倍数,编码后末尾用“=”填充,使编码后的Base64字符串最终长度为4的倍数,以满足转换后Base64长度要求。

例如字符串“AB”转换后为“QUI”为了凑齐4字节,需要在末尾补充“=”:

原始字符 A B - -
ASCII编码 65 66 - -
二进制位 01000001 01000010 - -
编码转换(不足6位补0) 010000 010100 001000 -
base64索引值 16 20 8 -
base64字符 Q U I =

为什么HashSet和TreeSet的add方法添加已存在元素时返回true

HashSet和TreeSet的add方法在API参考中的返回值描述为:成功添加新元素至容器返回true,当元素已存在时返回false。当前代码实现中,添加已存在的元素时返回值同样为true,与API参考中的返回值描述存在差异。

该差异仅体现在返回值上,不影响集合功能。已存在的元素不会重复添加,容器的length保持不变,集合元素的唯一性不受影响。

规避方案:不要通过add方法的返回值判断元素是否已存在,可在调用add方法前先使用has方法判断,或对比调用add方法前后容器的length是否发生变化。

示例代码

import { HashSet, TreeSet } from '@kit.ArkTS';

// 创建HashSet实例并添加元素
let hashSet = new HashSet<string>();
hashSet.add('squirrel');
// 添加已存在的元素
let result = hashSet.add('squirrel');
console.info('result:', result);
// 期望输出: result: false
// 实际输出: result: true
console.info('length:', hashSet.length); // length: 1,元素未重复添加

// 创建TreeSet实例并添加元素
let treeSet = new TreeSet<string>();
treeSet.add('squirrel');
// 添加已存在的元素
let treeResult = treeSet.add('squirrel');
console.info('treeResult:', treeResult);
// 期望输出: treeResult: false
// 实际输出: treeResult: true
console.info('length:', treeSet.length); // length: 1,元素未重复添加

// 推荐做法:先通过has方法判断,避免依赖add方法的返回值
let value = 'sparrow';
if (!hashSet.has(value)) {
  hashSet.add(value);
}