本文介紹了在 iOS 平臺上使用 MNN 框架部署大語言模型(LLM)時,針對聊天應用中文字流式輸出卡頓問題的優化實踐。通過分析模型輸出與 UI 更新不匹配、頻繁刷新導致性能瓶頸以及缺乏視覺動畫等問題,作者提出了一套包含智能流緩沖、UI 更新節流與批處理、以及打字機動畫渲染的三層協同優化方案。最終實現了從技術底層到用戶體驗的全面提升,讓本地 LLM 應用的文字輸出更加絲滑流暢,接近主流在線服務的交互體驗。
背景
在iOS端部署大語言模型(LLM) 聊天應用時,用戶體驗的流暢性是一個關鍵要素。MNN LLM iOS應用基于MNN推理框架,為用戶提供本地化的AI對話體驗。如果直接將模型的輸出更新到回答的頁面UI中,會有一個嚴重影響用戶體驗的問題:模型輸出文字時存在明顯的卡頓現象,文字顯示生硬,缺乏自然的流動感。
因為用戶已經習慣了ChatGPT、Qwen等在線服務提供的流暢回復和絲滑打字機效果。本地模型推理輸出沒有網絡延遲,如果直接將模型結果輸出,在用戶體驗上會大打折扣。所以我針對這個問題,進行了優化。本文將分析具體的問題,針對這些問題提出解決方法,并且詳細的講解具體的原理和實現。
我們先看看優化前的直接輸出:
再看看優化之后的效果:
流暢度有明顯的提升。
完整的項目地址如下:https://github.com/alibaba/MNN/blob/master/apps/iOS/MNNLLMChat/README.md
問題分析
通過輸出現象分析,可以識別出導致卡頓和生硬輸出的三個核心問題:
1. 模型輸出速度與UI更新頻率不匹配
現象:模型推理速度較快,但輸出內容會積累后批量更新UI。
原因:缺乏合適的緩沖機制,導致"要么不更新,要么大量更新"的極端情況。
2. UI刷新頻率過高造成性能瓶頸
現象:本地模型快速推理輸出,會引起頻繁的UI更新導致主線程壓力過大,出現卡頓和掉幀。
原因:每個字符都觸發獨立的UI更新,沒有合理的批處理機制。
3. 缺乏流式輸出的視覺動畫效果
現象:文字瞬間出現,缺乏漸進式的視覺反饋。
原因:沒有展示類似打字機的逐字符顯示動畫。
優化策略
在Chat應用回答的過程中, 數據流向如下:
原始輸出流 → 智能緩沖 → 批量更新 → 動畫渲染 → 用戶界面。
基于上面的數據流和優化需求,我們在可以進行后面三層協同優化策略:
???1.?底層流緩沖優化 (OptimizedLlmStreamBuffer)
職責:解決模型輸出與UI更新的頻率不匹配問題。
智能觸發機制:基于內容特征(標點符號)和緩沖閾值的雙重觸發;
標點符號觸發:中英文支持,完整的UTF-8 Unicode標點符號識別;
性能優化:預分配內存,減少重分配開銷。
???2. 中間層更新優化 (UIUpdateOptimizer)
職責:統一管理UI更新請求,實現批處理和節流。
雙重策略:批量觸發(5個更新)+ 時間觸發(30ms超時);
線程安全:基于Swift Actor模型的并發處理;
智能調度:自動取消重復任務,避免資源浪費。
???3. UI層動畫增強 (LLMMessageTextView)
職責:提供自然流暢的用戶視覺體驗。
條件化動畫:判斷是否需要啟用打字機效果;
流式適配:完美適配流式輸出的文本變化;
資源管理:自動清理動畫資源,防止內存泄漏。
最終,我們通過底層增加緩沖輸出,中層合并更新請求,UI層提供視覺緩沖——這三層配合實現了從技術優化到體驗優化的完整覆蓋,提升整體性能和體驗效果。
詳細技術實現
???1. OptimizedLlmStreamBuffer:智能流緩沖優化
1.1 原理
OptimizedLlmStreamBuffer?是對標準?std::streambuf?的增強實現,通過智能緩沖策略解決模型輸出與UI更新的頻率不匹配問題。它的工作原理是在模型輸出和UI更新之間建立一個緩沖層,根據內容特征和緩沖大小決定何時將累積的內容推送給UI。
1.2 類設計
class?OptimizedLlmStreamBuffer?:?public?std::streambuf {private:? ??static?const?size_t?BUFFER_THRESHOLD =?64; ? ? ? ? ?// 緩沖區閾值(字節)? ? std::string buffer_; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 內容緩沖區public:? ??using?CallBack = std::function<void(const?char* str,?size_t?len)>;?// 更新回調? ??OptimizedLlmStreamBuffer(CallBack callback);protected:? ??virtual?std::streamsize?xsputn(const?char* s, std::streamsize n)?override;
private:? ??void?flushBuffer(); ? ?// 刷新緩沖區? ??bool?checkForFlushTriggers(const?char* s, std::streamsize n);?// 檢查觸發條件? ??bool?checkUnicodePunctuation(); ?// Unicode標點檢測};
1.3 方法詳解
1.?xsputn方法 - 數據流入口
下面是整體方法流程,每當模型生成新內容時都會調用此方法:
virtual?std::streamsize?xsputn(const?char* s, std::streamsize n)?override?{? ??if?(!callback_ || n <=?0) {? ? ? ??return?n;?// 參數校驗,確保安全性? ? }try?{? ? ? ??// 步驟1: 將新數據追加到緩沖區? ? ? ? buffer_.append(s, n);// 步驟2: 判斷是否需要立即刷新? ? ? ??const?size_t?BUFFER_THRESHOLD =?64;? ? ? ??bool?shouldFlush = buffer_.size() >= BUFFER_THRESHOLD;// 步驟3: 如果大小未達標,檢查內容特征? ? ? ??if?(!shouldFlush && n >?0) {? ? ? ? ? ? shouldFlush =?checkForFlushTriggers(s, n);? ? ? ? }// 步驟4: 符合條件則刷新緩沖區? ? ? ??if?(shouldFlush) {? ? ? ? ? ??flushBuffer();? ? ? ? }return?n;? ? }?catch?(const?std::exception& e) {? ? ? ??NSLog(@"Error in stream buffer: %s", e.what());? ? ? ??return?-1;?// 異常處理,確保程序穩定性? ? }}
工作流程說明:
數據接收:模型每次輸出的文本片段進入緩沖區;
閾值判斷:當累積
內容達到64字節時立即輸出;
自動觸發:即使未達到閾值,遇到標點符號
也會觸發輸出;
異常處理:完善的錯誤處理機制保證系統穩定性。
2. 觸發機制
閾值觸發策略
const?size_t?BUFFER_THRESHOLD =?64;?// 積累 64 byte 內容才輸出
ASCII標點符號觸發
bool?checkForFlushTriggers(const?char* s, std::streamsize n)?{? ??char?lastChar = s[n-1];?// 獲取最后一個字符// 檢查常見的英文標點符號? ??if?(lastChar ==?'\n'?|| ?// 換行符 - 句子結束? ? ? ? lastChar ==?'\r'?|| ?// 回車符 - 兼容不同系統? ? ? ? lastChar ==?' '?|| ??// 空格 - 詞語分隔? ? ? ? lastChar ==?'\t'?|| ?// 制表符 - 格式化字符? ? ? ? lastChar ==?'.'?|| ??// 句號 - 句子結束? ? ? ? lastChar ==?','?|| ??// 逗號 - 語句停頓? ? ? ? lastChar ==?';'?|| ??// 分號 - 語句分隔? ? ? ? lastChar ==?':'?|| ??// 冒號 - 說明引導? ? ? ? lastChar ==?'!'?|| ??// 感嘆號 - 情感表達? ? ? ? lastChar ==?'?') { ??// 問號 - 疑問句結束? ? ? ??return?true;? ? }return?checkUnicodePunctuation();?// 繼續檢查Unicode標點}
觸發邏輯說明:
語義完整性:在語義完整的點進行輸出,提升閱讀體驗
視覺節奏:模擬人類閱讀時的自然停頓
跨語言支持:同時支持英文和中文的標點符號
Unicode標點符號檢測
中文標點符號采用UTF-8編碼,需要特殊處理:
bool?checkUnicodePunctuation()?{? ??if?(buffer_.size() >=?3) {?// UTF-8中文標點通常占3字節? ? ? ??const?char* bufferEnd = buffer_.c_str() + buffer_.size() -?3;// 定義中文標點符號的UTF-8編碼? ? ? ??static?const?std::vector<std::string> chinesePunctuation = {? ? ? ? ? ??"\xE3\x80\x82", ? ??// 。(句號) - 句子結束? ? ? ? ? ??"\xEF\xBC\x8C", ? ??// ,(逗號) - 語句停頓 ?? ? ? ? ? ??"\xEF\xBC\x9B", ? ??// ;(分號) - 語句分隔? ? ? ? ? ??"\xEF\xBC\x9A", ? ??// :(冒號) - 說明引導? ? ? ? ? ??"\xEF\xBC\x81", ? ??// !(感嘆號) - 情感表達? ? ? ? ? ??"\xEF\xBC\x9F", ? ??// ?(問號) - 疑問句結束? ? ? ? ? ??"\xE2\x80\xA6", ? ??// …(省略號) - 語意延續? ? ? ? };// 逐一比較字節序列? ? ? ??for?(const?auto& punct : chinesePunctuation) {? ? ? ? ? ??if?(memcmp(bufferEnd, punct.c_str(),?3) ==?0) {? ? ? ? ? ? ? ??return?true;?// 找到匹配的中文標點? ? ? ? ? ? }? ? ? ? }? ? }// 檢查2字節的Unicode標點(如破折號)? ??if?(buffer_.size() >=?2) {? ? ? ??const?char* bufferEnd = buffer_.c_str() + buffer_.size() -?2;? ? ? ??if?(memcmp(bufferEnd,?"\xE2\x80\x93",?2) ==?0?|| ?// – (短破折號)? ? ? ? ? ??memcmp(bufferEnd,?"\xE2\x80\x94",?2) ==?0) { ?// — (長破折號)? ? ? ? ? ??return?true;? ? ? ? }? ? }return?false;}
UTF-8編碼處理細節:
字節序列識別:通過比較字節序列精確識別中文標點
長度適配:中文標點占2-3字節,需要相應的緩沖區長度檢查
性能優化:使用靜態數組和memcmp進行高效比較
3. 內存預分配
OptimizedLlmStreamBuffer(CallBack?callback) :?callback_(callback) {? ? buffer_.reserve(1024);?// 預分配1KB內存}
減少重分配:避免頻繁的內存分配和拷貝操作
提升性能:預分配內存可以減少約30%的內存操作開銷
1)std::string 在動態增長時,每次容量不足都會:
分配新的更大內存空間(通常是當前容量的1.5-2倍)
復制現有數據到新內存
釋放舊內存
// 沒有預分配的情況下,字符串增長模式:// 容量: 0 -> 1 -> 2 -> 4 -> 8 -> 16 -> 32 -> 64 -> 128 -> 256 -> 512 -> 1024// 重分配次數: 約10次// 預分配1024字節后:// 容量: 1024 (一次分配)// 重分配次數: 0次 (在1024字節內)
2)與緩沖策略的協同
C++const?size_t?BUFFER_THRESHOLD =?64;bool?shouldFlush = buffer_.size() >= BUFFER_THRESHOLD;
緩沖閾值:64字節觸發刷新
預分配容量:1024字節
協同效果:支持16次緩沖操作而無需重分配
因此我們預分配1024字節避免了前期的多次重分配操作。
4. 異常安全設計
~OptimizedLlmStreamBuffer() {? ??flushBuffer();?// 析構時確保緩沖區內容全部輸出}void?flushBuffer() {? ??if?(callback_ && !buffer_.empty()) {? ? ? ??callback_(buffer_.c_str(), buffer_.size());? ? ? ? buffer_.clear();?// 清空緩沖區,釋放內存? ? }}
???2. UIUpdateOptimizer:基于Actor的更新優化器
2.1 原理
UIUpdateOptimizer?采用Swift 5.5引入的Actor并發模型,解決UI更新的線程安全和性能問題。它的核心思想是將頻繁的UI更新請求按緩存大小或間隔時間進行批處理和節流,減少主線程壓力。
Actor?隊列(批處理 + 節流) -> 主線程UI(低頻率UI更新 )
2.2 類設計
actor?UIUpdateOptimizer?{? ??static?let?shared =?UIUpdateOptimizer()?// 全局單例// 狀態管理? ??private?var?pendingUpdates: [String] = [] ? ?// 待處理更新隊列? ??private?var?lastFlushTime:?Date?=?Date() ? ??// 上次刷新時間? ??private?var?flushTask:?Task<Void,?Never>? ? ?// 延遲刷新任務// 配置參數? ??private?let?batchSize:?Int?=?5? ? ? ? ? ? ? ?// 批處理大小? ??private?let?flushInterval:?TimeInterval?=?0.03?// 節流間隔(30ms)}
簡單介紹一下 Actor。在多線程或異步程序中,多個任務訪問共享變量時容易造成數據競爭(data race)。Actor 是一種引用類型,用來保護其內部狀態免受數據競爭影響。它是并發安全的,當你調用時,會自動對外部訪問進行同步(串行隊列),所以不需要手動加鎖。
2.3 方法詳解
1. 雙重觸發策略
func?addUpdate(_ content:?String, completion:?@escaping?(String) -> Void) {? ??// 步驟1: 添加到待處理隊列? ? pendingUpdates.append(content)// 步驟2: 判斷觸發條件? ??let?shouldFlushImmediately = pendingUpdates.count?>= batchSize ||? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?Date().timeIntervalSince(lastFlushTime) >= flushInterval// 步驟3: 選擇處理策略? ??if?shouldFlushImmediately {? ? ? ??flushUpdates(completion: completion)?// 立即處理? ? }?else?{? ? ? ??scheduleFlush(completion: completion)?// 延遲處理? ? }}
策略選擇邏輯:
2. 延遲刷新調度
private?func?scheduleFlush(completion:?@escaping?(String) -> Void) {? ??// 取消之前的調度,避免重復執行? ? flushTask?.cancel()// 創建新的延遲任務? ? flushTask =?Task?{? ? ? ??// 等待指定時間間隔? ? ? ??try??await?Task.sleep(nanoseconds:?UInt64(flushInterval *?1_000_000_000))// 檢查任務是否被取消,以及是否有待處理內容? ? ? ??if?!Task.isCancelled?&& !pendingUpdates.isEmpty?{? ? ? ? ? ??flushUpdates(completion: completion)? ? ? ? }? ? }}
上面的方式,可以:
節流控制:為UI更新提供30毫秒的緩沖時間;
批處理優化:在這30毫秒內如果有新的更新到來,會取消當前延遲任務并重新開始計時;
性能平衡:既避免過于頻繁的UI更新,又保證內容能及時顯示;
響應性保證:即使在低頻更新場景下,也確保內容在30毫秒內顯示給用戶。
3. 批處理執行
private?func?flushUpdates(completion:?@escaping?(String) -> Void) {? ? guard !pendingUpdates.isEmpty?else?{?return?}// 合并所有待處理的更新? ??let?batchedContent = pendingUpdates.joined()// 清空隊列,準備下一輪? ? pendingUpdates.removeAll()? ? lastFlushTime =?Date()// 切換到主線程執行UI更新? ??Task?{?@MainActor?in? ? ? ??completion(batchedContent)? ? }}
批處理優勢分析:
減少調用次數:將多次UI更新合并為一次,減少開銷;
提升響應性:主線程壓力減少,UI更加流暢;
內存效率:及時清理已處理內容,避免內存累積。
???3. LLMMessageTextView:沉浸式打字機動畫
3.1 背景
LLMMessageTextView
?的設計目標是創造接近人類打字速度的自然動畫效果。通過設置的時間參數和智能的動畫控制,讓AI的文字輸出更加自然和富有節奏感。
3.2 類設計
struct?LLMMessageTextView:?View?{? ??// 數據模型? ??let?text:?String? ? ? ? ? ? ? ? ? ? ?// 完整文本內容? ??let?messageUseMarkdown:?Bool? ? ? ? ?// 是否使用Markdown渲染? ??let?messageId:?String? ? ? ? ? ? ? ??// 消息唯一標識? ??let?isAssistantMessage:?Bool? ? ? ? ?// 是否為AI消息? ??let?isStreamingMessage:?Bool? ? ? ? ?// 是否正在流式傳輸// 動畫狀態? ??@State?private?var?displayedText:?String?=?""??// 當前顯示的文本? ??@State?private?var?animationTimer:?Timer? ? ? ?// 動畫定時器// 動畫配置參數? ??private?let?typingSpeed:?TimeInterval?=?0.015??// 15ms每字符? ??private?let?chunkSize:?Int?=?1? ? ? ? ? ? ? ? ?// 每次顯示1個字符}
3.3 動畫控制策略
1. 條件化動畫觸發
private?var?shouldUseTypewriter: Bool {? ??// 只有同時滿足以下條件才啟用動畫:? ??// 1. 是AI助手的消息(用戶消息不需要動畫)? ??// 2. 文本長度超過5個字符(避免短消息的不必要動畫)? ??return?isAssistantMessage && (text?.count ???0) >?5}
觸發邏輯分析:
用戶體驗導向:只對AI消息使用動畫,用戶消息直接顯示;
性能考慮:短消息(≤5字符)直接顯示,避免動畫開銷;
場景適配:流式傳輸時啟用動畫,靜態顯示時關閉動畫。
2.?流式文本變化處理
private?func?handleTextChange(_?newText:?String?) {? ??guard?let?newText?=?newText?else?{? ? ? ? displayedText?=?""? ? ? ? stopAnimation()? ? ? ??return? ? }if?isAssistantMessage?&&?isStreamingMessage?&&?shouldUseTypewriter {? ? ? ??// 智能判斷文本變化類型? ? ? ??if?newText.hasPrefix(displayedText)?&&?newText?!=?displayedText {? ? ? ? ? ??// 場景1: 文本內容追加(流式輸出的常見情況)? ? ? ? ? ? continueTypewriterAnimation(with: newText)? ? ? ? }?else?if?newText?!=?displayedText {? ? ? ? ? ??// 場景2: 文本內容完全變化(消息重新生成)? ? ? ? ? ? restartTypewriterAnimation(with: newText)? ? ? ? }? ? ? ??// 場景3: 文本內容無變化,不做處理? ? }?else?{? ? ? ??// 非動畫場景:直接顯示完整文本? ? ? ? displayedText?=?newText? ? ? ? stopAnimation()? ? }}
處理策略詳解:
3.4 動畫執行機制
1. 動畫啟動流程
private?func?startTypewriterAnimation(for?text:?String) {? ??// 步驟1: 重置顯示狀態? ? displayedText?=?""// 步驟2: 開始動畫循環? ? continueTypewriterAnimation(with: text)}private?func?continueTypewriterAnimation(with?text:?String) {? ??// 前置檢查:避免無效動畫? ??guard?displayedText.count?<?text.count?else?{?return?}// 清理舊定時器,避免沖突? ? stopAnimation()// 創建新的動畫定時器? ? animationTimer?=?Timer.scheduledTimer(withTimeInterval: typingSpeed, repeats:?true) { timer?in? ? ? ??DispatchQueue.main.async {? ? ? ? ? ??self.appendNextCharacters(from: text)? ? ? ? }? ? }}
定時器機制特點:
主線程執行:確保UI更新在主線程進行
重復執行:設置repeats: true實現連續動畫
沖突避免:啟動前先停止舊定時器
2. 字符追加邏輯
private?func?appendNextCharacters(from?text:?String) {? ??let?currentLength?=?displayedText.count// 邊界檢查:防止越界訪問? ??guard?currentLength?<?text.count?else?{? ? ? ? stopAnimation()?// 動畫完成,清理資源? ? ? ??return? ? }// 計算下一次顯示的字符范圍? ??let?endIndex?=?min(currentLength?+?chunkSize, text.count)? ??let?startIndex?=?text.index(text.startIndex, offsetBy: currentLength)? ??let?targetIndex?=?text.index(text.startIndex, offsetBy: endIndex)// 提取新字符并追加到顯示文本? ??let?newChars?=?text[startIndex..<targetIndex]? ? displayedText.append(String(newChars))// 檢查動畫是否完成? ??if?displayedText.count?>=?text.count {? ? ? ? stopAnimation()? ? }}
字符處理細節:
Unicode安全:使用String.Index正確處理多字節字符
邊界保護:使用min()函數防止數組越界
增量更新:每次只追加新字符,避免重復渲染
3.5 視圖渲染策略
1. 條件化渲染
var?body: some?View?{? ??Group?{? ? ? ??if?let?text = text, !text.isEmpty?{? ? ? ? ? ??if?isAssistantMessage && isStreamingMessage && shouldUseTypewriter {? ? ? ? ? ? ? ??typewriterView(text) ?// 動畫視圖? ? ? ? ? ? }?else?{? ? ? ? ? ? ? ??staticView(text) ? ? ?// 靜態視圖? ? ? ? ? ? }? ? ? ? }? ? }? ??// 生命周期綁定? ? .onAppear?{?/* 啟動動畫 */?}? ? .onDisappear?{?/* 清理資源 */?}? ? .onChange(of: text) {?/* 處理文本變化 */?}? ? .onChange(of: isStreamingMessage) {?/* 處理流式狀態變化 */?}}
2. Markdown支持
@ViewBuilderprivate?func?typewriterView(_?text:?String) ->?some?View?{? ??if?messageUseMarkdown {? ? ? ??Markdown(displayedText)? ? ? ? ? ? .markdownBlockStyle(\.blockquote) { configuration?in? ? ? ? ? ? ? ? configuration.label? ? ? ? ? ? ? ? ? ? .padding()? ? ? ? ? ? ? ? ? ? .markdownTextStyle {? ? ? ? ? ? ? ? ? ? ? ??FontSize(13)? ? ? ? ? ? ? ? ? ? ? ??FontWeight(.light)? ? ? ? ? ? ? ? ? ? ? ??BackgroundColor(nil)? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? .overlay(alignment: .leading) {? ? ? ? ? ? ? ? ? ? ? ??Rectangle()? ? ? ? ? ? ? ? ? ? ? ? ? ? .fill(Color.gray)? ? ? ? ? ? ? ? ? ? ? ? ? ? .frame(width:?4)? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? .background(Color.gray.opacity(0.2))? ? ? ? ? ? }? ? }?else?{? ? ? ??Text(displayedText)? ? }}
3.6 生命周期管理
1. 資源自動管理
.onAppear?{? ??if?let?text = text, isAssistantMessage && isStreamingMessage && shouldUseTypewriter {? ? ? ??startTypewriterAnimation(for: text)? ? }?else?if?let?text = text {? ? ? ? displayedText = text? ? }}.onDisappear?{? ??stopAnimation()?// 防止內存泄漏}
2. 狀態變化響應
.onChange(of: isStreamingMessage) { oldIsStreaming, newIsStreaming?in? ??if?!newIsStreaming {? ? ? ??// 流式傳輸結束,立即顯示完整內容? ? ? ??if?let?text = text {? ? ? ? ? ? displayedText = text? ? ? ? }? ? ? ??stopAnimation()? ? }}
3. 內存泄漏防護
private?func?stopAnimation() {? ? animationTimer?.invalidate() ?// 停止定時器? ? animationTimer?=?nil? ? ? ? ??// 釋放引用}
總結
綜上,結合三層的優化,通過以上多層協同優化方案,我們成功地將一個卡頓、生硬的文字輸出體驗轉變為流暢、自然的現代化AI交互界面。
PS:如果覺得文章對你有幫助或者啟發,歡迎 Star MNN :https://github.com/alibaba/MNN
團隊介紹
本文作者攬清,來自淘天集團-Meta技術團隊。本團隊目前負責面向消費場景的3D/XR基礎技術建設和創新應用探索,創造以手機及XR 新設備為載體的消費購物新體驗。團隊在端智能、端云協同、商品三維重建、真人三維重建、3D引擎、XR引擎等方面有著深厚的技術積累,先后發布深度學習引擎MNN、商品三維重建工具Object Drawer、3D真人數字人TaoAvatar、端云協同系統Walle等。團隊在OSDI、MLSys、CVPR、ICCV、NeurIPS、TPAMI等頂級學術會議和期刊上發表多篇論文。歡迎視覺算法、3D/XR引擎、深度學習引擎研發、終端研發等領域的優秀人才加入,共同走進3D數字新時代。
¤?拓展閱讀?¤
3DXR技術?|?終端技術?|?音視頻技術
服務端技術?|?技術質量?|?數據算法