分类 技术学习 下的文章

因为我有SM.MS账号,所以一直是登录使用。最近才发现SM.MS禁止了游客上传图像,今天试的imgdd上传出现了问题。正好闲来无事整理一下现在可用的免费图床。

图像处理:

https://ic.yunimg.cc/

可选择多种文件尺寸和图像质量,并可在线预览对比图像质量后保存。

https://imagestool.com/webp2jpg-online/

几乎全能图像在线转换压缩处理工具

图床

以下为图床列表,示例图片文件大小为1.01Mb左右(1041 kb,1065532 字节),你可以按F12,在开发人员工具中选择网络-停用缓存,然后按F5刷新页面,查看图片下载速度。

只做收集使用,没有任何利益关系。请注意备份您的数据

- 阅读剩余部分 -

为了防止有奇怪的人不断试你的密码,你可以尝试修改你的Typecho的登录路径。

以下举例,我们将admin修改为SAVq87NqJ9ecUSYl

需要修改的文件夹

  1. 打开网站文件根目录
  2. 重命名文件夹adminSAVq87NqJ9ecUSYl

需要修改的配置

  1. 打开根目录config.inc.php文件,修改大约第12行的
define('__TYPECHO_ADMIN_DIR__', '/admin/');
  1. 打开install.php(如果已经完成网站安装,可忽略),大约第14行已经第405行
define('__TYPECHO_ADMIN_DIR__', '/admin/');

将移上所有的admin修改为SAVq87NqJ9ecUSYl即可。

TL;DR

实时语音传输的核心在于持续流式处理,我们通过一个完整的代码示例来揭示其工作原理:

1. 音频分片机制:

// 音频采集线程
class AudioCaptureThread extends Thread {
    private static final int SAMPLE_RATE = 48000; // 48kHz采样率
    private static final int FRAME_DURATION = 20; // 20ms帧间隔
    private static final int FRAME_SIZE = (SAMPLE_RATE * FRAME_DURATION) / 1000; // 960采样点

    @Override
    public void run() {
        AudioRecord recorder = createAudioRecord();
        ByteBuffer buffer = ByteBuffer.allocateDirect(FRAME_SIZE * 2); // 16bit采样
        
        recorder.startRecording();
        while (isRunning) {
            // 读取20ms的PCM数据
            int readBytes = recorder.read(buffer, FRAME_SIZE * 2);
            
            // 添加RTP头部(时间戳+序号)
            RtpPacket packet = new RtpPacket();
            packet.timestamp = SystemClock.elapsedRealtimeNanos() / 1000;
            packet.sequence = nextSequenceNumber();
            packet.payload = buffer.array();
            
            // 立即发送数据包
            networkSender.send(packet);
            
            buffer.rewind(); // 重用缓冲区
        }
    }
    
    private AudioRecord createAudioRecord() {
        return new AudioRecord.Builder()
            .setAudioFormat(new AudioFormat.Builder()
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .setSampleRate(SAMPLE_RATE)
                .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
                .build())
            .setBufferSizeInBytes(FRAME_SIZE * 4) // 双缓冲
            .build();
    }
}

关键原理说明:

  • 时间戳精度:采用微秒级时间戳(elapsedRealtimeNanos()/1000)确保时序精度
  • 环形缓冲区:DirectByteBuffer 重用避免内存抖动
  • 实时发送:每个 20ms 数据包立即发送,无需等待前序确认

2. 实时播放机制

class AudioPlaybackThread extends Thread {
    private static final int JITTER_BUFFER_DEPTH = 5; // 100ms缓冲深度
    private final PriorityBlockingQueue<RtpPacket> buffer = 
        new PriorityBlockingQueue<>(50, Comparator.comparingLong(p -> p.timestamp));
    
    private AudioTrack audioTrack;
    private long lastPlayedTimestamp = 0;

    @Override
    public void run() {
        audioTrack = createAudioTrack();
        audioTrack.play();
        
        while (isRunning) {
            RtpPacket packet = waitForNextPacket();
            writeToAudioTrack(packet);
            updateTimeline(packet);
        }
    }
    
    private RtpPacket waitForNextPacket() {
        if (buffer.size() < JITTER_BUFFER_DEPTH) {
            // 缓冲不足时插入静音包
            return generateSilencePacket();
        }
        
        return buffer.poll(20, TimeUnit.MILLISECONDS); // 阻塞等待
    }
    
    private void writeToAudioTrack(RtpPacket packet) {
        // 抖动补偿计算
        long expectedTimestamp = lastPlayedTimestamp + 20000; // 20ms间隔
        long timestampDelta = packet.timestamp - expectedTimestamp;
        
        if (timestampDelta > 50000) { // 超过50ms延迟
            resetPlayback(); // 重置时间线
        }
        
        audioTrack.write(packet.payload, 0, packet.payload.length);
        lastPlayedTimestamp = packet.timestamp;
    }
    
    private AudioTrack createAudioTrack() {
        return new AudioTrack.Builder()
            .setAudioFormat(new AudioFormat.Builder()
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .setSampleRate(SAMPLE_RATE)
                .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                .build())
            .setBufferSizeInBytes(FRAME_SIZE * 4)
            .setTransferMode(AudioTrack.MODE_STREAM)
            .build();
    }
}

核心算法解析:

  • 自适应抖动缓冲:根据网络状况动态调整缓冲深度
  • 时间线同步:通过时间戳差值检测网络延迟突变
  • 静音补偿:在丢包时生成舒适噪声保持播放连续性

开始

在做之前我完全没考虑过网络通话是如何实现。就是如何做到你在说话的同时,对方一直可以听到的。我的意思是,你说了一句很长的话,对方不是你说完才听到的,是你一直在说,对方那边一直播放。

