針對網絡音頻播放中「下載速度不穩導致卡頓」的問題,本文實現一套基於環形緩衝區的多線程非阻塞下載器,結合 HTTP Range 請求實現斷點續傳與動態線程調度,顯著提升弱網環境下的播放流暢度。
read(), seek()),供解碼器按需拉取。buf[] 作為鎖,wait()/notify() 協調生產消費;buf_bytes[] 記錄每塊有效長度。public final class HttpReader {
private HttpURLConnection httpConnection;
private InputStream in_stream;
private long cur_pos;
private final int connect_timeout, read_timeout;
public HttpReader(URL u, int connectTimeout, int readTimeout) { ... }
// 支援斷點續傳的 seek
public void seek(long start_pos) throws IOException {
if (start_pos == cur_pos && in_stream != null) return;
closeConnection();
httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setConnectTimeout(connect_timeout);
httpConnection.setReadTimeout(read_timeout);
httpConnection.setRequestProperty("Range", "bytes=" + start_pos + "-");
int responseCode = httpConnection.getResponseCode();
if (responseCode < 200 || responseCode >= 300) throw new IOException("HTTP " + responseCode);
in_stream = httpConnection.getInputStream();
cur_pos = start_pos;
}
public int read(byte[] b, int off, int len) throws IOException { ... }
public int getData(byte[] b, int off, int len) throws IOException { ... } // 保證讀滿 len
public void close() { ... }
}
public final class Writer implements Runnable {
private final HttpReader hr;
private final IWriterCallBack icb;
private final byte[] buf;
protected int index; // 緩衝區塊索引
protected long start_pos; // 文件偏移量
private int await_count; // 用於判斷是否縮容
public void run() {
while (alive) {
// 1. 互斥等待空閒塊 (icb.tryWriting)
// 2. hr.seek(start_pos) 定位
// 3. 循環讀取填滿一塊 (32KB)
// 4. icb.updateBuffer(index, actualBytes) 通知消費者
// 異常重試 MAX_RETRY 次
}
icb.updateWriterCount(); // 線程退出計數
}
}
public final class BuffRandAcceURL implements IRandomAccess, IWriterCallBack {
// 環形緩衝區常量
static final int UNIT_LENGTH_BITS = 15; // 32KB
static final int UNIT_LENGTH = 1 << UNIT_LENGTH_BITS;
static final int BUF_LENGTH = UNIT_LENGTH << 4; // 16 塊 = 512KB
static final int UNIT_COUNT = 16;
private final byte[] buf = new byte[BUF_LENGTH];
private final int[] buf_bytes = new int[UNIT_COUNT]; // 每塊有效長度
private int buf_index = 0; // 下一個寫入塊索引
private long alloc_pos = 0; // 已分配文件位置
private long file_pointer = 0; // 讀指針
private int read_pos = 0;
private int fill_bytes = 0; // 緩衝區總有效字節
private int writer_count; // 存活寫線程數
private int await_count; // 等待計數器
// IWriterCallBack 實現
public synchronized boolean tryWriting(Writer w) throws InterruptedException {
await_count++;
while (buf_bytes[buf_index] != 0 && alive) wait(); // 等待空塊
// 動態縮容邏輯:若速度足夠且等待輪數達標,返回 false 讓線程退出
if (writer_count > 1 && w.await_count >= await_count && w.await_count >= writer_count) return false;
w.start_pos = alloc_pos;
w.index = buf_index;
alloc_pos += UNIT_LENGTH;
buf_index = (buf_index + 1) % UNIT_COUNT;
await_count--;
return alive;
}
public void updateBuffer(int i, int len) {
synchronized (buf) {
buf_bytes[i] = len;
fill_bytes += len;
buf.notify();
}
}
// ... updateWriterCount, terminateWriters, notifyWriter
// IRandomAccess 讀取實現
public int read(byte[] b, int off, int len) throws Exception {
len = Math.min(len, UNIT_LENGTH);
int i = read_pos >> UNIT_LENGTH_BITS;
if (try_reading(i, len) < len || !alive) return -1; // 等待數據就緒
// 環形拷貝邏輯 (處理跨塊邊界)
int tail = BUF_LENGTH - read_pos;
if (tail < len) {
System.arraycopy(buf, read_pos, b, off, tail);
System.arraycopy(buf, 0, b, off + tail, len - tail);
} else {
System.arraycopy(buf, read_pos, b, off, len);
}
// 更新指針與計數
fill_bytes -= len;
file_pointer += len;
read_pos = (read_pos + len) & (BUF_LENGTH - 1);
buf_bytes[i] -= len;
if (buf_bytes[i] <= 0) notifyWriter();
return len;
}
private int try_reading(int i, int len) throws InterruptedException {
// 等待當前塊與下一塊合計數據足夠
while ((buf_bytes[i] == 0 ? 0 : buf_bytes[i] + buf_bytes[(i+1)%UNIT_COUNT]) < len) {
if (writer_count == 0 || !alive) return -1;
try_cache(); // 預緩衝邏輯
}
return len;
}
}
buf[] 同時作為緩衝區存儲與同步鎖,避免多鎖死鎖。tryWriting 同步獲取空塊,同一時刻只有一線程分配塊。buf_bytes[] 與 buf[],寫線程寫入後 notify(),無環路等待。await_count 機制讓多餘線程主動退出,減少競爭。TagThread),不阻塞主流程。完整代碼包含
HttpReader、Writer、BuffRandAcceURL、IRandomAccess、IWriterCallBack五大類,可直接集成至 Java 音視頻播放器項目。