<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>IOS · Nhân Tài Đức</title>
    <link>https://jared.lynskey.co.nz/vi/tags/ios/</link>
    <description>Những câu chuyện ngắn về kỹ thuật phần mềm và lãnh đạo đội ngũ kỹ sư — performance review, tuyển dụng và mở rộng team, CI/CD, giám sát DevOps và AI agent. Viết từ Seoul.</description>
    <generator>Hugo</generator>
    <language>vi</language>
    <copyright>© 2026 Nhân Tài Đức</copyright>
    <lastBuildDate>Sat, 07 Nov 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/vi/tags/ios/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Không cần Sentry tự host (self-hosted): nhận báo cáo crash React Native trên máy chủ của mình</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2026/2026-10-09-sentry-sdk-own-server/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2026/2026-10-09-sentry-sdk-own-server/</guid>
      <pubDate>Fri, 09 Oct 2026 00:00:00 +1300</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>React Native</category>
      <category>Expo</category>
      <category>Sentry</category>
      <category>Django</category>
      <category>Crash Reporting</category>
      <category>Self-Hosted</category>
      <category>iOS</category>
      <category>Android</category>
      <category>Privacy</category>
      <description>Không cần chạy Sentry tự host hay trả phí sentry.io: giữ SDK mã nguồn mở của Sentry, trỏ DSN về API Django của mình để nhận báo cáo crash React Native, rồi tự gom nhóm và cảnh báo.</description>
      <content:encoded><![CDATA[<p>Nếu bạn muốn tự nhận báo cáo crash (crash reporting) cho ứng dụng React Native, bạn không cần chạy máy chủ Sentry tự host (self-hosted), cũng không cần trả tiền cho sentry.io. SDK của Sentry là mã nguồn mở, và DSN của nó có thể trỏ tới bất kỳ máy chủ nào. Tôi trỏ nó về một endpoint trong API Django của mình; ở đó báo cáo được nhận, gom nhóm, gửi cảnh báo và lưu lại, không tốn thêm gì ngoài API vốn đã chạy sẵn. Thứ tôi chưa thay được là khôi phục ký hiệu (symbolication), nên stack trace vẫn chưa chỉ ra được dòng nào trong mã nguồn.</p>
<p>Cho tới tuần này, cách duy nhất để tôi biết ứng dụng của tôi bị sập là có ai đó nhắn cho tôi. Mà phần lớn mọi người không nhắn. Họ mở lại ứng dụng, và nếu nó sập thêm lần nữa thì họ thôi không mở nữa. Phía API thì đã có giám sát: mọi log mức ERROR đều được đẩy vào một kênh chat của đội. Nhưng một lần sập trên điện thoại thì không để lại dấu vết gì ở bất cứ chỗ nào tôi nhìn thấy được.</p>
<p>Thật ra ứng dụng đã có sẵn một cơ chế báo lỗi. Mỗi khi ứng dụng hiện một thông báo lỗi cho người dùng, nó gửi nội dung thông báo đó lên một endpoint nhỏ của API. Máy chủ ghi lại ở mức ERROR, nên lỗi đó cũng tới được kênh chat. Tôi thêm cơ chế này sau một lần thao tác xoá thực ra đã <em>thành công</em>, nhưng dòng đó vẫn nằm trong danh sách; người dùng bấm vào lần nữa và nhận được thông báo &ldquo;No Item matches the given query&rdquo;. Phía máy chủ chỉ ghi hai lỗi 404 bình thường và không báo gì cả. Cơ chế đó bao quát được những lỗi mà ứng dụng bắt được và hiện ra cho người dùng. Còn sập ứng dụng (crash) thì khác hẳn: chính đoạn JavaScript lẽ ra sẽ gửi báo cáo lại là thứ vừa chết.</p>
<p>Cách sửa hiển nhiên là dùng Sentry. Tôi rất thích SDK của Sentry; nhờ nó mà báo cáo sự cố trên React Native gần như là bài toán đã có lời giải. Điều tôi không muốn là thêm một nhà cung cấp nữa giữ dữ liệu người dùng, thêm một dòng trong chính sách quyền riêng tư, và thêm một hoá đơn cho một thứ mà với quy mô của chúng tôi thì nhiều lắm cũng chỉ vài báo cáo mỗi ngày. Vì vậy tôi giữ SDK và thay phần dịch vụ: DSN của ứng dụng trỏ về API của chính chúng tôi, và một phần nhỏ trong ứng dụng Django ở đó nhận những gì SDK gửi lên, gom nhóm, gửi cảnh báo và lưu lại.</p>
<p>Bài này kể cách hệ thống đó hoạt động, và cách tôi kiểm tra để chắc rằng một lần sập gốc (native crash) thật sự đi được tới máy chủ.</p>

<h2 class="relative group">Mẹo với DSN
    <div id="mẹo-với-dsn" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#m%e1%ba%b9o-v%e1%bb%9bi-dsn" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Một DSN của Sentry có dạng <code>https://&lt;public-key&gt;@&lt;host&gt;/&lt;project-id&gt;</code>. SDK biến nó thành một yêu cầu POST tới <code>https://&lt;host&gt;/api/&lt;project-id&gt;/envelope/</code>, với khoá nằm trong header <code>X-Sentry-Auth</code>. Không có chi tiết nào trong đó gắn riêng với sentry.io. Vì vậy DSN được đóng gói sẵn trong ứng dụng là:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">https://&lt;CRASH_REPORT_KEY&gt;@api.example.com/1</span></span></code></pre></div></div>
<p>và phía Django có một route khớp đúng với địa chỉ mà SDK sẽ gọi:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">path</span><span class="p">(</span><span class="s2">&#34;api/&lt;int:project_id&gt;/envelope/&#34;</span><span class="p">,</span> <span class="n">crash_envelope</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s2">&#34;crash_envelope&#34;</span><span class="p">),</span></span></span></code></pre></div></div>
<p>Khoá này nằm trong mọi bản build của ứng dụng, nên nó không phải bí mật. Nó tồn tại vì hai lý do: nếu một ngày endpoint bắt đầu hứng rác thì tôi có thể đổi khoá, và nếu máy chủ chưa cấu hình khoá thì endpoint sẽ từ chối tất cả thay vì nhận bừa mọi thứ:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">crash_envelope</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">project_id</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">expected</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">CRASH_REPORT_KEY</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">expected</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">Response</span><span class="p">({</span><span class="s2">&#34;detail&#34;</span><span class="p">:</span> <span class="s2">&#34;Crash reporting is not configured&#34;</span><span class="p">},</span> <span class="n">status</span><span class="o">=</span><span class="mi">503</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">hmac</span><span class="o">.</span><span class="n">compare_digest</span><span class="p">(</span><span class="n">_key</span><span class="p">(</span><span class="n">request</span><span class="p">)</span><span class="o">.</span><span class="n">encode</span><span class="p">(),</span> <span class="n">expected</span><span class="o">.</span><span class="n">encode</span><span class="p">()):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">Response</span><span class="p">({</span><span class="s2">&#34;detail&#34;</span><span class="p">:</span> <span class="s2">&#34;Unknown key&#34;</span><span class="p">},</span> <span class="n">status</span><span class="o">=</span><span class="mi">401</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span></span></span></code></pre></div></div>
<p>Hàm <code>_key</code> đọc giá trị <code>sentry_key=</code> trong header <code>X-Sentry-Auth</code>; nếu không có thì lấy từ query string, vì một số SDK đặt khoá ở đó.</p>

<h2 class="relative group">SDK gửi gì lên: envelope
    <div id="sdk-gửi-gì-lên-envelope" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#sdk-g%e1%bb%adi-g%c3%ac-l%c3%aan-envelope" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Định dạng truyền dữ liệu của Sentry là <a href="https://develop.sentry.dev/sdk/data-model/envelopes/"  target="_blank" rel="noreferrer">envelope</a>: một dòng JSON làm header, sau đó là bao nhiêu mục (item) cũng được, mỗi mục gồm một dòng JSON header rồi tới phần nội dung (payload). Nếu header của mục có trường <code>length</code> thì payload dài đúng bằng số byte đó; nếu không thì payload kéo dài tới ký tự xuống dòng kế tiếp.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;event_id&#34;:&#34;9ec79c33...&#34;,&#34;dsn&#34;:&#34;https://...@api.example.com/1&#34;}
</span></span><span class="line"><span class="cl">{&#34;type&#34;:&#34;event&#34;,&#34;length&#34;:4123}
</span></span><span class="line"><span class="cl">{ ...the event JSON, 4123 bytes... }
</span></span><span class="line"><span class="cl">{&#34;type&#34;:&#34;client_report&#34;}
</span></span><span class="line"><span class="cl">{&#34;timestamp&#34;:1791400000.5,&#34;discarded_events&#34;:[]}</span></span></code></pre></div></div>
<p>Có hai chi tiết khiến tôi vấp:</p>
<ul>
<li><strong>SDK gốc (native) nén gzip phần thân yêu cầu, còn SDK JavaScript thì không.</strong> Một lỗi JS mà <code>@sentry/react-native</code> bắt được sẽ tới dưới dạng văn bản thường, trong khi một lần sập gốc từ sentry-cocoa hoặc sentry-android tới kèm <code>Content-Encoding: gzip</code>. Bộ giải mã xử lý được gzip, deflate có bọc zlib và deflate thô, và dừng giải nén ở mức 1MB, vì một sự kiện sập chỉ nặng vài kilobyte, thứ gì lớn hơn thế thì không phải báo cáo sự cố.</li>
<li><strong>Payload có tiền tố độ dài có thể chứa ký tự xuống dòng.</strong> Nếu bạn tách theo <code>\n</code> thì sẽ làm hỏng mọi sự kiện mà SDK gốc gửi lên. Khi có <code>length</code> thì bắt buộc phải tôn trọng nó.</li>
</ul>
<p>Bộ phân tích cú pháp (parser) khá ngắn. Nó chỉ giữ các mục <code>event</code>; session, transaction, tệp đính kèm và client report đều được nhận rồi bỏ đi.</p>

<h2 class="relative group">Trả lời theo cách SDK hiểu được
    <div id="trả-lời-theo-cách-sdk-hiểu-được" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#tr%e1%ba%a3-l%e1%bb%9di-theo-c%c3%a1ch-sdk-hi%e1%bb%83u-%c4%91%c6%b0%e1%bb%a3c" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Phần này quan trọng hơn tôi tưởng. SDK giữ một hàng đợi trên đĩa và gửi lại bất cứ thứ gì nó không chắc là đã tới nơi, nên câu trả lời của máy chủ quyết định SDK có tiếp tục thử lại hay không:</p>
<ul>
<li><strong>200</strong> khi đã lưu, <em>hoặc khi cố ý bỏ đi</em>. Nếu tôi trả 4xx cho một mục session, SDK sẽ giữ nó lại và thử gửi mãi mãi.</li>
<li><strong>429 kèm <code>Retry-After</code></strong> khi bị giới hạn tần suất. Endpoint có giới hạn theo từng IP (120 lần mỗi giờ), vì một chiếc điện thoại bị sập liên tục sẽ gửi một báo cáo mỗi lần khởi động, và SDK lùi lại khi gặp 429 đúng như mình mong.</li>
<li><strong>400</strong> cho envelope sai định dạng, ghi log ở mức cảnh báo (warning) chứ không phải lỗi: một báo cáo hỏng chỉ cho biết có một client đang hỏng, và báo cáo tốt tiếp theo sẽ nói cái gì đã sập.</li>
</ul>
<p>SDK cũng gửi lại cùng một sự kiện nếu nó không chắc lần gửi đầu đã tới nơi, nên <code>event_id</code> có ràng buộc duy nhất (unique constraint) và một bản trùng lặp sẽ được bỏ qua lặng lẽ.</p>

<h2 class="relative group">Gom nhóm khi không có source map
    <div id="gom-nhóm-khi-không-có-source-map" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#gom-nh%c3%b3m-khi-kh%c3%b4ng-c%c3%b3-source-map" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Đây là chỗ một bộ nhận tự viết phải đưa ra lựa chọn mà Sentry vốn làm sẵn cho bạn. Sentry gom nhóm các lần sập theo stack trace. Nhưng bundle Hermes ở bản phát hành đã bị rút gọn (minify), nên tên các frame chỉ còn một chữ cái và vị trí dòng/cột thay đổi sau mỗi lần build. Nếu gom theo frame, cùng một lỗi sẽ mở ra một issue mới ở mỗi bản phát hành.</p>
<p>Khi chưa có source map (phần cuối bài sẽ nói thêm), thứ ổn định nhất trong một lỗi JavaScript là kiểu lỗi và thông điệp lỗi. Vì vậy dấu vân tay (fingerprint) là kiểu exception cộng với thông điệp, trong đó những phần thay đổi giữa các lần xuất hiện được che đi:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">_MASKS</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;0x[0-9a-fA-F]+&#34;</span><span class="p">),</span> <span class="s2">&#34;&lt;addr&gt;&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\b[0-9a-fA-F]</span><span class="si">{8}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{4}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{4}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{4}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{12}</span><span class="s2">\b&#34;</span><span class="p">),</span> <span class="s2">&#34;&lt;uuid&gt;&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\b[0-9a-fA-F]{16,}\b&#34;</span><span class="p">),</span> <span class="s2">&#34;&lt;hex&gt;&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\d+&#34;</span><span class="p">),</span> <span class="s2">&#34;&lt;n&gt;&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span></span></span></code></pre></div></div>
<p><code>Cannot read property 'title' of undefined (reading index 3)</code> và <code>(reading index 17)</code> sẽ thành cùng một issue. Nếu SDK tự gửi kèm <code>fingerprint</code> mà không có <code>{{ default }}</code> bên trong thì giá trị đó được ưu tiên, nhờ vậy tôi có thể ép cách gom nhóm từ phía client khi cách mặc định chưa đúng. Sự kiện nào không có exception thì dùng thông điệp log đã che làm dấu vân tay, còn nếu không có cả hai thì dùng frame đầu tiên thuộc mã của ứng dụng (in-app). Với một lần sập do tín hiệu (signal) ở tầng gốc, thông điệp thường kiểu <code>Signal 6, Code 0</code>, nên các lần sập này được gom theo tín hiệu. Cách đó hơi thô, nhưng trung thực cho tới khi có giải mã ký hiệu (symbolication).</p>

<h2 class="relative group">Cảnh báo mà không làm ngập kênh
    <div id="cảnh-báo-mà-không-làm-ngập-kênh" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%e1%ba%a3nh-b%c3%a1o-m%c3%a0-kh%c3%b4ng-l%c3%a0m-ng%e1%ba%adp-k%c3%aanh" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Mỗi dấu vân tay ứng với một issue, và mỗi báo cáo được lưu thành một sự kiện nằm dưới issue đó. Issue chỉ ghi log ở mức ERROR (đó là thứ cảnh báo trên kênh chat lắng nghe) đúng trong những trường hợp sau:</p>
<ul>
<li>một issue <strong>mới</strong></li>
<li>một issue <strong>đã đánh dấu xử lý xong (resolved) nhưng quay lại</strong>, khi đó issue được mở lại</li>
<li>lần xuất hiện thứ <strong>10, 100, 1.000 và 10.000</strong></li>
</ul>
<p>Một lần lặp lại bình thường chỉ tăng bộ đếm. Issue bị đánh dấu <em>ignored</em> vẫn tiếp tục được đếm nhưng không bao giờ cảnh báo. Cộng thêm giới hạn tần suất riêng của handler gửi lên kênh chat, một vòng lặp sập trên một bản phát hành lỗi chỉ hiện ra thành vài tin nhắn chứ không thành một bức tường chữ.</p>
<p>Dung lượng lưu trữ cũng được giới hạn theo cùng cách: mỗi issue chỉ giữ <strong>50 sự kiện gần nhất</strong> và xoá các sự kiện cũ hơn khi có sự kiện mới. Điều này làm hỏng biểu đồ đầu tiên của tôi, vì 50 sự kiện không thể cho bạn biết tuần trước lỗi đó xảy ra bao nhiêu lần. Vì vậy mỗi issue giữ thêm số đếm theo từng ngày trong 90 ngày, được cập nhật trong cùng giao dịch (transaction) với bộ đếm. Biểu đồ vẽ từ các số đếm này; các sự kiện chỉ để đọc chi tiết.</p>
<p>Nếu bạn tự làm một hệ thống như vậy, có hai tình huống tranh chấp (race) nhỏ đáng xử lý. Khi một lần sập hoàn toàn mới xảy ra trên nhiều điện thoại cùng lúc, hai báo cáo có thể cùng cố tạo issue, nên hãy đặt ràng buộc duy nhất cho dấu vân tay và để báo cáo chậm chân hơn đọc lại dòng đã có. Ngoài ra, khung 90 ngày nên tính từ ngày hôm nay theo đồng hồ của máy chủ, chứ không tính từ báo cáo mới nhất, vì một chiếc điện thoại cài sai đồng hồ có thể ghi ngày cho báo cáo là bất kỳ ngày nào, kể cả năm sau.</p>
<p>Payload bị giới hạn ở 128KB. Sự kiện nào vượt quá thì bị bỏ breadcrumb và siêu dữ liệu gỡ lỗi (debug metadata) trước; nếu vẫn còn quá lớn thì chỉ giữ exception, thông điệp, phiên bản phát hành và nền tảng, kèm một dấu đánh dấu để tôi biết nó đã bị cắt.</p>

<h2 class="relative group">Lọc bỏ dữ liệu cá nhân
    <div id="lọc-bỏ-dữ-liệu-cá-nhân" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#l%e1%bb%8dc-b%e1%bb%8f-d%e1%bb%af-li%e1%bb%87u-c%c3%a1-nh%c3%a2n" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>SDK chạy với <code>sendDefaultPii: false</code>, nên về nguyên tắc không có địa chỉ IP, cookie hay nội dung yêu cầu nào được gửi lên. Máy chủ không dựa vào điều đó: trước khi lưu bất cứ thứ gì, nó chỉ giữ <code>id</code> và <code>username</code> trong <code>user</code> và bỏ hẳn <code>request</code>, phòng khi một phiên bản SDK hoặc một thiết lập sau này bắt đầu gửi chúng.</p>
<p>Kiểm thử trên thiết bị thật làm lộ ra thêm hai thứ cần loại bỏ:</p>
<ul>
<li><strong>Breadcrumb ghi lại URL đầy đủ, kể cả query string.</strong> Các yêu cầu tìm kiếm của ứng dụng đặt từ khoá ngay trong query string, nên một lần sập ngay sau khi tìm kiếm sẽ lưu luôn từ khoá người dùng đã gõ. Giờ máy chủ cắt mọi URL trong breadcrumb tại dấu <code>?</code>.</li>
<li><strong>Breadcrumb từ console mang theo nguyên các đối số</strong> đã truyền vào <code>console.log</code>, mà đó có thể là bất kỳ object nào ứng dụng tình cờ ghi log. Phần văn bản thông điệp được giữ (vì nó hữu ích), còn các đối số thì bị bỏ.</li>
</ul>
<p>Tôi chỉ phát hiện ra hai điều đó khi nhìn vào một báo cáo thật, và đó cũng là lý do chính cho phần tiếp theo.</p>

<h2 class="relative group">Phía client
    <div id="phía-client" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ph%c3%ada-client" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Trong ứng dụng, mọi thứ đều đi qua một module nhỏ duy nhất, cùng lý do với việc tôi bọc SDK cập nhật qua mạng (OTA) lại: một bundle OTA cũng chạy trên các bản build được tạo ra trước khi module gốc tồn tại. Một câu <code>import * as Sentry from &quot;@sentry/react-native&quot;</code> ở cấp cao nhất trên bản build không có <code>RNSentry</code> sẽ ném lỗi ngay khi import, và kết quả là màn hình trắng. Vì vậy SDK được nạp lười (lazy require), và chỉ sau khi đã kiểm tra module gốc thực sự có mặt:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">NativeModules</span><span class="p">,</span> <span class="nx">Platform</span><span class="p">,</span> <span class="nx">TurboModuleRegistry</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;react-native&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">Constants</span> <span class="kr">from</span> <span class="s2">&#34;expo-constants&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">DSN</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">EXPO_PUBLIC_CRASH_DSN</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">nativeModulePresent</span><span class="p">()</span><span class="o">:</span> <span class="kr">boolean</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">Boolean</span><span class="p">(</span><span class="nx">TurboModuleRegistry</span><span class="p">.</span><span class="kr">get</span><span class="p">(</span><span class="s2">&#34;RNSentry&#34;</span><span class="p">)</span> <span class="o">??</span> <span class="nx">NativeModules</span><span class="p">.</span><span class="nx">RNSentry</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kd">function</span> <span class="nx">initCrashReporting</span><span class="p">()</span><span class="o">:</span> <span class="kr">boolean</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">DSN</span> <span class="o">||</span> <span class="nx">Platform</span><span class="p">.</span><span class="nx">OS</span> <span class="o">===</span> <span class="s2">&#34;web&#34;</span> <span class="o">||</span> <span class="o">!</span><span class="nx">nativeModulePresent</span><span class="p">())</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">Sentry</span> <span class="o">=</span> <span class="kr">require</span><span class="p">(</span><span class="s2">&#34;@sentry/react-native&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">Sentry</span><span class="p">.</span><span class="nx">init</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">dsn</span>: <span class="kt">DSN</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">release</span>: <span class="kt">Constants.expoConfig?.version</span> <span class="o">??</span> <span class="s2">&#34;unknown&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">sendDefaultPii</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">tracesSampleRate</span>: <span class="kt">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">enableAutoSessionTracking</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// ...turn off the other tracing options the same way
</span></span></span><span class="line"><span class="cl">    <span class="nx">maxBreadcrumbs</span>: <span class="kt">50</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">beforeSend</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* tag the OTA bundle */</span> <span class="k">return</span> <span class="nx">event</span><span class="p">;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>Chỉ báo lỗi. Máy chủ không lưu trace, session, profile hay bản ghi phát lại (replay), nên không tính năng nào trong số đó được bật. Không có DSN (khi phát triển, khi chạy kiểm thử E2E) thì SDK không bao giờ khởi động. Các cấu hình build (build profile) đặt sẵn DSN và một môi trường (<code>staging</code> hoặc <code>production</code>), nên cả hai loại bản build đều báo về cùng một endpoint mà vẫn phân biệt được với nhau. Người dùng đã đăng nhập được gắn bằng ID tài khoản, không bao giờ bằng tên hay email.</p>
<p>Các error boundary cần được gọi báo lỗi một cách tường minh. <code>ErrorBoundary</code> gốc của Expo Router bắt lỗi khi render và hiện màn hình của riêng nó, nên lỗi đó không bao giờ tới được handler toàn cục mà SDK móc vào. Cách sửa là một lớp bọc nhỏ: gọi <code>captureException</code> trong một <code>useEffect</code> rồi render boundary của router như cũ, dùng chung cho cả boundary gốc lẫn boundary của từng route. Lời gọi được đặt trong effect chứ không nằm trong hàm render, để nó chỉ báo một lần thay vì báo lại sau mỗi lần render lại. Component gốc cũng được bọc bằng <code>Sentry.wrap</code>, và nếu việc bọc ném lỗi thì quay về dùng component trần.</p>
<p>Trên web có một phiên bản <code>.web.ts</code> của module, trong đó mọi hàm đều bị tắt. Metro chọn tệp theo nền tảng, nên SDK hoàn toàn không lọt vào bundle web. Ứng dụng web vốn không có module gốc nào để SDK chạy trên đó.</p>

<h2 class="relative group">Chứng minh một lần sập gốc thật sự về tới nơi
    <div id="chứng-minh-một-lần-sập-gốc-thật-sự-về-tới-nơi" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ch%e1%bb%a9ng-minh-m%e1%bb%99t-l%e1%ba%a7n-s%e1%ba%adp-g%e1%bb%91c-th%e1%ba%adt-s%e1%bb%b1-v%e1%bb%81-t%e1%bb%9bi-n%c6%a1i" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Các unit test gửi những envelope viết tay, tức là chỉ chứng minh parser xử lý được thứ mà tôi <em>nghĩ</em> SDK sẽ gửi. Hai ngày sau khi phát hành, trên production chưa có báo cáo sự cố nào. Lúc đó mới có ba người cập nhật, nên con số 0 là hợp lý, nhưng nhìn từ phía máy chủ thì &ldquo;chưa ai bị sập&rdquo; và &ldquo;báo cáo sập không về tới&rdquo; trông giống hệt nhau. Tôi muốn thấy một báo cáo thật.</p>
<p>Vì vậy tôi build một bản Release cho simulator, với DSN trỏ về một Django chạy cục bộ ở cổng 8000 cùng một khoá dùng một lần:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">EXPO_PUBLIC_CRASH_DSN</span><span class="o">=</span>http://localcrashtestkey0000@localhost:8000/1 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nv">EXPO_PUBLIC_CRASH_ENVIRONMENT</span><span class="o">=</span>local-verify <span class="se">\
</span></span></span><span class="line"><span class="cl">xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Release <span class="se">\
</span></span></span><span class="line"><span class="cl">  -destination <span class="s1">&#39;platform=iOS Simulator,id=&lt;udid&gt;&#39;</span> -derivedDataPath /tmp/iosbuild</span></span></code></pre></div></div>
<p>(Dùng Release chứ không phải Debug: bản Debug có liên kết dev launcher và hoạt động khác đi, còn Release mới là thứ người dùng thật sự chạy.) Sau đó tôi cài, mở ứng dụng, chờ vài giây, rồi giết tiến trình theo đúng cách một lỗi thật sẽ làm:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">xcrun simctl install booted /tmp/iosbuild/Build/Products/Release-iphonesimulator/MyApp.app
</span></span><span class="line"><span class="cl">xcrun simctl launch booted com.example.myapp      <span class="c1"># prints the pid</span>
</span></span><span class="line"><span class="cl"><span class="nb">kill</span> -ABRT &lt;pid&gt;</span></span></code></pre></div></div>
<p>Ứng dụng trong simulator là các tiến trình macOS bình thường, nên tín hiệu tới được handler của SentryCrash y như trên thiết bị thật. Handler ghi báo cáo xuống đĩa; trong lúc tiến trình đang chết thì không có gì được gửi đi. Ở <strong>lần khởi động tiếp theo</strong>, SDK tìm thấy báo cáo đó và gửi lên. Tôi mở lại ứng dụng, và một giây sau nhật ký mạng của simulator cho thấy có kết nối tới <code>:8000</code>.</p>
<p>Rồi trong log của Django chẳng có gì, khiến tôi bối rối mất một phút: ở mức log tôi đang đặt, máy chủ phát triển chỉ ghi lại các phản hồi không phải 2xx. Nhưng trong cơ sở dữ liệu thì có:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">SIGABRT: Signal 6, Code 0 | cocoa 1.4.0 iOS 27.1 iPhone19,4 handled=False
</span></span><span class="line"><span class="cl">received 09:19:53, crashed 09:19:38
</span></span><span class="line"><span class="cl">contexts: app, device, os, ota_updates, trace · 24 breadcrumbs</span></span></code></pre></div></div>
<p>Mười lăm giây từ lúc sập tới lúc được lưu, mà phần lớn thời gian đó là tôi mở lại ứng dụng. Và chính việc nhìn vào payload thật đã dạy tôi những điều mà kiểm thử không thể:</p>
<ul>
<li><strong>ID người dùng không phải ID tài khoản.</strong> Bản build thử nghiệm không thật sự đăng nhập, nên sentry-cocoa dùng ID riêng cho từng lượt cài đặt của nó. Điều này đáng biết, và đáng ghi vào phần lưu ý quyền riêng tư: một báo cáo mang ID tài khoản nếu bạn đã đăng nhập, và một ID cài đặt ngẫu nhiên nếu chưa.</li>
<li><strong>Không có múi giờ.</strong> Tôi đã cho rằng sẽ có context về ngôn ngữ và vùng (culture). Context thiết bị có <code>locale</code>, bộ nhớ, kích thước màn hình, chế độ tiết kiệm pin và phiên bản kernel, nhưng không có múi giờ.</li>
<li><strong>Sập gốc bỏ qua <code>beforeSend</code>.</strong> Hàm <code>beforeSend</code> phía JS của tôi gắn thêm tag <code>ota</code> ghi bundle OTA đang chạy, nhưng một lần sập gốc được SDK gốc lắp ráp ở lần khởi động sau và không bao giờ đi qua hook JS này. Muốn gắn tag cho sập gốc thì phải đặt tag trên scope bằng <code>setTag</code> ngay khi biết bundle, để nó được đồng bộ xuống tầng gốc.</li>
<li><strong>Breadcrumb cho thấy ứng dụng đang làm gì.</strong> Cảnh báo trên console, vòng đời của view controller, và các lệnh gọi HTTP, kể cả các yêu cầu của chính SDK phân tích (analytics) tới endpoint thu thập dữ liệu của nó. Đó là lời nhắc tốt về việc ứng dụng còn nói chuyện với những ai khác. Và các URL có kèm query string, đó là cách tôi phát hiện ra vấn đề từ khoá tìm kiếm ở trên.</li>
</ul>

<h2 class="relative group">Đọc báo cáo
    <div id="đọc-báo-cáo" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%c4%91%e1%bb%8dc-b%c3%a1o-c%c3%a1o" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Báo cáo được đọc trong trang quản trị Django, trên một trang issue mô phỏng đại khái theo Sentry: phần đầu có các con số đếm và các nút Resolve / Ignore / Reopen, tiếp theo là biểu đồ cột theo ngày, tỷ lệ theo phiên bản phát hành, hệ điều hành, thiết bị và bundle OTA, rồi tới từng sự kiện một với exception, các frame (frame của ứng dụng được tô nổi, frame của thư viện được thu gọn), breadcrumb và context thiết bị. Không cần thêm thư viện phụ thuộc nào.</p>
<p>Trong lúc làm trang này có một chỗ phải sửa: với một lần sập gốc, frame &ldquo;thủ phạm&rdquo; (culprit) là đường dẫn của tệp thực thi trên thiết bị, trong đó có một UUID riêng cho mỗi lượt cài đặt, nên cùng một lần sập lại trông khác nhau trên mỗi điện thoại. Giờ nó chỉ hiện địa chỉ và tên tệp thực thi, kiểu <code>0x1000a1b2c (MyApp)</code>.</p>

<h2 class="relative group">Chính sách quyền riêng tư, toàn bộ
    <div id="chính-sách-quyền-riêng-tư-toàn-bộ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ch%c3%adnh-s%c3%a1ch-quy%e1%bb%81n-ri%c3%aang-t%c6%b0-to%c3%a0n-b%e1%bb%99" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Báo cáo sự cố gần như chắc chắn là thông tin cá nhân: một ID tài khoản, một mẫu điện thoại, và những gì một người đã làm trong phút cuối trước khi ứng dụng sập. Vì vậy chính sách quyền riêng tư cần thêm một đoạn. Viết cho đàng hoàng nghĩa là phải nói chính xác một báo cáo mang theo những gì, nên tôi liệt kê dựa trên payload vừa xem: lỗi, phiên bản ứng dụng và bản cập nhật OTA, mẫu điện thoại, hệ điều hành và ngôn ngữ, bộ nhớ trống và kích thước màn hình, vài chục bước gần nhất (các màn hình, địa chỉ web đã bỏ mọi thứ sau dấu <code>?</code>, các dòng log của chính ứng dụng), và ID tài khoản hoặc một ID cài đặt ngẫu nhiên. Chính sách cũng nói rõ báo cáo được gửi về máy chủ của chúng tôi, không gửi tới Sentry hay bất kỳ công ty báo cáo sự cố nào khác. Thời hạn lưu giữ có một dòng riêng trong bảng: 50 báo cáo gần nhất cho mỗi vấn đề, cộng với số đếm theo ngày trong 90 ngày.</p>
<p>Một khi đã đối chiếu một đoạn với mã nguồn, thật khó mà không đối chiếu phần còn lại. Và chính sách hiện có không đứng vững. Trang xoá tài khoản ghi rằng token thông báo đẩy (push notification) và passkey bị xoá ngay khi bạn đóng tài khoản, và log máy chủ sẽ biến mất trong vòng 90 ngày. Không điều nào trong số đó đúng. Đóng tài khoản sẽ gỡ liên kết các phương thức đăng nhập và ẩn danh hoá hồ sơ, nhưng token thông báo đẩy và passkey vẫn còn đó cho tới khi có người xoá bằng tay, và kho lưu log hiện chưa có hạn xoá. Vì vậy toàn bộ chính sách được viết lại dựa trên bản kiểm kê những gì mã nguồn thực sự làm, và những dòng đó giờ ghi &ldquo;chưa được xoá tự động; hãy email cho chúng tôi&rdquo;. Nghe kém yên tâm hơn, nhưng đúng sự thật.</p>
<p>Giờ repo có hẳn một quy tắc cho việc này: chính sách mô tả những gì mã nguồn làm, chứ không phải những gì chúng tôi dự định, và mọi thay đổi khiến dữ liệu cá nhân được thu thập, gửi đi hay lưu giữ ở một chỗ mới đều phải cập nhật chính sách trong cùng commit. Việc cập nhật đó bao gồm cả màn hình tóm tắt trong ứng dụng, trang xoá tài khoản, và các biểu mẫu quyền riêng tư trên cửa hàng ứng dụng, vốn chỉ nằm trong bảng điều khiển của cửa hàng. Báo cáo sự cố chính là thay đổi khiến tôi phải viết quy tắc đó ra.</p>

<h2 class="relative group">Còn thiếu gì
    <div id="còn-thiếu-gì" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%b2n-thi%e1%ba%bfu-g%c3%ac" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p><strong>Giải mã ký hiệu (symbolication).</strong> Đây là khoảng trống thật sự, và là thứ duy nhất sentry.io làm mà tôi chưa thay thế được. Stack của JavaScript thì đã bị rút gọn, còn của mã gốc thì chưa được giải mã ký hiệu:</p>
<ul>
<li><strong>JavaScript:</strong> các frame trỏ vào bundle Hermes. Muốn sửa thì phải giữ source map Hermes đã ghép (composed) cho mọi bản build lên cửa hàng và mọi bundle OTA, đánh khoá theo phiên bản phát hành và bundle, rồi áp vào các frame JS khi render trang. Đó là phần làm tiếp theo, vì phần lớn các lần sập của chúng tôi sẽ là JavaScript.</li>
<li><strong>iOS gốc:</strong> địa chỉ lệnh cần dSYM của bản build.</li>
<li><strong>Android:</strong> frame Java bị R8 làm rối (obfuscate) khi bật rút gọn mã (cần <code>mapping.txt</code>), còn frame C++ cần ký hiệu NDK.</li>
</ul>
<p>Còn một khoảng trống nhỏ hơn mà tôi đã nhắc ở trên: các lần sập gốc chưa mang tag OTA, vì tag này mới chỉ được đặt trong <code>beforeSend</code>. Đặt nó trên scope ngay khi bundle đang chạy được xác nhận chỉ mất vài dòng, nhưng tôi chưa viết.</p>
<p>Hiện tại, một issue cho tôi biết <em>cái gì</em> đã sập, trên phiên bản phát hành, bundle OTA và thiết bị nào, và người dùng đang làm gì ngay trước đó. Nó chưa cho tôi biết lỗi nằm <em>ở đâu</em> trong mã nguồn. Với một ứng dụng nhỏ phát hành vài lần mỗi tuần, cho tới giờ kiểu lỗi, thông điệp và breadcrumb vẫn đủ để tìm ra vấn đề.</p>
<p>Bộ nhận, trang quản trị và phần kiểm thử cộng lại cũng không nhiều mã, và nó không tốn thêm đồng nào ngoài chi phí của API vốn đã chạy sẵn. SDK làm phần khó nhất: bắt được lần sập trên điện thoại và gửi báo cáo đi ở lần khởi động kế tiếp.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-10-09-sentry-sdk-own-server/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>Cách debug ứng dụng iOS trong Xcode: breakpoint, LLDB, Instruments và Simulator</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2026/2026-02-26-debugging-ios-mobile-apps/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2026/2026-02-26-debugging-ios-mobile-apps/</guid>
      <pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>iOS</category>
      <category>Xcode</category>
      <category>Debugging</category>
      <category>Swift</category>
      <category>iOS Simulator</category>
      <category>Mobile Development</category>
      <description>Cách debug ứng dụng iOS trong Xcode: điểm dừng (breakpoint) có điều kiện và theo ký hiệu, những lệnh LLDB đáng nhớ, View Debugger và Memory Graph, Instruments, các Sanitizer, nhật ký sự cố (crash log), và những lỗi trình giả lập bỏ sót mà chỉ iPhone thật mới bắt được.</description>
      <content:encoded><![CDATA[<p>Để gỡ lỗi (debug) một ứng dụng iOS trong Xcode, trước hết hãy thêm điểm dừng ngoại lệ (exception breakpoint) để mỗi khi ứng dụng sập, Xcode dừng ngay ở dòng mã đã ném ra ngoại lệ. Sau đó tái hiện lỗi, rồi kiểm tra trạng thái trong console LLDB bằng lệnh <code>po</code>. Tiếp theo, chọn công cụ theo triệu chứng: View Debugger cho lỗi bố cục giao diện, Memory Graph và Instruments cho rò rỉ bộ nhớ và chạy chậm, còn Address Sanitizer và Thread Sanitizer cho những vụ sập lúc có lúc không. Cuối cùng, hãy chạy thử trên iPhone thật, vì trình giả lập che mất cả một nhóm lỗi chỉ lộ ra trên máy thật.</p>
<p>Thời gian tôi dành cho việc gỡ lỗi nhiều đến mức khó tin. Mà lập trình viên nào chẳng thế. Dạo này phần lớn thời gian đó tôi ngồi trong Xcode và trình giả lập (Simulator), lần theo lỗi trong chính ứng dụng của mình, và có một điều tôi cứ thấy lạ mãi: Xcode đi kèm cả một kho công cụ gỡ lỗi, vậy mà gần như chẳng ai đụng tới. Dưới đây là quy trình tôi đúc kết qua nhiều năm, chỉ gồm những phần thực sự giúp tiết kiệm thời gian mỗi khi có gì đó hỏng.</p>

<h2 class="relative group">Điểm dừng làm được nhiều hơn bạn nghĩ
    <div id="điểm-dừng-làm-được-nhiều-hơn-bạn-nghĩ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%c4%91i%e1%bb%83m-d%e1%bb%abng-l%c3%a0m-%c4%91%c6%b0%e1%bb%a3c-nhi%e1%bb%81u-h%c6%a1n-b%e1%ba%a1n-ngh%c4%a9" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Đa số chúng ta chỉ nhấp vào lề trái để đặt điểm dừng (breakpoint) rồi thôi. Nhưng thử nhấp chuột phải vào một điểm dừng xem, bạn sẽ thấy cả một menu tùy chọn, và nó thay đổi hẳn cách bạn gỡ lỗi.</p>

<h3 class="relative group">Điểm dừng có điều kiện (Conditional Breakpoint)
    <div id="điểm-dừng-có-điều-kiện-conditional-breakpoint" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%c4%91i%e1%bb%83m-d%e1%bb%abng-c%c3%b3-%c4%91i%e1%bb%81u-ki%e1%bb%87n-conditional-breakpoint" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Giả sử bạn có một vòng lặp xử lý 1000 phần tử, và chỉ riêng phần tử thứ 347 là trục trặc. Nếu phải dừng ở điểm dừng 346 lần trước khi tới được nó thì coi như mất toi một buổi chiều.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mf">0.</span><span class="p">.&lt;</span><span class="n">users</span><span class="p">.</span><span class="bp">count</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">processUser</span><span class="p">(</span><span class="n">users</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>  <span class="c1">// Set breakpoint here with condition: i == 347</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>Nhấp chuột phải vào điểm dừng, thêm điều kiện <code>i == 347</code>, thế là Xcode chỉ dừng khi điều kiện đó đúng. Mấy điều kiện kiểu <code>userId == &quot;abc123&quot;</code> hay <code>error != nil</code> cũng dùng tốt y như vậy.</p>

<h3 class="relative group">Điểm dừng theo ký hiệu (Symbolic Breakpoint)
    <div id="điểm-dừng-theo-ký-hiệu-symbolic-breakpoint" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%c4%91i%e1%bb%83m-d%e1%bb%abng-theo-k%c3%bd-hi%e1%bb%87u-symbolic-breakpoint" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Loại này dùng khi bạn không biết một phương thức đang được gọi từ đâu. Mở Breakpoint Navigator (Cmd+8), bấm nút +, rồi chọn &ldquo;Symbolic Breakpoint&rdquo;.</p>
<p>Muốn biết mỗi lần có view controller nào được nạp? Đặt ký hiệu là <code>viewDidLoad</code>, Xcode sẽ dừng lại ở từng cái một. Thấy nhiều quá? Thì thu hẹp lại: <code>MyViewController.viewDidLoad</code>.</p>
<p>Có mấy cái tôi để cố định trong mọi dự án:</p>
<ul>
<li><code>UIViewAlertForUnsatisfiableConstraints</code> bắt các xung đột Auto Layout ngay khi chúng xảy ra</li>
<li><code>objc_exception_throw</code> dừng ở mọi ngoại lệ (exception) Objective-C, trước khi ứng dụng kịp bị sập (crash)</li>
<li><code>malloc_error_break</code> giúp tìm ra các lỗi cấp phát bộ nhớ</li>
</ul>

<h3 class="relative group">Điểm dừng ngoại lệ (Exception Breakpoint)
    <div id="điểm-dừng-ngoại-lệ-exception-breakpoint" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%c4%91i%e1%bb%83m-d%e1%bb%abng-ngo%e1%ba%a1i-l%e1%bb%87-exception-breakpoint" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Đây nên là thứ đầu tiên bạn thiết lập khi tạo dự án mới. Thêm một cái (Breakpoint Navigator, + → Exception Breakpoint) là Xcode sẽ dừng ngay lúc có ngoại lệ được ném ra, chứ không phải đợi đến khi ứng dụng đã chết hẳn.</p>
<p>Nhờ nó mà tôi bắt được không biết bao nhiêu lỗi mở gói (unwrap) giá trị nil, nhiều hơn mức tôi muốn thừa nhận. Không có nó thì mấy lỗi này chỉ hiện ra dưới dạng những vụ sập khó hiểu trong console.</p>

<h3 class="relative group">Hành động của điểm dừng (Breakpoint Action)
    <div id="hành-động-của-điểm-dừng-breakpoint-action" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#h%c3%a0nh-%c4%91%e1%bb%99ng-c%e1%bb%a7a-%c4%91i%e1%bb%83m-d%e1%bb%abng-breakpoint-action" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Điểm dừng không nhất thiết phải dừng chương trình. Sửa một điểm dừng, bấm &ldquo;Add Action&rdquo;, và bạn có thể cho nó:</p>
<ul>
<li>In giá trị biến ra mà không dừng: <code>&quot;User count: @(users.count)@&quot;</code></li>
<li>Tự động chạy lệnh LLDB</li>
<li>Chạy shell script</li>
<li>Phát âm thanh (tôi hay dùng cho những nhánh mã hiếm khi chạy tới, để biết ngay khi nó xảy ra)</li>
</ul>
<p>Đánh dấu thêm &ldquo;Automatically continue after evaluating actions&rdquo; là điểm dừng biến thành một công cụ ghi nhật ký (log), không bao giờ làm gián đoạn bạn.</p>

<h2 class="relative group">LLDB: những lệnh console bạn sẽ dùng thật
    <div id="lldb-những-lệnh-console-bạn-sẽ-dùng-thật" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#lldb-nh%e1%bb%afng-l%e1%bb%87nh-console-b%e1%ba%a1n-s%e1%ba%bd-d%c3%b9ng-th%e1%ba%adt" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Khi chương trình dừng ở điểm dừng, cái console ở dưới cùng không chỉ để đọc nhật ký sự cố (crash log). Đó chính là LLDB, và nó hữu ích hơn bạn tưởng nhiều.</p>

<h3 class="relative group">Cơ bản
    <div id="cơ-bản" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c6%a1-b%e1%ba%a3n" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) po user
▿ User
  - id: &#34;123&#34;
  - name: &#34;John Doe&#34;
  - email: &#34;john@example.com&#34;</code></pre></div>
<p><code>po</code> in đối tượng (object) ra ở dạng dễ đọc. Riêng lệnh này đã chiếm tới 90% số lần tôi dùng LLDB.</p>
<p>Cần chi tiết hơn thì dùng <code>p</code>:</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) p user.name
(String) $R0 = &#34;John Doe&#34;</code></pre></div>
<p>Còn muốn xem toàn bộ biến cục bộ cùng lúc:</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) frame variable</code></pre></div>

<h3 class="relative group">Thay đổi giá trị khi chương trình đang chạy
    <div id="thay-đổi-giá-trị-khi-chương-trình-đang-chạy" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#thay-%c4%91%e1%bb%95i-gi%c3%a1-tr%e1%bb%8b-khi-ch%c6%b0%c6%a1ng-tr%c3%acnh-%c4%91ang-ch%e1%ba%a1y" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>LLDB thực sự đáng đồng tiền ở chỗ này: bạn sửa được giá trị biến mà không cần biên dịch lại.</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) expr user.name = &#34;Jane Doe&#34;
(lldb) expr index = 0</code></pre></div>
<p>Tìm ra lỗi rồi và muốn thử cách sửa mà không phải biên dịch lại? Cứ đổi giá trị biến rồi cho chương trình chạy tiếp. Tôi dùng cách này liên tục mỗi khi cần khoanh vùng các trường hợp biên (edge case).</p>
<p>Gọi phương thức cũng được luôn:</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) po self.refreshUI()
(lldb) expr navigationController?.popViewController(animated: true)</code></pre></div>

<h3 class="relative group">Di chuyển trong chương trình
    <div id="di-chuyển-trong-chương-trình" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#di-chuy%e1%bb%83n-trong-ch%c6%b0%c6%a1ng-tr%c3%acnh" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) bt              # Show the call stack
(lldb) frame select 3  # Jump to a different stack frame
(lldb) continue        # Keep running (or just &#39;c&#39;)
(lldb) n               # Step over (next line)
(lldb) s               # Step into function</code></pre></div>

<h3 class="relative group">Điểm theo dõi (Watchpoint)
    <div id="điểm-theo-dõi-watchpoint" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%c4%91i%e1%bb%83m-theo-d%c3%b5i-watchpoint" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Muốn biết khi nào một biến bị thay đổi? Đặt điểm theo dõi:</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) watchpoint set variable user.isLoggedIn</code></pre></div>
<p>Từ giờ, cứ mỗi lần giá trị đó thay đổi là chương trình dừng lại, bất kể dòng mã nào gây ra. Khi phải truy lùng những lần trạng thái bị thay đổi ngoài ý muốn thì công cụ này quý như vàng.</p>

<h2 class="relative group">Console.app: công cụ gỡ lỗi ít người để ý
    <div id="consoleapp-công-cụ-gỡ-lỗi-ít-người-để-ý" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#consoleapp-c%c3%b4ng-c%e1%bb%a5-g%e1%bb%a1-l%e1%bb%97i-%c3%adt-ng%c6%b0%e1%bb%9di-%c4%91%e1%bb%83-%c3%bd" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Console của Xcode dùng tạm thì ổn, cho đến khi bạn cần xem mọi thứ: nhật ký hệ thống, báo cáo sự cố (crash report), đủ cả. Những lúc đó tôi mở Console.app.</p>
<p>Nếu bạn đang dùng OSLog (và bạn nên dùng), Console.app cho phép tìm kiếm và lọc các nhật ký đó:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">os</span><span class="p">.</span><span class="nc">log</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">logger</span> <span class="p">=</span> <span class="n">Logger</span><span class="p">(</span><span class="n">subsystem</span><span class="p">:</span> <span class="s">&#34;com.example.app&#34;</span><span class="p">,</span> <span class="n">category</span><span class="p">:</span> <span class="s">&#34;networking&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">&#34;Starting API request&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">debug</span><span class="p">(</span><span class="s">&#34;Request URL: </span><span class="si">\(</span><span class="n">url</span><span class="p">.</span><span class="n">absoluteString</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">error</span><span class="p">(</span><span class="s">&#34;Failed to decode: </span><span class="si">\(</span><span class="n">error</span><span class="p">.</span><span class="n">localizedDescription</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Sau đó, trong Console.app:</p>
<ol>
<li>Chọn trình giả lập hoặc thiết bị đang kết nối</li>
<li>Lọc theo phân hệ (subsystem) của bạn: <code>subsystem:com.example.app</code></li>
<li>Lọc theo mức độ: <code>subsystem:com.example.app AND level:error</code></li>
</ol>
<p>Với những kiểu gỡ lỗi hay làm, bạn có thể lưu sẵn biểu thức lọc (predicate). Tôi có một cái cho yêu cầu mạng, một cái cho các thao tác với cơ sở dữ liệu, và một cái chỉ hiện lỗi với cảnh báo.</p>

<h3 class="relative group">Sao lại dùng OSLog thay vì print()?
    <div id="sao-lại-dùng-oslog-thay-vì-print" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#sao-l%e1%ba%a1i-d%c3%b9ng-oslog-thay-v%c3%ac-print" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>So với việc rải <code>print()</code> khắp nơi thì OSLog hơn hẳn. Nhật ký có cấu trúc nên lọc được theo mức độ và danh mục. Nó đủ nhanh để việc ghi nhật ký không làm ứng dụng chậm đi. Trên môi trường production, dữ liệu nhạy cảm được tự động ẩn đi, và nhật ký vẫn còn nguyên kể cả khi ứng dụng bị sập.</p>
<p><code>print()</code> vẫn ổn cho mấy lần kiểm tra nhanh rồi xóa. Còn thứ gì sau này bạn có thể cần xem lại thì nên ghi bằng OSLog.</p>

<h3 class="relative group">Lọc nâng cao trong Console.app
    <div id="lọc-nâng-cao-trong-consoleapp" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#l%e1%bb%8dc-n%c3%a2ng-cao-trong-consoleapp" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Console.app hỗ trợ đầy đủ truy vấn bằng biểu thức lọc. Đây là mấy câu tôi dùng thường xuyên:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># All errors and faults in your app
</span></span><span class="line"><span class="cl">subsystem:com.example.app AND (level:error OR level:fault)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Network requests that failed
</span></span><span class="line"><span class="cl">subsystem:com.example.app AND category:networking AND eventMessage CONTAINS &#34;failed&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Everything from the last 5 minutes
</span></span><span class="line"><span class="cl">subsystem:com.example.app AND timestamp &gt;= now(-5m)</span></span></code></pre></div></div>
<p>Bạn cũng có thể xuất nhật ký ra để đính kèm vào báo cáo lỗi, tiện hơn nhiều so với sao chép thủ công từ console của Xcode.</p>

<h2 class="relative group">Gỡ lỗi giao diện với View Debugger
    <div id="gỡ-lỗi-giao-diện-với-view-debugger" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#g%e1%bb%a1-l%e1%bb%97i-giao-di%e1%bb%87n-v%e1%bb%9bi-view-debugger" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Khi giao diện bị vỡ mà bạn không hiểu tại sao, View Debugger là cách nhanh nhất để tìm ra nguyên nhân.</p>
<p>Chạy ứng dụng, mở tới màn hình bị lỗi, rồi bấm nút &ldquo;Debug View Hierarchy&rdquo; trên thanh gỡ lỗi của Xcode (hoặc nhấn Cmd+Shift+D). Xcode sẽ hiện toàn bộ cây phân cấp view (view hierarchy) dưới dạng 3D, tách ra từng lớp.</p>
<p>Xoay qua xoay lại một chút là vấn đề lộ ra ngay:</p>
<ul>
<li>View bị vẽ nằm phía sau view khác</li>
<li>View nằm tít ngoài màn hình</li>
<li>View có kích thước bằng 0</li>
<li>Toàn bộ chuỗi ràng buộc (constraint) của view bạn đang chọn</li>
</ul>
<p>Tôi từng nhờ nó mà tìm ra những cái nút vô hình nuốt mất sự kiện chạm, những nhãn bị vẽ ở kích thước 0x0, và có lần là một tấm ảnh lỡ tay rộng tới 10,000 pixel.</p>

<h3 class="relative group">Gỡ lỗi Auto Layout
    <div id="gỡ-lỗi-auto-layout" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#g%e1%bb%a1-l%e1%bb%97i-auto-layout" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Trong cây phân cấp view, biểu tượng cảnh báo màu tím nghĩa là đang có xung đột ràng buộc. Nhấp vào đó, Xcode sẽ chỉ rõ những ràng buộc nào đang &ldquo;đánh nhau&rdquo;.</p>
<p>Hãy đặt định danh (identifier) cho ràng buộc:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">heightConstraint</span><span class="p">.</span><span class="n">identifier</span> <span class="p">=</span> <span class="s">&#34;ProfileImageHeight&#34;</span></span></span></code></pre></div></div>
<p>Khi ràng buộc bị vỡ, thông báo lỗi sẽ hiện <code>&quot;ProfileImageHeight&quot;</code> thay vì một địa chỉ bộ nhớ, và lúc đó nhật ký mới thực sự đọc được.</p>
<p>Bạn cũng có thể in cây ràng buộc ra từ LLDB:</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) po view.hasAmbiguousLayout
(lldb) po view._autolayoutTrace()</code></pre></div>

<h2 class="relative group">Những tính năng của trình giả lập giúp tiết kiệm thời gian
    <div id="những-tính-năng-của-trình-giả-lập-giúp-tiết-kiệm-thời-gian" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#nh%e1%bb%afng-t%c3%adnh-n%c4%83ng-c%e1%bb%a7a-tr%c3%acnh-gi%e1%ba%a3-l%e1%ba%adp-gi%c3%bap-ti%e1%ba%bft-ki%e1%bb%87m-th%e1%bb%9di-gian" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>

<h3 class="relative group">Làm chậm hoạt ảnh (Slow Animations)
    <div id="làm-chậm-hoạt-ảnh-slow-animations" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#l%c3%a0m-ch%e1%ba%adm-ho%e1%ba%a1t-%e1%ba%a3nh-slow-animations" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Debug → Slow Animations làm mọi thứ chạy chậm lại còn 1/10. Rất hợp để xem trong một hiệu ứng chuyển cảnh thực sự đang xảy ra chuyện gì, hoặc tìm hiểu vì sao một hoạt ảnh trông sai sai.</p>

<h3 class="relative group">Giả lập cảnh báo bộ nhớ
    <div id="giả-lập-cảnh-báo-bộ-nhớ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#gi%e1%ba%a3-l%e1%ba%adp-c%e1%ba%a3nh-b%c3%a1o-b%e1%bb%99-nh%e1%bb%9b" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Debug → Simulate Memory Warning giúp bạn kiểm tra xem ứng dụng xoay xở ra sao khi thiếu bộ nhớ. Tôi đã tìm ra khá nhiều lỗi liên quan đến bộ nhớ đệm (cache) ảnh theo cách này: mọi thứ vẫn chạy ổn, cho tới khi cảnh báo được phát ra, và tự dưng toàn bộ ảnh biến mất sạch.</p>

<h3 class="relative group">Network Link Conditioner
    <div id="network-link-conditioner" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#network-link-conditioner" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Xcode → Open Developer Tools → Network Link Conditioner</p>
<p>Công cụ này giả lập mạng 3G, LTE, mạng mất gói nhiều, hoặc mất kết nối hoàn toàn. Nếu bạn từng gặp một yêu cầu API chạy ngon lành trên WiFi nhưng lại hết thời gian chờ (timeout) khi dùng 4G, thì đây là cách để tái hiện lỗi đó.</p>
<p>Tôi luôn để sẵn một cấu hình tên &ldquo;Bad Network&rdquo;, với độ trễ 500ms và tỉ lệ mất gói 10%. Nhờ nó mà lộ ra những lỗi hết thời gian chờ và lỗi ở trạng thái đang tải, loại lỗi bạn sẽ không bao giờ thấy khi ngồi dùng WiFi tốc độ cao ở văn phòng.</p>

<h3 class="relative group">Ghi đè thanh trạng thái
    <div id="ghi-đè-thanh-trạng-thái" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ghi-%c4%91%c3%a8-thanh-tr%e1%ba%a1ng-th%c3%a1i" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Nhấp chuột phải vào thanh trạng thái (status bar) trong trình giả lập là bạn ghi đè được giờ, mức pin, cường độ sóng và tên nhà mạng. Rất tiện khi cần chụp ảnh màn hình đồng nhất, hoặc muốn kiểm tra giao diện trông thế nào ở các mức pin khác nhau.</p>

<h3 class="relative group">Giả lập vị trí
    <div id="giả-lập-vị-trí" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#gi%e1%ba%a3-l%e1%ba%adp-v%e1%bb%8b-tr%c3%ad" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Debug → Location cho phép giả lập các vị trí khác nhau mà không cần rời khỏi bàn làm việc: tọa độ tùy chọn, đi bộ trong thành phố, hay lái xe trên cao tốc. Rất hữu ích khi làm các tính năng dựa trên vị trí, hoặc khi gặp những lỗi chỉ xuất hiện ở một số khu vực nhất định.</p>

<h2 class="relative group">Gỡ lỗi bộ nhớ
    <div id="gỡ-lỗi-bộ-nhớ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#g%e1%bb%a1-l%e1%bb%97i-b%e1%bb%99-nh%e1%bb%9b" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>

<h3 class="relative group">Debug Memory Graph
    <div id="debug-memory-graph" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#debug-memory-graph" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Bấm nút Debug Memory Graph (Cmd+Shift+M), Xcode sẽ hiện ra mọi đối tượng đang nằm trong bộ nhớ, quan hệ giữa chúng, và phần bạn thực sự quan tâm: đối tượng nào đang bị rò rỉ bộ nhớ (memory leak).</p>
<p>Dấu chấm than màu tím nghĩa là có rò rỉ. Nhấp vào là bạn thấy ngay vòng tham chiếu (retain cycle).</p>
<p>Đây là kiểu rò rỉ tôi gặp nhiều nhất:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">ViewController</span><span class="p">:</span> <span class="n">UIViewController</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nv">onComplete</span><span class="p">:</span> <span class="p">(()</span> <span class="p">-&gt;</span> <span class="nb">Void</span><span class="p">)?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">setupHandler</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">onComplete</span> <span class="p">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kc">self</span><span class="p">.</span><span class="n">dismiss</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>  <span class="c1">// ❌ Captures self strongly</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>Cách sửa là dùng weak self:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">onComplete</span> <span class="p">=</span> <span class="p">{</span> <span class="p">[</span><span class="kr">weak</span> <span class="kc">self</span><span class="p">]</span> <span class="k">in</span>
</span></span><span class="line"><span class="cl">    <span class="kc">self</span><span class="p">?.</span><span class="n">dismiss</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>  <span class="c1">// ✓ No retain cycle</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>Delegate cũng phải khai báo weak:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kr">weak</span> <span class="kd">var</span> <span class="nv">delegate</span><span class="p">:</span> <span class="n">ManagerDelegate</span><span class="p">?</span>  <span class="c1">// Not just &#39;var&#39;</span></span></span></code></pre></div></div>

<h3 class="relative group">Instruments
    <div id="instruments" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#instruments" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Khi cần điều tra bộ nhớ nghiêm túc, hãy dùng Instruments (Product → Profile, hoặc Cmd+I).</p>
<p>Công cụ Allocations trong Instruments cho bạn thấy từng lần cấp phát đối tượng, bộ nhớ tăng lên theo thời gian ra sao, lớp nào đang ngốn nhiều bộ nhớ nhất, kèm stack trace cho biết mỗi lần cấp phát xảy ra ở đâu.</p>
<p>Tôi thường tìm ba thứ: bộ nhớ tăng đều theo thời gian (nhiều khả năng là rò rỉ), những lần cấp phát lớn bất thường (mình vừa nạp cả 1000 tấm ảnh một lúc à?), và những đối tượng lẽ ra đã phải được giải phóng nhưng vẫn còn đó.</p>
<p>Bấm đánh dấu generation (nút hình lá cờ nhỏ) trước và sau một thao tác để xem thứ gì vẫn còn nằm lại trong bộ nhớ khi đáng lẽ phải được giải phóng.</p>

<h2 class="relative group">Đo hiệu năng
    <div id="đo-hiệu-năng" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%c4%91o-hi%e1%bb%87u-n%c4%83ng" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>

<h3 class="relative group">Time Profiler
    <div id="time-profiler" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#time-profiler" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Product → Profile → Time Profiler. Bấm ghi, thực hiện đúng thao tác đang chậm trong ứng dụng, rồi đọc cây lời gọi hàm (call tree).</p>
<p>Sắp xếp theo &ldquo;Self Weight&rdquo; để tìm điểm nghẽn (bottleneck). Hàm nào có self weight tới 40% thì đó chính là chỗ thời gian của bạn đang bị ngốn.</p>
<p>Có lần tôi phát hiện một hàm phân tích JSON bị gọi tới 1000 lần mỗi giây. Chuyển nó sang hàng đợi chạy nền (background queue) xong là giao diện hết giật ngay.</p>

<h3 class="relative group">Main Thread Checker
    <div id="main-thread-checker" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#main-thread-checker" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Xcode tự động phát hiện những chỗ cập nhật giao diện từ luồng nền (background thread). Nếu bạn thấy dòng này:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Main Thread Checker: UI API called on a background thread: -[UILabel setText:]</span></span></code></pre></div></div>
<p>thì nghĩa là bạn đã viết kiểu như thế này:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">URLSession</span><span class="p">.</span><span class="n">shared</span><span class="p">.</span><span class="n">dataTask</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="n">data</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">error</span> <span class="k">in</span>
</span></span><span class="line"><span class="cl">    <span class="kc">self</span><span class="p">.</span><span class="n">label</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="s">&#34;Loaded&#34;</span>  <span class="c1">// ❌ Crash!</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>Sửa lại như sau:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">URLSession</span><span class="p">.</span><span class="n">shared</span><span class="p">.</span><span class="n">dataTask</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="n">data</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">error</span> <span class="k">in</span>
</span></span><span class="line"><span class="cl">    <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kc">self</span><span class="p">.</span><span class="n">label</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="s">&#34;Loaded&#34;</span>  <span class="c1">// ✓</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h2 class="relative group">Chẩn đoán khi chạy (Runtime Diagnostics)
    <div id="chẩn-đoán-khi-chạy-runtime-diagnostics" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ch%e1%ba%a9n-%c4%91o%c3%a1n-khi-ch%e1%ba%a1y-runtime-diagnostics" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Vào Edit Scheme → Run → Diagnostics. Ở đây có một loạt ô đánh dấu, mỗi ô bắt được những lỗi mà tự tay bạn sẽ không bao giờ tìm ra.</p>

<h3 class="relative group">Address Sanitizer
    <div id="address-sanitizer" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#address-sanitizer" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Công cụ này tìm lỗi hỏng bộ nhớ: use-after-free, tràn bộ đệm (buffer overflow), rò rỉ bộ nhớ. Nó làm ứng dụng chậm đi 2-3 lần, nhưng bù lại bắt được những lỗi gần như không thể lần ra bằng cách nào khác. Tôi bật nó mỗi khi đi săn những vụ sập lúc có lúc không.</p>

<h3 class="relative group">Thread Sanitizer
    <div id="thread-sanitizer" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#thread-sanitizer" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Thread Sanitizer bắt lỗi tranh chấp dữ liệu (data race) và các lỗi liên quan đến luồng (thread). Bạn chỉ cần bật lên, chạy ứng dụng và dùng như bình thường; nếu trong mã có tình trạng tranh chấp (race condition), nó sẽ tìm ra.</p>
<p>Không thể chạy Address Sanitizer và Thread Sanitizer cùng lúc, nên khi gặp một vụ sập có biểu hiện kỳ quặc, tôi bật luân phiên từng cái một.</p>

<h3 class="relative group">Zombie Objects
    <div id="zombie-objects" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#zombie-objects" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Tùy chọn này bắt các thông điệp (message) được gửi tới đối tượng đã bị giải phóng. Khi bật lên, Xcode sẽ báo chính xác lúc nào bạn đang đụng vào vùng nhớ đã được giải phóng:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">*** -[MyViewController viewDidLoad]: message sent to deallocated instance 0x600001234000</span></span></code></pre></div></div>
<p>Đừng để nó bật thường trực. Zombie Objects ngăn đối tượng bị giải phóng, nên bộ nhớ cứ thế tăng mãi. Nhưng để khoanh vùng một lỗi use-after-free cụ thể thì nó rất tuyệt.</p>

<h2 class="relative group">Những tình huống hay gặp
    <div id="những-tình-huống-hay-gặp" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#nh%e1%bb%afng-t%c3%acnh-hu%e1%bb%91ng-hay-g%e1%ba%b7p" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>

<h3 class="relative group">Ứng dụng bị sập ngay khi mở
    <div id="ứng-dụng-bị-sập-ngay-khi-mở" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e1%bb%a9ng-d%e1%bb%a5ng-b%e1%bb%8b-s%e1%ba%adp-ngay-khi-m%e1%bb%9f" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Trước hết hãy thêm điểm dừng ngoại lệ; thường thì thế là bắt được luôn.</p>
<p>Nếu vẫn chưa ra, mấy &ldquo;nghi phạm quen mặt&rdquo; là: thiếu khóa (key) trong Info.plist, ép mở gói (force-unwrap) optional trong phần mã khởi tạo, hoặc cơ chế tiêm phụ thuộc (dependency injection) bị lỗi.</p>

<h3 class="relative group">Giao diện không cập nhật
    <div id="giao-diện-không-cập-nhật" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#giao-di%e1%bb%87n-kh%c3%b4ng-c%e1%ba%adp-nh%e1%ba%adt" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Lần. Nào. Cũng. Vậy. Nguyên nhân chỉ nằm trong mấy trường hợp sau:</p>
<ol>
<li>Việc cập nhật không chạy trên luồng chính (main thread)</li>
<li>Outlet chưa được nối</li>
<li>View thực ra không hiển thị (kiểm tra bằng <code>po view.window</code> trong LLDB; nếu kết quả là nil thì view không nằm trong cây phân cấp)</li>
</ol>
<p>Còn nếu là table view hay collection view: bạn quên gọi <code>reloadData()</code> rồi.</p>

<h3 class="relative group">Ứng dụng bị sập vì cảnh báo bộ nhớ
    <div id="ứng-dụng-bị-sập-vì-cảnh-báo-bộ-nhớ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e1%bb%a9ng-d%e1%bb%a5ng-b%e1%bb%8b-s%e1%ba%adp-v%c3%ac-c%e1%ba%a3nh-b%c3%a1o-b%e1%bb%99-nh%e1%bb%9b" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Dùng Allocations trong Instruments để xem thứ gì đang giữ bộ nhớ. Thường là ảnh nằm trong bộ nhớ đệm mà không bao giờ được nhả ra, view controller không chịu giải phóng (vòng tham chiếu), hoặc một mảng hay dictionary nào đó cứ phình to mãi.</p>
<p>Hãy giả lập cảnh báo bộ nhớ trong trình giả lập để kiểm tra phần mã dọn dẹp của bạn.</p>

<h3 class="relative group">Những vụ sập khó hiểu
    <div id="những-vụ-sập-khó-hiểu" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#nh%e1%bb%afng-v%e1%bb%a5-s%e1%ba%adp-kh%c3%b3-hi%e1%bb%83u" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Bật Sanitizer lên. Sập lúc có lúc không thường là do luồng, nên hãy chạy với Thread Sanitizer. Còn sập bên trong framework của hệ thống thì thường là do hỏng bộ nhớ, nên chạy với Address Sanitizer.</p>

<h3 class="relative group">Những vụ sập không tái hiện được
    <div id="những-vụ-sập-không-tái-hiện-được" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#nh%e1%bb%afng-v%e1%bb%a5-s%e1%ba%adp-kh%c3%b4ng-t%c3%a1i-hi%e1%bb%87n-%c4%91%c6%b0%e1%bb%a3c" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Khi vụ sập xảy ra trên điện thoại của người khác, thường thì báo cáo sự cố vẫn còn lưu lại ở đâu đó. Với các bản build TestFlight và App Store, Organizer của Xcode (Window → Organizer → Crashes) thu thập báo cáo sự cố từ những người dùng đồng ý chia sẻ dữ liệu phân tích, và báo cáo đã được giải mã ký hiệu (symbolicate) sẵn nếu tệp dSYM được tải lên cùng bản build. Còn với máy của người kiểm thử, báo cáo nằm ngay trên thiết bị, trong Settings → Privacy &amp; Security → Analytics &amp; Improvements → Analytics Data, dưới dạng các tệp <code>.ips</code> có tên bắt đầu bằng tên ứng dụng, và họ có thể gửi cho bạn qua AirDrop. Hãy bắt đầu từ stack trace của luồng bị sập, rồi lần lượt đối chiếu với các tình huống ở trên.</p>

<h3 class="relative group">Gỡ lỗi bản cập nhật OTA trong trình giả lập
    <div id="gỡ-lỗi-bản-cập-nhật-ota-trong-trình-giả-lập" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#g%e1%bb%a1-l%e1%bb%97i-b%e1%ba%a3n-c%e1%ba%adp-nh%e1%ba%adt-ota-trong-tr%c3%acnh-gi%e1%ba%a3-l%e1%ba%adp" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Ứng dụng của tôi có phát hành bản cập nhật qua mạng (OTA), và trình giả lập là nơi dễ nhất để gỡ lỗi toàn bộ luồng cập nhật đó.</p>
<p>Việc đầu tiên tôi làm là theo dõi lưu lượng mạng. Mở Console.app, lọc theo phân hệ của ứng dụng, bạn sẽ thấy các yêu cầu và phản hồi của tệp manifest theo thời gian thực: tiêu đề (header) nào được gửi đi, máy chủ trả về cái gì, rõ ràng từng thứ.</p>
<p>Tiếp theo, tôi cố tình làm cho mạng tệ đi. Network Link Conditioner cho bạn thấy luồng cập nhật xoay xở ra sao khi kết nối chậm và mất gói. Ứng dụng có bị treo không? Có hiện biểu tượng đang tải không? Có chuyển sang phương án dự phòng êm ái không?</p>
<p>Quanh bước kiểm tra cập nhật, tôi ghi nhật ký mọi thứ: lúc bắt đầu kiểm tra, lúc phát hiện có bản mới, lúc bắt đầu tải xuống, và lúc gói (bundle) mới được nạp.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">os</span><span class="p">.</span><span class="nc">log</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">logger</span> <span class="p">=</span> <span class="n">Logger</span><span class="p">(</span><span class="n">subsystem</span><span class="p">:</span> <span class="s">&#34;com.example.app&#34;</span><span class="p">,</span> <span class="n">category</span><span class="p">:</span> <span class="s">&#34;updates&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">&#34;Checking for updates...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">update</span> <span class="p">=</span> <span class="k">try</span> <span class="n">await</span> <span class="n">Updates</span><span class="p">.</span><span class="n">checkForUpdateAsync</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">&#34;Update available: </span><span class="si">\(</span><span class="n">update</span><span class="p">.</span><span class="n">isAvailable</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Cuối cùng, bạn nên kiểm tra riêng endpoint manifest. Dùng curl gọi nó với đúng những tiêu đề mà ứng dụng gửi đi:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -H <span class="s2">&#34;expo-protocol-version: 1&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">     -H <span class="s2">&#34;expo-platform: ios&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">     -H <span class="s2">&#34;expo-runtime-version: 1.0.0&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">     https://updates.example.com/manifest</span></span></code></pre></div></div>
<p>Làm vậy để chắc chắn máy chủ đang trả về đúng dữ liệu, trước khi bạn lao vào đào bới phía máy khách.</p>

<h2 class="relative group">Công cụ bên thứ ba
    <div id="công-cụ-bên-thứ-ba" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%b4ng-c%e1%bb%a5-b%c3%aan-th%e1%bb%a9-ba" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>

<h3 class="relative group">Charles Proxy / Proxyman
    <div id="charles-proxy--proxyman" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#charles-proxy--proxyman" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Hai công cụ này cho bạn xem toàn bộ lưu lượng mạng, sửa yêu cầu và phản hồi, và thử các tình huống lỗi. Gỡ lỗi API mà thiếu chúng thì rất khổ.</p>
<p>Proxyman có giao diện đẹp hơn Charles và dùng trên macOS thấy tự nhiên hơn. Tôi chuyển sang từ năm ngoái và chưa một lần muốn quay lại.</p>

<h3 class="relative group">Reveal
    <div id="reveal" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#reveal" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Reveal giống View Debugger của Xcode nhưng mạnh hơn. Nó chạy được trên máy thật mà không cần kết nối với Xcode, nên rất hợp để truy lỗi bố cục ngay trên máy của người kiểm thử (tester).</p>

<h2 class="relative group">Trình giả lập hay máy thật
    <div id="trình-giả-lập-hay-máy-thật" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#tr%c3%acnh-gi%e1%ba%a3-l%e1%ba%adp-hay-m%c3%a1y-th%e1%ba%adt" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Phần lớn việc gỡ lỗi tôi làm trên trình giả lập, vì nó nhanh: không cần cáp, không cần ký ứng dụng (code signing), không phải chờ cài đặt. Nhưng trình giả lập thực chất là một bản iOS chạy trên bộ xử lý của máy Mac, dùng bộ nhớ và kết nối mạng của máy Mac, nên có những lỗi chỉ tồn tại trên điện thoại thật.</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Trình giả lập</th>
					<th>iPhone thật</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Tốc độ</td>
					<td>CPU của máy Mac, thường nhanh hơn điện thoại</td>
					<td>Số liệu thật; nên đo hiệu năng ở đây</td>
			</tr>
			<tr>
					<td>Bộ nhớ</td>
					<td>Gần như không giới hạn, nên bạn phải tự giả lập cảnh báo bộ nhớ</td>
					<td>Hệ thống tắt ứng dụng nào dùng quá nhiều</td>
			</tr>
			<tr>
					<td>Mạng</td>
					<td>Kết nối của máy Mac</td>
					<td>Mạng di động, sóng yếu, chuyển qua lại giữa Wi-Fi và dữ liệu di động</td>
			</tr>
			<tr>
					<td>Camera, Bluetooth, NFC</td>
					<td>Không có</td>
					<td>Có</td>
			</tr>
			<tr>
					<td>Thông báo đẩy (push notification)</td>
					<td>Giả lập được bằng <code>xcrun simctl push</code></td>
					<td>Nhận thật, đúng như cách người dùng nhận</td>
			</tr>
			<tr>
					<td>Vòng sửa và chạy thử</td>
					<td>Vài giây</td>
					<td>Chậm hơn: ký ứng dụng, cài đặt, qua cáp hoặc Wi-Fi</td>
			</tr>
	</tbody>
</table>
<p>Vì vậy tôi viết và gỡ lỗi phần logic trên trình giả lập, còn trước mỗi lần phát hành thì chạy ứng dụng trên chiếc iPhone cũ nhất mà tôi vẫn còn hỗ trợ. Hiệu năng, tình trạng thiếu bộ nhớ và bất cứ thứ gì đụng tới phần cứng thì chưa chạy trên máy thật là coi như chưa thực sự được kiểm thử.</p>

<h3 class="relative group">Chạy ứng dụng trên máy thật
    <div id="chạy-ứng-dụng-trên-máy-thật" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ch%e1%ba%a1y-%e1%bb%a9ng-d%e1%bb%a5ng-tr%c3%aan-m%c3%a1y-th%e1%ba%adt" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Cắm iPhone vào máy Mac, chọn nó làm thiết bị chạy (run destination), rồi chọn team trong mục Signing &amp; Capabilities (nếu chỉ chạy trên điện thoại của chính bạn thì một Apple ID miễn phí là đủ). Từ iOS 16 trở đi, điện thoại còn phải bật Developer Mode trong Settings → Privacy &amp; Security, và lần đầu bật thì máy sẽ khởi động lại. Bật xong, ngay trong Settings → Developer của điện thoại cũng có Network Link Conditioner, với đúng những cấu hình mạng kém giống như trên máy Mac.</p>

<h3 class="relative group">Gỡ lỗi không dây
    <div id="gỡ-lỗi-không-dây" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#g%e1%bb%a1-l%e1%bb%97i-kh%c3%b4ng-d%c3%a2y" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Cắm máy vào Mac qua cáp USB một lần, vào Window → Devices and Simulators, đánh dấu &ldquo;Connect via network&rdquo;, rồi rút cáp ra. Chỉ cần máy và Mac dùng chung mạng WiFi là máy vẫn hiện trong Xcode.</p>

<h3 class="relative group">Device Console
    <div id="device-console" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#device-console" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Window → Devices and Simulators → Open Console. Ở đây hiện đủ mọi thứ: nhật ký hệ thống, nhật ký của ứng dụng, báo cáo sự cố. Chi tiết hơn console của Xcode rất nhiều.</p>
<p>Mỗi khi người kiểm thử báo &ldquo;ứng dụng bị sập&rdquo;, tôi luôn nhờ họ cắm máy vào để lấy nhật ký console. Rất hay gặp trường hợp có sẵn một lỗi ở cấp hệ thống nằm ngay đó, giải thích được toàn bộ câu chuyện.</p>

<h2 class="relative group">Tôi thực sự làm gì khi gỡ lỗi
    <div id="tôi-thực-sự-làm-gì-khi-gỡ-lỗi" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#t%c3%b4i-th%e1%bb%b1c-s%e1%bb%b1-l%c3%a0m-g%c3%ac-khi-g%e1%bb%a1-l%e1%bb%97i" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<ol>
<li>Thêm điểm dừng ngoại lệ nếu chưa có</li>
<li>Tái hiện lỗi</li>
<li>Đặt điểm dừng gần chỗ tôi nghi là đang sai</li>
<li>Kiểm tra giá trị bằng <code>po</code> trong LLDB</li>
<li>Sửa giá trị biến để thử cách sửa mà không cần biên dịch lại</li>
<li>Xem cây phân cấp view nếu lỗi liên quan đến giao diện</li>
<li>Phân tích hiệu năng (profile) bằng Instruments nếu lỗi liên quan đến hiệu năng</li>
<li>Bật Sanitizer nếu lỗi liên quan đến bộ nhớ hoặc luồng</li>
</ol>
<p>Bí quyết là đi ngược từ triệu chứng. Sập? Điểm dừng ngoại lệ. Chậm? Time Profiler. Bộ nhớ? Instruments. Giao diện kỳ cục? View Debugger.</p>

<h2 class="relative group">Những mẹo tôi ước mình biết sớm hơn
    <div id="những-mẹo-tôi-ước-mình-biết-sớm-hơn" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#nh%e1%bb%afng-m%e1%ba%b9o-t%c3%b4i-%c6%b0%e1%bb%9bc-m%c3%acnh-bi%e1%ba%bft-s%e1%bb%9bm-h%c6%a1n" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Hãy dùng assertion. Chúng bắt được lỗi logic trước khi lỗi đó kịp gây hậu quả thật:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="bp">assert</span><span class="p">(</span><span class="n">users</span><span class="p">.</span><span class="bp">count</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">,</span> <span class="s">&#34;Users array should never be empty here&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Commit trước khi bắt đầu gỡ lỗi. Một khi đã sửa lung tung để thử các giả thuyết, bạn sẽ cần một điểm sạch sẽ để quay về.</p>
<p>Gỡ lỗi xong thì xóa mấy lệnh print đi, hoặc ít nhất bọc chúng trong <code>#if DEBUG</code>. Nhật ký gỡ lỗi cũ để lại chỉ làm cho lỗi tiếp theo khó thấy hơn.</p>
<p>Học LLDB cho tử tế. Nhanh hơn nhiều so với việc biên dịch lại ứng dụng hai mươi lần.</p>
<p>Và nhớ rằng trình giả lập không phải điện thoại thật. Lỗi chỉ xuất hiện trên máy thật thì gần như lúc nào cũng là do luồng hoặc bộ nhớ. Hoặc do hiệu năng, vì trình giả lập chạy bằng CPU của máy Mac chứ không phải chip iPhone thật.</p>

<h2 class="relative group">Những công cụ tôi luôn để mở
    <div id="những-công-cụ-tôi-luôn-để-mở" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#nh%e1%bb%afng-c%c3%b4ng-c%e1%bb%a5-t%c3%b4i-lu%c3%b4n-%c4%91%e1%bb%83-m%e1%bb%9f" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<ul>
<li>Xcode, tất nhiên rồi</li>
<li>Console.app, lọc sẵn theo phân hệ của ứng dụng</li>
<li>Proxyman để gỡ lỗi mạng</li>
<li>Instruments, khi mọi chuyện bắt đầu nghiêm trọng</li>
</ul>
<hr>
<p>Không có công cụ nào lo được hết mọi thứ. View Debugger không tìm ra rò rỉ bộ nhớ, Instruments cũng không gỡ được xung đột Auto Layout, nên phần lớn kỹ năng nằm ở chỗ chọn đúng công cụ cho đúng triệu chứng. Cái này thì chỉ có làm nhiều mới quen: dính vòng tham chiếu đủ nhiều lần rồi, chỉ cần liếc stack trace là bạn đã &ldquo;ngửi&rdquo; ra nó. Gỡ lỗi chẳng bao giờ thực sự vui, nhưng ít ra nó sẽ không còn là cực hình nữa.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-02-26-debugging-ios-mobile-apps/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