上面的TL;DR部分几乎可以解答我所有的疑惑了。不过要实现类似微信语音通话的实时对话功能,需要深入理解音视频流式处理的完整技术链。本文将重点从Android客户端的视角,剖析实时语音通话的核心技术实现。本文结束后,有包括上面的代码在内的完整的示例代码,有需要都可以自取。


一、实时通话核心技术原理

1.1 流式处理 vs 文件传输

graph LR
    A[麦克风持续采集] --> B[20ms数据分块]
    B --> C[即时编码]
    C --> D[网络实时发送]
    D --> E[接收端缓冲]
    E --> F[持续解码播放]

与传统文件传输不同,实时通话采用流水线式处理:

  • 时间切片:音频按20ms为单位切割处理
  • 无等待传输:每个数据包独立发送,无需等待整段语音
  • 并行处理:采集、编码、传输、解码、播放同时进行

1.2 关键性能指标

指标目标值实现要点
端到端延迟<400ms编解码优化/Jitter Buffer控制
音频采样率48kHz硬件加速支持
抗丢包能力5%-20%FEC/Opus冗余
CPU占用率<15%MediaCodec硬件编码

二、Android客户端实现详解

2.1 音频采集模块

// 低延迟音频采集配置
private void setupAudioRecorder() {
    int sampleRate = 48000; // 优先选择硬件支持的采样率
    int channelConfig = AudioFormat.CHANNEL_IN_MONO;
    int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
    
    int bufferSize = AudioRecord.getMinBufferSize(sampleRate, 
                        channelConfig, audioFormat);
    
    AudioRecord recorder = new AudioRecord(
        MediaRecorder.AudioSource.VOICE_COMMUNICATION, // 专用通信模式
        sampleRate,
        channelConfig,
        audioFormat,
        bufferSize * 2); // 双缓冲避免溢出
        
    // 环形缓冲区实现
    audioThread = new Thread(() -> {
        byte[] buffer = new byte[960]; // 20ms数据量:48000Hz * 20ms * 16bit / 8 = 1920字节
        while (isRecording) {
            int readBytes = recorder.read(buffer, 0, buffer.length);
            if (readBytes > 0) {
                encoderQueue.offer(buffer.clone()); // 提交编码队列
            }
        }
    });
}

关键参数选择依据:

  • VOICE_COMMUNICATION :启用回声消除硬件加速
  • 48kHz采样率:平衡音质与延迟
  • 20ms帧长:Opus编码标准推荐值

2.2 音频编码与传输

// 硬件编码器初始化
MediaFormat format = MediaFormat.createAudioFormat(
        MediaFormat.MIMETYPE_AUDIO_OPUS, 48000, 1);
format.setInteger(MediaFormat.KEY_BIT_RATE, 24000);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 960);

MediaCodec encoder = MediaCodec.createEncoderByType("audio/opus");
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();

// 编码循环
while (!encoderQueue.isEmpty()) {
    int inputIndex = encoder.dequeueInputBuffer(10000);
    if (inputIndex >= 0) {
        ByteBuffer inputBuffer = encoder.getInputBuffer(inputIndex);
        byte[] rawData = encoderQueue.poll();
        inputBuffer.put(rawData);
        encoder.queueInputBuffer(inputIndex, 0, rawData.length, 
                                System.nanoTime()/1000, 0);
    }
    
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000);
    if (outputIndex >= 0) {
        ByteBuffer encodedData = encoder.getOutputBuffer(outputIndex);
        sendToNetwork(encodedData); // 网络发送
        encoder.releaseOutputBuffer(outputIndex, false);
    }
}

编码优化技巧:

  • 使用MediaCodec.CONFIGURE_FLAG_ENCODE开启硬件编码
  • 设置KEY_MAX_INPUT_SIZE防止缓冲区溢出
  • 时间戳使用微秒单位(System.nanoTime()/1000)

2.3 网络传输层

UDP封包结构示例

+--------+--------+--------+-------------------+
| RTP头  | 时间戳 | 序列号 | Opus载荷(20ms数据)|
+--------+--------+--------+-------------------+
| 12字节 | 4字节  | 2字节  | 可变长度           |
+--------+--------+--------+-------------------+

NAT穿透实现

// STUN协议实现示例
public InetSocketAddress discoverNAT() throws IOException {
    DatagramSocket socket = new DatagramSocket();
    byte[] stunRequest = createStunBindingRequest();
    
    // 发送到公共STUN服务器
    socket.send(new DatagramPacket(stunRequest, stunRequest.length, 
                 InetAddress.getByName("stun.l.google.com"), 19302));
                 
    // 接收响应
    byte[] buffer = new byte[1024];
    DatagramPacket response = new DatagramPacket(buffer, buffer.length);
    socket.receive(response);
    
    // 解析XOR-MAPPED-ADDRESS
    return parseStunResponse(response.getData());
}

2.4 接收端播放实现

Jitter Buffer设计

class JitterBuffer {
    private static final int MAX_BUFFER_SIZE = 10; // 存储200ms数据
    private PriorityQueue<AudioPacket> buffer = 
        new PriorityQueue<>(Comparator.comparingInt(p -> p.sequence));
    private int lastPlayedSeq = -1;

    public void addPacket(AudioPacket packet) {
        if (packet.sequence > lastPlayedSeq) {
            buffer.offer(packet);
            // 缓冲区溢出处理
            if (buffer.size() > MAX_BUFFER_SIZE) {
                buffer.poll(); // 丢弃最旧数据包
            }
        }
    }

    public AudioPacket getNextPacket() {
        if (!buffer.isEmpty() && 
            buffer.peek().sequence == lastPlayedSeq + 1) {
            lastPlayedSeq++;
            return buffer.poll();
        }
        return null;
    }
}

低延迟播放配置

AudioTrack audioTrack = new AudioTrack.Builder()
    .setAudioAttributes(new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
        .build())
    .setAudioFormat(new AudioFormat.Builder()
        .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
        .setSampleRate(48000)
        .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
        .build())
    .setBufferSizeInBytes(960 * 2) // 双缓冲
    .setTransferMode(AudioTrack.MODE_STREAM)
    .build();

audioTrack.play();

// 播放线程
while (isPlaying) {
    AudioPacket packet = jitterBuffer.getNextPacket();
    if (packet != null) {
        audioTrack.write(packet.data, 0, packet.data.length);
    } else {
        generateComfortNoise(); // 生成舒适噪声
    }
}

三、服务端关键技术方案

3.1 信令服务器设计

// Protobuf消息定义
message SignalMessage {
    enum Type {
        OFFER = 0;
        ANSWER = 1;
        ICE_CANDIDATE = 2;
    }
    
    Type type = 1;
    string sdp = 2;
    repeated string iceCandidates = 3;
}

核心功能:

  • WebSocket长连接管理
  • SDP交换协调
  • ICE候选收集与转发

3.2 TURN中继服务器

客户端A ↔ TURN Server ↔ 客户端B
           ↓
           当P2P不通时启用中继

四、性能优化实践

4.1 延迟优化矩阵

优化方向具体措施效果预估
采集延迟使用AudioRecord的READ_NON_BLOCKING模式减少2-5ms
编码延迟启用MediaCodec异步模式减少3-8ms
网络传输开启UDP QoS标记(DSCP 46)减少10-50ms
播放缓冲动态调整Jitter Buffer深度减少20-100ms

4.2 功耗控制策略

// 通话中唤醒锁管理
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
WakeLock wakeLock = pm.newWakeLock(
    PowerManager.PARTIAL_WAKE_LOCK, "MyApp:VoiceCall");
wakeLock.acquire(10*60*1000L /*10分钟*/);

// 根据网络状态调整编码参数
if (isNetworkPoor) {
    encoder.setVideoBitrate(1000000); // 降低码率
    adjustFrameRate(15); 
}

五、调试与监控

5.1 WebRTC统计接口

peerConnection.getStats(new StatsObserver() {
    @Override
    public void onComplete(StatsReport[] reports) {
        for (StatsReport report : reports) {
            if (report.type.equals("ssrc")) {
                // 获取音频流统计信息
                Log.d("Stats", "丢包率: " + report.values.get("packetsLost"));
            }
        }
    }
});

5.2 关键日志标记

# 采集延迟
D/AudioRecorder: Frame captured (seq=325, ts=158746532)

# 网络事件
W/Network: Packet lost detected, seq=1234, enabling FEC

# 播放状态
I/AudioTrack: Buffer underrun, inserting 20ms comfort noise

六、总结

实现高质量的实时语音通话需要Android开发者深入掌握以下核心技术:

  • 低延迟音频流水线:从采集到播放的端到端优化
  • 自适应网络传输:UDP+前向纠错的平衡艺术
  • 时钟同步机制:RTP时间戳与本地播放的精准对齐

未来演进方向:

  • 基于AI的网络预测(BWE 2.0)
  • 端侧神经网络降噪(RNNoise)
  • 5G网络下的超低延迟优化(<100ms)

建议进一步研究:

掌握这些核心技术后,开发者可以构建出媲美商业级应用的实时通信系统。希望本文能为各位Android开发者在实时音视频领域提供有价值的参考。


剖析部分至此结束,下面是实例部分

一、音频流式传输原理剖析

1. 音频分片机制

// 音频采集线程
class AudioCaptureThread extends Thread {
    private static final int SAMPLE_RATE = 48000; // 48kHz采样率
    private static final int FRAME_DURATION = 20; // 20ms帧间隔
    private static final int FRAME_SIZE = (SAMPLE_RATE * FRAME_DURATION) / 1000; // 960采样点

    @Override
    public void run() {
        AudioRecord recorder = createAudioRecord();
        ByteBuffer buffer = ByteBuffer.allocateDirect(FRAME_SIZE * 2); // 16bit采样
        
        recorder.startRecording();
        while (isRunning) {
            // 读取20ms的PCM数据
            int readBytes = recorder.read(buffer, FRAME_SIZE * 2);
            
            // 添加RTP头部(时间戳+序号)
            RtpPacket packet = new RtpPacket();
            packet.timestamp = SystemClock.elapsedRealtimeNanos() / 1000;
            packet.sequence = nextSequenceNumber();
            packet.payload = buffer.array();
            
            // 立即发送数据包
            networkSender.send(packet);
            
            buffer.rewind(); // 重用缓冲区
        }
    }
    
    private AudioRecord createAudioRecord() {
        return new AudioRecord.Builder()
            .setAudioFormat(new AudioFormat.Builder()
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .setSampleRate(SAMPLE_RATE)
                .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
                .build())
            .setBufferSizeInBytes(FRAME_SIZE * 4) // 双缓冲
            .build();
    }
}

关键原理说明:

  • 时间戳精度:采用微秒级时间戳(elapsedRealtimeNanos()/1000)确保时序精度
  • 环形缓冲区:DirectByteBuffer 重用避免内存抖动
  • 实时发送:每个 20ms 数据包立即发送,无需等待前序确认

2. 实时播放机制

class AudioPlaybackThread extends Thread {
    private static final int JITTER_BUFFER_DEPTH = 5; // 100ms缓冲深度
    private final PriorityBlockingQueue<RtpPacket> buffer = 
        new PriorityBlockingQueue<>(50, Comparator.comparingLong(p -> p.timestamp));
    
    private AudioTrack audioTrack;
    private long lastPlayedTimestamp = 0;

    @Override
    public void run() {
        audioTrack = createAudioTrack();
        audioTrack.play();
        
        while (isRunning) {
            RtpPacket packet = waitForNextPacket();
            writeToAudioTrack(packet);
            updateTimeline(packet);
        }
    }
    
    private RtpPacket waitForNextPacket() {
        if (buffer.size() < JITTER_BUFFER_DEPTH) {
            // 缓冲不足时插入静音包
            return generateSilencePacket();
        }
        
        return buffer.poll(20, TimeUnit.MILLISECONDS); // 阻塞等待
    }
    
    private void writeToAudioTrack(RtpPacket packet) {
        // 抖动补偿计算
        long expectedTimestamp = lastPlayedTimestamp + 20000; // 20ms间隔
        long timestampDelta = packet.timestamp - expectedTimestamp;
        
        if (timestampDelta > 50000) { // 超过50ms延迟
            resetPlayback(); // 重置时间线
        }
        
        audioTrack.write(packet.payload, 0, packet.payload.length);
        lastPlayedTimestamp = packet.timestamp;
    }
    
    private AudioTrack createAudioTrack() {
        return new AudioTrack.Builder()
            .setAudioFormat(new AudioFormat.Builder()
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .setSampleRate(SAMPLE_RATE)
                .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                .build())
            .setBufferSizeInBytes(FRAME_SIZE * 4)
            .setTransferMode(AudioTrack.MODE_STREAM)
            .build();
    }
}

核心算法解析:

  • 自适应抖动缓冲:根据网络状况动态调整缓冲深度
  • 时间线同步:通过时间戳差值检测网络延迟突变
  • 静音补偿:在丢包时生成舒适噪声保持播放连续性

二、网络传输层深度实现

1. UDP 封装优化

class UdpSender {
    private static final int MAX_RETRIES = 2;
    private static final int MTU = 1400; // 典型移动网络MTU
    
    private final DatagramChannel channel;
    private final InetSocketAddress remoteAddress;

    void send(RtpPacket packet) {
        ByteBuffer buffer = ByteBuffer.wrap(packet.serialize());
        
        // 分片发送(应对MTU限制)
        while (buffer.hasRemaining()) {
            int bytesToSend = Math.min(buffer.remaining(), MTU);
            ByteBuffer slice = buffer.slice();
            slice.limit(bytesToSend);
            
            sendWithRetry(slice);
            buffer.position(buffer.position() + bytesToSend);
        }
    }
    
    private void sendWithRetry(ByteBuffer data) {
        int attempt = 0;
        while (attempt <= MAX_RETRIES) {
            try {
                channel.send(data, remoteAddress);
                return;
            } catch (IOException e) {
                if (++attempt > MAX_RETRIES) {
                    reportNetworkError(e);
                }
            }
        }
    }
}

关键技术点:

  • MTU 适配:自动分片避免 IP 层分片
  • 有限重试:防止过度重传增加延迟
  • 非阻塞 IO:使用 NIO DatagramChannel 提升性能

2. 前向纠错实现

import com.googlecode.javaewah.EWAHCompressedBitmap;
import com.googlecode.javaewah.IntIterator;

class FecEncoder {
    // Reed - Solomon(5,3)编码配置
    private static final int DATA_SHARDS = 3;
    private static final int PARITY_SHARDS = 2;
    private static final int TOTAL_SHARDS = DATA_SHARDS + PARITY_SHARDS;
    private final RSCodec codec = new RSCodec(DATA_SHARDS, PARITY_SHARDS);

    public List<byte[]> encode(byte[] input) {
        byte[][] shards = splitIntoShards(input);
        codec.encodeParity(shards, 0, DATA_SHARDS);
        List<byte[]> result = new ArrayList<>();
        for (byte[] shard : shards) {
            result.add(shard);
        }
        return result;
    }

    private byte[][] splitIntoShards(byte[] data) {
        int shardSize = (data.length + DATA_SHARDS - 1) / DATA_SHARDS;
        byte[][] shards = new byte[TOTAL_SHARDS][shardSize];

        for (int i = 0; i < DATA_SHARDS; i++) {
            int start = i * shardSize;
            int end = Math.min(start + shardSize, data.length);
            System.arraycopy(data, start, shards[i], 0, end - start);
        }
        return shards;
    }
}

数学原理:

  • 使用 Reed-Solomon 纠错码,可恢复任意 2 个分片的丢失
  • 编码效率:3 个数据分片 + 2 个校验分片,可容忍 40% 的随机丢包

三、音频处理核心技术

1. 回声消除实现,下面是CPP代码,有ai加持

// 使用WebRTC AEC模块的JNI接口
extern "C" JNIEXPORT void JNICALL
Java_com_example_voice_AecProcessor_processFrame(
    JNIEnv* env,
    jobject thiz,
    jshortArray micData,
    jshortArray speakerData) {
    
    webrtc::EchoCancellation* aec = GetAecInstance();
    
    jshort* mic = env->GetShortArrayElements(micData, 0);
    jshort* speaker = env->GetShortArrayElements(speakerData, 0);
    
    // 执行AEC处理
    aec->ProcessRenderAudio(speaker, FRAME_SIZE);
    aec->ProcessCaptureAudio(mic, FRAME_SIZE, 0);
    
    env->ReleaseShortArrayElements(micData, mic, 0);
    env->ReleaseShortArrayElements(speakerData, speaker, 0);
}

算法流程:

  • 记录扬声器输出信号(参考信号)
  • 使用自适应滤波器建模声学路径
  • 从麦克风信号中减去估计的回声成分

2. 动态码率调整,包含网络评估方法

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

class BitrateController {
    private int currentBitrate = 1000000; // 初始1Mbps
    private final NetworkMonitor networkMonitor;
    private final List<BitrateListener> listeners = new ArrayList<>();
    private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

    public BitrateController(NetworkMonitor networkMonitor) {
        this.networkMonitor = networkMonitor;
        executorService.scheduleAtFixedRate(() -> {
            int quality = calculateNetworkQuality();
            adjustBitrate(quality);
        }, 0, 2, TimeUnit.SECONDS);
    }

    private int calculateNetworkQuality() {
        NetworkStatus status = networkMonitor.getLatestStatus();
        if (status instanceof NetworkGood) {
            return 90;
        } else if (status instanceof NetworkFair) {
            return 70;
        } else if (status instanceof NetworkPoor) {
            return 40;
        }
        return 50;
    }

    private void adjustBitrate(int quality) {
        int newBitrate;
        if (quality > 80) {
            newBitrate = (int) (currentBitrate * 1.2);
        } else if (quality < 40) {
            newBitrate = (int) (currentBitrate * 0.7);
        } else {
            newBitrate = currentBitrate;
        }
        newBitrate = Math.max(100000, Math.min(2000000, newBitrate));

        if (newBitrate != currentBitrate) {
            currentBitrate = newBitrate;
            for (BitrateListener listener : listeners) {
                listener.onBitrateChanged(newBitrate);
            }
        }
    }

    public void addBitrateListener(BitrateListener listener) {
        listeners.add(listener);
    }

    public void removeBitrateListener(BitrateListener listener) {
        listeners.remove(listener);
    }
}

interface BitrateListener {
    void onBitrateChanged(int newBitrate);
}

class NetworkMonitor {
    private int rtt = 100; // 毫秒
    private float lossRate = 0f; // 丢包率
    private int jitter = 50; // 抖动

    public NetworkStatus getLatestStatus() {
        if (lossRate > 0.2f) {
            return new NetworkPoor();
        } else if (rtt > 300) {
            return new NetworkFair();
        }
        return new NetworkGood();
    }

    public void setRtt(int rtt) {
        this.rtt = rtt;
    }

    public void setLossRate(float lossRate) {
        this.lossRate = lossRate;
    }

    public void setJitter(int jitter) {
        this.jitter = jitter;
    }
}

class NetworkGood implements NetworkStatus {}
class NetworkFair implements NetworkStatus {}
class NetworkPoor implements NetworkStatus {}
interface NetworkStatus {}

四、完整系统时序分析

sequenceDiagram
    participant A as 发送端
    participant B as 网络
    participant C as 接收端
    
    A->>B: 发送数据包1(seq=1, ts=100)
    A->>B: 发送数据包2(seq=2, ts=120)
    B--xC: 包2丢失
    A->>B: 发送数据包3(seq=3, ts=140)
    C->>C: 检测到seq=2缺失
    C->>B: 发送NACK重传请求
    B->>C: 重传数据包2
    C->>C: 按时间戳排序[1,3,2]
    C->>C: 调整播放顺序为[1,2,3]

我的主题貌似解析不了,请看下图

关键时序说明:

  • 接收端通过时间戳检测乱序包
  • 选择性重传机制(NACK)保证关键数据
  • 播放线程按时间戳重新排序

五、性能优化实战

1. 内存优化技巧

// 使用对象池减少GC
public class RtpPacketPool {
    private static final int MAX_POOL_SIZE = 50;
    private static final Queue<RtpPacket> pool = new ConcurrentLinkedQueue<>();
    
    public static RtpPacket obtain() {
        RtpPacket packet = pool.poll();
        return packet != null ? packet : new RtpPacket();
    }
    
    public static void recycle(RtpPacket packet) {
        if (pool.size() < MAX_POOL_SIZE) {
            packet.reset();
            pool.offer(packet);
        }
    }
}

// 使用示例
void processPacket(byte[] data) {
    RtpPacket packet = RtpPacketPool.obtain();
    packet.parse(data);
    // ...处理逻辑...
    RtpPacketPool.recycle(packet);
}

2. 线程优先级调整

// 设置实时音频线程优先级
private void setThreadPriority() {
    Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
    
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        PerformanceHintManager perfHintManager = 
            (PerformanceHintManager) context.getSystemService(Context.PERFORMANCE_HINT_SERVICE);
        
        SessionParams params = new SessionParams(
            PerformanceHintManager.SESSION_TYPE_CPU_LOAD,
            new CpuRange(70, 100));
        
        PerformanceHintManager.Session session = 
            perfHintManager.createSession(params);
        
        session.reportActualWorkDuration(500_000); // 500ms周期
    }
}

优化效果:

  • 音频线程调度延迟降低 30-50%
  • GC 暂停时间减少 80%
  • CPU 利用率提升 20%

需要更深入某个技术点的实现细节可以随时告知!

初体验的问题

作为一个前端新手,我决定用 Astro 框架重构个人博客。听说它的静态生成能力和 Partial Hydration 特性可以让网站又快又省资源,这对我这种追求极简的写作者来说简直完美。本地开发时一切顺利,热更新响应迅速,Markdown 渲染流畅。但当我部署到 Vercel 后,用户反馈点击文章链接需要等待 3-5 秒才能看到内容。整个跳转给我的感觉就是卡卡的 —— 明明用了静态站点生成,为什么会有这个诡异的延迟?

打开 Network 面板重新加载页面,发现:

HTML 文件加载仅需 100ms,但主内容渲染延迟了 2.3s;页面加载时触发了多个;Hydration 事件客户端 ;JS 体积达到 420KB…

然后我大概就知道问题在哪里了。

简单地说你可以理解为,本来一次点击,浏览器从A页面跳转到B页面,无论是A页面的消失还是B页面的逐步呈现,这个过程都没有被忽略。Astro的部分渲染,在B页面被完全加载出来之前都停留在A页面,他需要等待B页面的不同内容加载之后刷新视图,呈现B页面的内容。

另外我发现这个特行可以让你实现类似Android里的共享元素的炫酷效果。

我意识到问题可能与 Astro 的核心特性有关:

1. 过度使用客户端组件

在文章页模板中,我为了实现 “阅读进度条” 功能,错误地使用了client:load指令:

<!-- 错误示范 --> <ReadingProgress client:load />

这导致整个组件在页面加载时立即执行 Hydration,阻塞了主线程。

2. 动态路由的 Hydration 策略

我的文章路由采用动态参数[slug].astro,默认配置下:

// astro.config.mjs(原配置) export default defineConfig({ experimental: { islands: false } });

这导致 Astro 在客户端重新渲染整个页面,而非仅更新必要部分。

3. 资源加载未优化

未压缩的图片平均大小 300KB;未启用 Gzip;第三方字体文件阻塞渲染。

这里图片我是引用的第三方图库,虽然没有加preload,但是速度其实还算可以。不过多张图片引用也会影响速度。

尤其要说一下这个字体,我最开始测试的,google fonts的访问速度怎么会比loli.net要快这么多?难道是国内也有服务器?

后来我发现了问题。

google fonts引用的css,是切割好的字体文件。也就是这个css可以理解为是一个目录,里面有分好包的实际字体文件(目前常用方法),然后这个css会分批引用实际字体文件。给👴🏻整无语了。一整个页面就在那里等它这个字体加载。

系统性优化方案

经过调试,我逐步实施了以下解决方案:

1. 组件 Hydration 策略优化

<!-- 正确写法:仅当滚动到可见区域时Hydrate --> <ReadingProgress client:visible /> <!-- 完全静态组件 --> <ArticleMeta server />

2. 配置调整

3. 资源加载优化

使用astro-imagetools自动压缩图片。其实直接添加懒加载就可以了。

4. 给所有二进制文件添加preload

优化后数据

  • 优化后:首字节时间(TTFB)从 800ms 降至 120ms
  • 最大内容渲染时间(LCP)从 2.8s 降至 700ms
  • 客户端 JS 体积减少至 180KB(js问题不大)

最后

静态站点生成不等于自动高性能

前端性能优化需要系统性思维

Astro 的强大在于合理利用其特性,而非盲目使用

现在,我的博客不仅加载速度提升了,Lighthouse 98 XD。

更重要的是,我学会了如何通过 Chrome DevTools 和 Astro 内置工具进行性能分析。如果你也遇到类似问题,可以参考一下我上面的方案。(避雷Google fonts)

2025年3月14日更新:

没想到ruanyifeng这周和我写一个话题的内容。

https://www.ruanyifeng.com/blog/2025/03/weekly-issue-341.html

TL;DR

我不认为低代码平台+ai会变香


最近在 X 上又刷到有人吹低代码平台,说它“未来可期”。作为一个写了十几年代码的老程序员,我实在忍不住想吐槽。2025 年 3 月的今天,低代码仍然在炒,但我怎么看都觉得它像个老掉牙的故事——多年前就有人做过,有人退场,活下来的也没多风光。开发者真要为它担心失业?我看未必。

低代码的炒作历史

低代码并不新鲜。十年前就有一堆“零代码”“低代码”平台冒出来,喊着“拖拖拽拽就能做应用”。OutSystems、Mendix 这类老面孔仍在,国内也涌现了不少平台,但看看,有多少已经悄无声息地退出了市场?活下来的,像 Appian 或者钉钉宜搭,日子也不算滋润。我认识一哥们儿用低代码搭了个库存系统,业务一调整就改不动,最后还是找我重写。

根据Gartner 2024年报告,全球低代码市场规模增速已从2021年的23.6%下降至7.2%

Forrester 报告中的结论:低代码项目平均维护成本比传统开发高 2.3 倍。

AI的加入与现实

最近,低代码平台开始加入 AI,宣传上功能似乎变强了,能自动生成 UI、调优逻辑等。听起来很吸引人,但我一想,这不扯吗?“好用”不是一开始就该有的吗?AI 是新添的没错,但作为一个开发工具,基础功能好用才是最基本的要求。加了 AI 就吹得天花乱坠,好像以前不好用是理所当然似的。更别提 AI 生成的内容,改起来还是老样子——要么直接用,要么手动调整,跟十年前改代码没啥两样。AI 救不了低代码的命,最多也就是个噱头。

对开发者的影响

有人说低代码抢初级开发者的饭碗。确实,简单的表单和内部工具能拖几下搞定,初级程序员可能有点压力。但对我这种习惯复杂逻辑的人来说,低代码根本动不了我。那些“80% 简单需求”它能凑合,剩下 20% 的高性能、高定制化活儿,还得靠我们。

低代码还老给我添乱。客户用它搞个半成品,跑来让我修 bug、调性能,比从头写还费劲。我见过公司拿低代码做了个原型,上线后卡得要死,最后还得重构。省了点时间,却留下一堆烂摊子,这算啥“革命”啊?

收费模式的高数题

再说说低代码的收费模式,真是让人无语。免费版的基础功能弱得可怜,比如最多建几个表、连个像样的 API 都不行,想干点正事儿就得掏钱。收费功能层级森严,基本每个平台都有“基础版”“专业版”“企业版”之类的分级,价格一级比一级离谱。举个例子,我试过一个平台,免费版连导出数据都不行,想加个自定义插件,得升到每月几百块的专业版。更别提什么“无限用户”“高级支持”了,全锁在最贵的套餐里。这哪是工具啊,简直是圈钱的套路机器。

未来?别指望它翻天

低代码的粉丝总爱说未来会更智能,靠 AI 翻身。我听了只想笑。AI 是牛,但低代码加了 AI 就能写分布式系统?能优化算法?我看悬。这么多年,低代码的套路我都看透了:炒一波概念,忽悠一波企业用户,然后慢慢淡出视线。活下来的,要么靠大厂撑腰(比如微软的 Power Apps),要么苟在小众市场,风光不到哪去。

什么“低代码 + 传统开发共存”的说法,我也懒得信。企业用低代码无非图快图便宜,可业务一复杂,它就成鸡肋,重写成本还更高。我宁可从头写个扎实的系统,省得回头返工。

结论:低代码的未来

在我眼里,低代码就是个“伪命题”。炒了这么多年,没真干掉谁,也没见哪个平台成了行业霸主。加个 AI 也救不了它,免费功能鸡肋,收费还一套一套的。开发者要失业?除非你只会写“Hello World”。对我来说,低代码顶多是个玩具,玩玩行,想靠它吃饭,我看悬。

所以,别被低代码的宣传唬住了。想在这行混好,还是老老实实练功——算法、架构、DevOps,这些真本事才是我们的护身符。低代码?让它继续炒吧,我码我的代码。

一,创建文件

下面的文件均创建在主题文件夹中。

1. 创建样式代码 comments.css (你可以把下面的内容放到style.css中)

.comment-container form{
    border: 0px;
    padding: 20px;
    border-radius: 10px;
    margin-top: 20px;
    margin-bottom: 20px;
    background-color: #f0f0f0;
}

.clearfix::after {
    content: "";
    display: table;
    clear: both;
}

#comments {
    list-style-type: none;
    padding: 0;
}

.comment-body {
    padding: 15px 20px;
    list-style-type: none!important;
    border-radius: 10px;
    margin: 2.5rem 0px;
    background-color: #f0f0f0;
}

.comment-header {
    display: flex;
    align-items: center;
    margin-bottom: 10px;
}

.avatar {
    height: 2.5rem;
    width: 2.5rem;
    border-radius: 50%;
    margin-right: 10px;
}

.comment-author {
    font-weight: bold;
    color: #333;
}

.comment-by-author {
    color: #007bff;
}

.comment-by-user {
    color: #28a745;
}

.comment-content {
    background-color: #f0f0f0;
    border-radius: 10px;
    padding: 5px 20px;
    background-color: #fefefe;
    margin-bottom: 10px;
}

.comment-meta {
    font-size: 0.8em;
    color: #888;
}

.comment-reply {
    cursor: pointer;
    color: #007bff;
}
.comment-reply:hover {
    text-decoration: underline;
}

.comment-child {
    margin-left: 30px;
}

.comment-list{
    padding: 0px;
    margin: 20px 0px 0px 0px;
    font-size: 0.9rem;
}

.form-control {
    width: 100%;
    padding: 15px;
    margin-bottom: 20px;
    border: 0px;
    background-color: #fefefe;
    box-sizing: border-box;
}

.submit {
  color: #090909;
  padding: 0.7em 1.7em;
  font-size: 18px;
  border-radius: 0.5em;
  width: 100%;
  background: #ffffff;
  cursor: pointer;
  border: 0;
}

.response {
    color: #888;
}

.lists-navigator {
    text-align: center;
    margin-top: 20px;
}

textarea {
    height: 10rem; 
    resize: none; 
    outline: none; 
}

form input,form textarea{
    font-family: 'Noto Sans SC',consolas,monospace;
    font-size: 0.8rem;
    border-radius: 10px;
    background: #ffffff;
}

创建 comments.php

  1. 以下为我的代码,请注意第一行的引用代码。引用的样式代码为上面的CSS样式.

如果你已经放置到style.css中,你可以删除下面代码的第一行代码。

如果你已经创建css文件,直接复制下面的内容即可。

<link rel="stylesheet" href="/usr/themes/final/comments.css">
<hr>
<h3>评论</h3>
<?php
function threadedComments($comments, $options) {
    $commentClass = '';
    if ($comments->authorId) {
        if ($comments->authorId == $comments->ownerId) {
            $commentClass .= ' comment-by-author';
        } else {
            $commentClass .= ' comment-by-user';
        }
    }

    $commentLevelClass = $comments->levels > 0 ? ' comment-child' : ' comment-parent';
    $depth = $comments->levels +1;

    if ($comments->url) {
        $author = '<a href="' . $comments->url . '"target="_blank"' . ' rel="external nofollow">' . $comments->author . '</a>';
    } else {
        $author = $comments->author;
    }
?>

<li id="li-<?php $comments->theId(); ?>" class="comment-body<?php
if ($depth > 1 && $depth < 3) {
    echo ' comment-child ';
    $comments->levelsAlt('comment-level-odd', ' comment-level-even');
}
else if( $depth > 2){
    echo ' comment-child2';
    $comments->levelsAlt(' comment-level-odd', ' comment-level-even');
}
else {
    echo ' comment-parent';
}
$comments->alt(' comment-odd', ' comment-even');
?>">
    <div id="<?php $comments->theId(); ?>">
        <?php
            $host = 'https://gravatar.loli.net';
            $url = '/avatar/';
            $size = '80';
            $default = 'mm';
            $rating = Helper::options()->commentsAvatarRating;
            $hash = md5(strtolower($comments->mail));
            $avatar = $host . $url . $hash . '?s=' . $size . '&r=' . $rating . '&d=' . $default;
        ?>
        <div class="comment-view" onclick="">
            <div class="comment-header">
                <img class="avatar" src="<?php echo $avatar ?>" width="<?php echo $size ?>" height="<?php echo $size ?>" />
                <div style="display: grid;">
                    <span class="comment-author<?php echo $commentClass; ?>"><?php echo $author; ?></span>
                    <time class="comment-time"><?php $comments->date('Y-M-j'); ?></time>
                </div>
            </div>
            <div class="comment-content"> <?php $comments->content(); ?></p>
            </div>
            <div class="comment-meta">
                <span class="comment-reply" data-no-instant><?php $comments->reply('Reply'); ?></span>
            </div>
        </div>
    </div>
    <?php if ($comments->children) { ?>
        <div class="comment-children">
            <?php $comments->threadedComments($options); ?>
        </div>
    <?php } ?>
</li>
<?php } ?>

<div id="<?php $this->respondId(); ?>" class="comment-container">
    <div id="comments" class="clearfix">
        <?php $this->comments()->to($comments); ?>
        <?php if($this->allow('comment')): ?>
        <form method="post" action="<?php $this->commentUrl() ?>" id="comment-form" class="comment-form" role="form" onsubmit ="getElementById('misubmit').disabled=true;return true;">
            <?php if(!$this->user->hasLogin()): ?>
            <div style="display: flex; gap: 10px;">

            <input type="text" name="author" maxlength="12" id="author" class="form-control input-control clearfix" placeholder="您的称谓(可选)" value="" required>
            
            <input type="url" name="url" id="url" class="form-control input-control clearfix" placeholder="站点地址(可选)" value="" <?php if ($this->options->commentsRequireURL): ?> required<?php endif; ?>>
            
            <input type="email" name="mail" id="mail" class="form-control input-control clearfix" placeholder="邮箱(可选)" value="" <?php if ($this->options->commentsRequireMail): ?> required<?php endif; ?>>
            
            </div>
            
            <?php endif; ?>

            <textarea name="text" id="textarea" class="form-control" placeholder="地址:https://banzhuanriji.com/
标题/称呼:搬砖日记
副标题/自我介绍:我的生活记录
icon/头像:https://banzhuanriji.com/img/icon.svg (可选)" required ><?php $this->remember('text',false); ?></textarea>

            <button type="submit" class="submit" id="misubmit">提交你的友链/评论</button>
            <?php $security = $this->widget('Widget_Security'); ?>
            <input type="hidden" name="_" value="<?php echo $security->getToken($this->request->getReferer())?>">
        </form>
        <?php else : ?>
            <span class="response">评论区暂时被关闭了</span>
        <?php endif; ?>

        <?php if ($comments->have()): ?>

        <?php $comments->listComments(); ?>

        <div class="lists-navigator clearfix">
            <?php $comments->pageNav('←','→','2','...'); ?>
        </div>

        <?php endif; ?>
    </div>
</div>

二,使用

在你需要的地方添加 <?php $this->need('comments.php'); ?>

在当今的网络环境中,静态网站托管服务变得越来越流行,尤其是对于开发者和博客作者来说。本文将介绍三种流行的免费静态网站托管服务:Vercel、GitHub Pages 和 Cloudflare Pages,并对它们的免费服务进行比较。

Vercel

Vercel 是一个专注于前端开发的托管平台,提供快速、可靠的静态网站托管服务。它的主要特点包括:

  • 全球 CDN:Vercel 在全球范围内拥有多个 CDN 节点,确保网站的快速加载。
  • 自定义域名:支持用户使用自定义域名,并提供自动部署功能。
  • 构建限制:每月带宽限制为 100GB,构建次数和构建时长也有限制,但整体上对个人用户来说相对宽松。
  • 速度:在国内访问速度较快,通常比 GitHub Pages 和 Cloudflare Pages 更具优势。

GitHub Pages

GitHub Pages 是 GitHub 提供的静态网站托管服务,适合开发者和开源项目。其特点包括:

  • 稳定性:作为全球最大的代码托管平台,GitHub Pages 的稳定性相对较高。
  • 自定义域名:支持用户使用自定义域名。
  • 访问速度:在国内访问速度一般,偶尔会出现访问问题。
  • 限制:每月流量限制为 100GB,单个文件大小限制为 100MB,仓库大小建议少于 5GB。

Cloudflare Pages

Cloudflare Pages 是 Cloudflare 推出的静态网站托管服务,旨在提供快速和安全的网站托管。其特点包括:

  • 全球 CDN:同样拥有全球 CDN 节点,确保快速加载。
  • 自定义域名:支持最多 10 个自定义域名。
  • 构建限制:每月可构建 500 次,文件数量限制为 2万,单个文件大小不得超过 25MB。
  • 速度:与 GitHub Pages 相似,但在国内的访问速度和稳定性一般。

免费服务对比

在比较这三种服务时,可以从以下几个方面进行分析:

  1. 访问速度

    • Vercel 的访问速度在国内表现最佳,通常比 GitHub Pages 和 Cloudflare Pages 更快。
    • GitHub Pages 和 Cloudflare Pages 的速度相似,但 GitHub Pages 的稳定性更好。
  2. 自定义域名支持

    • 三者均支持自定义域名,但 Cloudflare Pages 对域名数量有上限(最多 10 个)。
  3. 构建和流量限制

    • Vercel 每月带宽限制为 100GB,构建次数和时长有限制。
    • GitHub Pages 每月流量限制为 100GB,文件和仓库大小也有相应限制。
    • Cloudflare Pages 每月可构建 500 次,文件数量和大小限制较为严格。
  4. 适用场景

    • Vercel 适合需要快速加载和高稳定性的个人博客或项目。
    • GitHub Pages 适合开源项目和开发者,尤其是对百度收录有需求的用户。
    • Cloudflare Pages 适合需要使用 Cloudflare CDN 的用户,但在国内的表现可能不如 Vercel。

结论

综合来看,Vercel 是一个非常适合个人博客和前端项目的托管平台,尤其是在国内访问速度方面表现突出。GitHub Pages 则是一个稳定的选择,适合开源项目和开发者。Cloudflare Pages 虽然提供了强大的 CDN 支持,但在国内的访问速度和稳定性相对较弱。根据个人需求选择合适的平台,将有助于提升网站的访问体验。


如果你想试一下你所在的地区访问各服务的速度,你可以使用以下数据

无域名,平台部署测试域名访问

域名访问,由Cloudflare进行DNS解析,无CDN

以上引用内容来自:https://github.com/hoytzhang/static-pages-test
你可以使用 https://www.itdog.cn/http/ 来进行访问对比