<?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/"><channel><title>Logcat on Tarragon</title><link>https://tarrragon.github.io/blog/tags/logcat/</link><description>Recent content in Logcat on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 21 Sep 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/logcat/index.xml" rel="self" type="application/rss+xml"/><item><title>用 adb 遠端診斷 Android 實機</title><link>https://tarrragon.github.io/blog/work-log/adb-remote-android-diagnosis/</link><pubDate>Mon, 21 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/adb-remote-android-diagnosis/</guid><description>&lt;p>這篇整理電腦經由 Wi-Fi 用 adb 連線到 Android 手機之後，遠端排查用到的指令：在多台裝置中指定要操作的那一台、擷取螢幕畫面與錄影並確認傳回的檔案完整、查出畫面最上層是哪個 App 的哪個 Activity、用 logcat 篩出某個元件啟動前後的紀錄、查詢 App 權限與使用者限制這類狀態，以及放寬安裝管控的修改型指令。Wi-Fi 配對與連線的步驟在&lt;a href="https://tarrragon.github.io/blog/other/android_wireless_debug/" data-link-title="Android 無線調試連接指南" data-link-desc="ADB 無線偵錯的配對與連接流程，含 device offline、端口錯誤、AP 隔離等連線中斷的排查。">Android 無線調試連接指南&lt;/a>。&lt;/p>
&lt;h2 id="指定裝置">指定裝置&lt;/h2>
&lt;p>adb 的一行指令分成三段：&lt;code>adb&lt;/code> 自己的全域選項、子命令（如 &lt;code>shell&lt;/code>）自己的選項、以及子命令之後要送去遠端執行的字串。選擇裝置的 &lt;code>-s&lt;/code> / &lt;code>-t&lt;/code> 是全域選項，要寫在子命令之前。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">adb devices -l &lt;span class="c1"># -l 多印 product / model / transport_id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">adb -s &amp;lt;serial&amp;gt; shell &amp;lt;command&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">adb -t &amp;lt;transport_id&amp;gt; shell &amp;lt;command&amp;gt; &lt;span class="c1"># transport_id 較短、同一次連線內有效&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>寫成 &lt;code>adb shell -s &amp;lt;serial&amp;gt; &amp;lt;command&amp;gt;&lt;/code> 時，&lt;code>-s&lt;/code> 被劃進 &lt;code>shell&lt;/code> 子命令的選項，而 &lt;code>shell&lt;/code> 的選項解析器不認得它，回 &lt;code>adb shell: illegal option -- s&lt;/code>，指令沒有被送到遠端執行。沒有指定 transport 而多台在線時，adb 在解析目標裝置那一步就失敗、回 &lt;code>adb: more than one device/emulator&lt;/code>，&lt;code>shell&lt;/code> 的選項解析沒有被執行。同一個寫錯的指令因此有兩種回報，指向選項位置的那一則只在單機在線時看得到。&lt;/p>
&lt;p>同一台無線機器可能同時以 IP 位址與 mDNS 名稱兩個 transport 在線，&lt;code>adb devices&lt;/code> 因此列出兩列而背後是同一台。&lt;code>adb devices -l&lt;/code> 印的 product / model 欄位在同一批機器上完全相同，靠它們分辨不了；硬體序號可以分辨。&lt;code>getprop&lt;/code> 讀裝置的系統屬性，引數是屬性名稱，&lt;code>ro.serialno&lt;/code> 存的是硬體序號。逐個 transport 問一次，回同一個值的是同一台。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> serial in &lt;span class="k">$(&lt;/span>adb devices &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;NR&amp;gt;1 &amp;amp;&amp;amp; $2==&amp;#34;device&amp;#34; {print $1}&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nv">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>adb -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$serial&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> shell getprop ro.product.model &lt;span class="p">|&lt;/span> tr -d &lt;span class="s1">&amp;#39;\r&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nv">brand&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>adb -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$serial&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> shell getprop ro.product.brand &lt;span class="p">|&lt;/span> tr -d &lt;span class="s1">&amp;#39;\r&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nv">sdk&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>adb -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$serial&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> shell getprop ro.build.version.sdk &lt;span class="p">|&lt;/span> tr -d &lt;span class="s1">&amp;#39;\r&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nv">hw&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>adb -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$serial&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> shell getprop ro.serialno &lt;span class="p">|&lt;/span> tr -d &lt;span class="s1">&amp;#39;\r&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$serial&lt;/span>&lt;span class="s2"> | &lt;/span>&lt;span class="nv">$brand&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="nv">$model&lt;/span>&lt;span class="s2"> (API &lt;/span>&lt;span class="nv">$sdk&lt;/span>&lt;span class="s2">) | &lt;/span>&lt;span class="nv">$hw&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>迴圈的成員取自 &lt;code>adb devices&lt;/code> 的輸出：&lt;code>awk&lt;/code> 的 &lt;code>NR&amp;gt;1&lt;/code> 跳過 &lt;code>List of devices attached&lt;/code> 這行標頭，&lt;code>$2==&amp;quot;device&amp;quot;&lt;/code> 只留下狀態欄是 &lt;code>device&lt;/code> 的列（&lt;code>offline&lt;/code> 與 &lt;code>unauthorized&lt;/code> 的機器接不上、要排掉），&lt;code>{print $1}&lt;/code> 印出第一欄的 transport 名稱。&lt;/p>
&lt;p>腳本裡每一行 &lt;code>getprop&lt;/code> 的結尾都接了 &lt;code>tr -d '\r'&lt;/code>，作用是去掉輸出裡的歸位字元（&lt;code>\r&lt;/code>）。adb 送輸出回來時若配置了 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/pty/" data-link-title="PTY（Pseudo-Terminal，虛擬終端機）" data-link-desc="二進位資料經過某條通道之後壞掉、或程式在管線裡與在終端機裡行為不同時讀 — 核心提供的一對虛擬終端機裝置">pty&lt;/a>（pseudo-terminal，虛擬終端機），而作業系統核心的 pty 行規則會把每個 &lt;code>0x0A&lt;/code>（換行）補成 &lt;code>0x0D 0x0A&lt;/code>（歸位加換行）。補進來的 &lt;code>0x0D&lt;/code> 肉眼看不見，但它會讓 shell 的字串比對失敗——&lt;code>&amp;quot;$model&amp;quot; = &amp;quot;&amp;lt;型號字串&amp;gt;&amp;quot;&lt;/code> 這種比較永遠回假，因為 &lt;code>$model&lt;/code> 的值尾端多了一個 &lt;code>\r&lt;/code>。沒有配置 pty 時 &lt;code>tr -d '\r'&lt;/code> 是空操作，加上去沒有副作用。&lt;/p>
&lt;h2 id="遠端擷圖">遠端擷圖&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 不寫路徑：PNG 的位元組送到標準輸出，由本機的 &amp;gt; 存檔，裝置端不留中繼檔&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">adb -s &amp;lt;serial&amp;gt; exec-out screencap -p &amp;gt; screen.png
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 寫路徑：PNG 存成裝置上的那個檔，標準輸出是空的，檔案要另外 pull 回來&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">adb -s &amp;lt;serial&amp;gt; shell screencap -p /sdcard/screen.png&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>screencap&lt;/code> 在裝置上抓當前畫面，&lt;code>-p&lt;/code> 指定編碼成 PNG。它另外接受一個選用的引數——輸出的檔案路徑——&lt;strong>上面兩條的差別只在寫不寫這個引數，而它決定畫面往哪裡去&lt;/strong>。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理電腦經由 Wi-Fi 用 adb 連線到 Android 手機之後，遠端排查用到的指令：在多台裝置中指定要操作的那一台、擷取螢幕畫面與錄影並確認傳回的檔案完整、查出畫面最上層是哪個 App 的哪個 Activity、用 logcat 篩出某個元件啟動前後的紀錄、查詢 App 權限與使用者限制這類狀態，以及放寬安裝管控的修改型指令。Wi-Fi 配對與連線的步驟在<a href="/blog/other/android_wireless_debug/" data-link-title="Android 無線調試連接指南" data-link-desc="ADB 無線偵錯的配對與連接流程，含 device offline、端口錯誤、AP 隔離等連線中斷的排查。">Android 無線調試連接指南</a>。</p>
<h2 id="指定裝置">指定裝置</h2>
<p>adb 的一行指令分成三段：<code>adb</code> 自己的全域選項、子命令（如 <code>shell</code>）自己的選項、以及子命令之後要送去遠端執行的字串。選擇裝置的 <code>-s</code> / <code>-t</code> 是全域選項，要寫在子命令之前。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb devices -l                          <span class="c1"># -l 多印 product / model / transport_id</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">adb -s &lt;serial&gt; shell &lt;command&gt;
</span></span><span class="line"><span class="ln">3</span><span class="cl">adb -t &lt;transport_id&gt; shell &lt;command&gt;   <span class="c1"># transport_id 較短、同一次連線內有效</span></span></span></code></pre></div><p>寫成 <code>adb shell -s &lt;serial&gt; &lt;command&gt;</code> 時，<code>-s</code> 被劃進 <code>shell</code> 子命令的選項，而 <code>shell</code> 的選項解析器不認得它，回 <code>adb shell: illegal option -- s</code>，指令沒有被送到遠端執行。沒有指定 transport 而多台在線時，adb 在解析目標裝置那一步就失敗、回 <code>adb: more than one device/emulator</code>，<code>shell</code> 的選項解析沒有被執行。同一個寫錯的指令因此有兩種回報，指向選項位置的那一則只在單機在線時看得到。</p>
<p>同一台無線機器可能同時以 IP 位址與 mDNS 名稱兩個 transport 在線，<code>adb devices</code> 因此列出兩列而背後是同一台。<code>adb devices -l</code> 印的 product / model 欄位在同一批機器上完全相同，靠它們分辨不了；硬體序號可以分辨。<code>getprop</code> 讀裝置的系統屬性，引數是屬性名稱，<code>ro.serialno</code> 存的是硬體序號。逐個 transport 問一次，回同一個值的是同一台。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">for</span> serial in <span class="k">$(</span>adb devices <span class="p">|</span> awk <span class="s1">&#39;NR&gt;1 &amp;&amp; $2==&#34;device&#34; {print $1}&#39;</span><span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nv">model</span><span class="o">=</span><span class="k">$(</span>adb -s <span class="s2">&#34;</span><span class="nv">$serial</span><span class="s2">&#34;</span> shell getprop ro.product.model <span class="p">|</span> tr -d <span class="s1">&#39;\r&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nv">brand</span><span class="o">=</span><span class="k">$(</span>adb -s <span class="s2">&#34;</span><span class="nv">$serial</span><span class="s2">&#34;</span> shell getprop ro.product.brand <span class="p">|</span> tr -d <span class="s1">&#39;\r&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nv">sdk</span><span class="o">=</span><span class="k">$(</span>adb -s <span class="s2">&#34;</span><span class="nv">$serial</span><span class="s2">&#34;</span> shell getprop ro.build.version.sdk <span class="p">|</span> tr -d <span class="s1">&#39;\r&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nv">hw</span><span class="o">=</span><span class="k">$(</span>adb -s <span class="s2">&#34;</span><span class="nv">$serial</span><span class="s2">&#34;</span> shell getprop ro.serialno <span class="p">|</span> tr -d <span class="s1">&#39;\r&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$serial</span><span class="s2"> | </span><span class="nv">$brand</span><span class="s2"> </span><span class="nv">$model</span><span class="s2"> (API </span><span class="nv">$sdk</span><span class="s2">) | </span><span class="nv">$hw</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>迴圈的成員取自 <code>adb devices</code> 的輸出：<code>awk</code> 的 <code>NR&gt;1</code> 跳過 <code>List of devices attached</code> 這行標頭，<code>$2==&quot;device&quot;</code> 只留下狀態欄是 <code>device</code> 的列（<code>offline</code> 與 <code>unauthorized</code> 的機器接不上、要排掉），<code>{print $1}</code> 印出第一欄的 transport 名稱。</p>
<p>腳本裡每一行 <code>getprop</code> 的結尾都接了 <code>tr -d '\r'</code>，作用是去掉輸出裡的歸位字元（<code>\r</code>）。adb 送輸出回來時若配置了 <a href="/blog/linux/dotfile/knowledge-cards/pty/" data-link-title="PTY（Pseudo-Terminal，虛擬終端機）" data-link-desc="二進位資料經過某條通道之後壞掉、或程式在管線裡與在終端機裡行為不同時讀 — 核心提供的一對虛擬終端機裝置">pty</a>（pseudo-terminal，虛擬終端機），而作業系統核心的 pty 行規則會把每個 <code>0x0A</code>（換行）補成 <code>0x0D 0x0A</code>（歸位加換行）。補進來的 <code>0x0D</code> 肉眼看不見，但它會讓 shell 的字串比對失敗——<code>&quot;$model&quot; = &quot;&lt;型號字串&gt;&quot;</code> 這種比較永遠回假，因為 <code>$model</code> 的值尾端多了一個 <code>\r</code>。沒有配置 pty 時 <code>tr -d '\r'</code> 是空操作，加上去沒有副作用。</p>
<h2 id="遠端擷圖">遠端擷圖</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 不寫路徑：PNG 的位元組送到標準輸出，由本機的 &gt; 存檔，裝置端不留中繼檔</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">adb -s &lt;serial&gt; exec-out screencap -p &gt; screen.png
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 寫路徑：PNG 存成裝置上的那個檔，標準輸出是空的，檔案要另外 pull 回來</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">adb -s &lt;serial&gt; shell screencap -p /sdcard/screen.png</span></span></code></pre></div><p><code>screencap</code> 在裝置上抓當前畫面，<code>-p</code> 指定編碼成 PNG。它另外接受一個選用的引數——輸出的檔案路徑——<strong>上面兩條的差別只在寫不寫這個引數，而它決定畫面往哪裡去</strong>。</p>
<p>兩條各自的完整流程分開講：第一種的位元組要經過 adb 這條通道回到本機，本節接下來處理那條通道上的兩個問題（行尾轉換與 stderr 混入）；第二種在〈舊環境的兩段式做法〉，它多兩條指令（取回與清除），而因為沒有位元組經過通道，用 <code>shell</code> 送就夠了。</p>
<p>位元組從裝置送回本機的過程中會經過一次可能的行尾轉換。走 <code>exec-out</code> 時 adb 不配置 pty，位元組原樣送回；走 <code>shell</code> 且配置了 pty 時，pty 行規則會把 <code>0x0A</code> 換成 <code>0x0D 0x0A</code>。PNG 裡的 <code>0x0A</code> 是資料，被換過的每一處都讓檔案多一個位元組，解碼失敗。</p>
<p><code>adb shell</code> 配不配置 pty 取決於 adb 版本、裝置端有沒有 shell protocol v2（<code>adb features</code> 裡有沒有 <code>shell_v2</code>）、以及有沒有帶命令參數——帶命令的不配置，不帶命令的互動模式才配。這條規則在裝置有 shell_v2 且沒帶 <code>-t</code> / <code>-tt</code> 時成立，沒有 v2 的舊組合不適用。用 <code>exec-out</code> 取二進位輸出，行尾轉換與否不再取決於這組版本組合。</p>
<p>可以用一段已知的位元組確認手上這條通道有沒有配 pty：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb -s &lt;serial&gt; exec-out <span class="s2">&#34;printf &#39;a\nb\n&#39;&#34;</span> <span class="p">|</span> xxd
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 610a 620a        — 沒有轉換</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">adb -s &lt;serial&gt; shell -tt <span class="s2">&#34;printf &#39;a\nb\n&#39;&#34;</span> &lt; /dev/null <span class="p">|</span> xxd
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 610d 0a62 0d0a   — 轉換了</span></span></span></code></pre></div><p><code>exec-out</code> 有一個代價：它把遠端的 stderr 併進 stdout。遠端吐一行警告，它就排在 PNG 前面進了檔案。<code>file</code> 與本機 stderr 分不出這種壞檔跟行尾被改寫的壞檔——兩種都是 <code>file</code> 回 <code>data</code>、stderr 零行；分得出的是檔案開頭的位元組（<code>xxd -l 16</code>）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb -s &lt;serial&gt; exec-out <span class="s2">&#34;printf &#39;OUT\n&#39;; printf &#39;ERR\n&#39; &gt;&amp;2&#34;</span> <span class="p">|</span> xxd
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 4f55 540a 4552 520a        — stderr 混進來了</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">adb -s &lt;serial&gt; shell <span class="s2">&#34;printf &#39;OUT\n&#39;; printf &#39;ERR\n&#39; &gt;&amp;2&#34;</span> <span class="p">|</span> xxd
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 4f55 540a                  — shell（有 v2 時）分開處理</span></span></span></code></pre></div><h3 id="驗證檔案">驗證檔案</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">file screen.png
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 完好：PNG image data, &lt;寬&gt; x &lt;高&gt;, 8-bit/color RGBA, non-interlaced</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 開頭被塞了別的內容（行尾改寫或 stderr 混入）：data</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 零位元組（顯示器編號錯）：empty</span></span></span></code></pre></div><p><code>data</code> 有兩種來歷，分辨看檔案開頭：<code>xxd -l 16 screen.png</code>。完好 PNG 以 <code>8950 4e47 0d0a 1a0a</code> 起頭；行尾被改寫的簽章變成 <code>8950 4e47 0d0d 0a1a</code>；stderr 混入的開頭是讀得懂的文字。</p>
<h3 id="舊環境的兩段式做法">舊環境的兩段式做法</h3>
<p><code>exec-out</code> 不可用時，讓裝置端先寫檔再 <code>pull</code> 回來。三條指令的引數各是什麼：</p>
<ul>
<li><code>screencap -p &lt;裝置上的路徑&gt;</code>：寫了路徑，<code>screencap</code> 把 PNG 存成裝置上的檔案</li>
<li><code>pull &lt;裝置上的路徑&gt; &lt;本機的路徑&gt;</code>：第一個引數是來源、在裝置上，第二個是目的地、在本機，順序固定。<code>pull</code> 走檔案傳輸通道，不做行尾處理、也不併 stderr</li>
<li><code>rm &lt;裝置上的路徑&gt;</code>：刪掉中繼檔，裝置上不留排查產生的檔案</li>
</ul>
<p><strong>路徑寫在 <code>/sdcard</code> 底下，是因為 <code>pull</code> 與 <code>shell</code> 讀得到的範圍不一樣</strong>：<code>/sdcard</code> 兩者都讀得到，系統分割區 <code>shell</code> 讀得到而 <code>pull</code> 可能回 <code>Permission denied</code>。<code>/sdcard</code> 是 <code>/storage/emulated/0</code> 的符號連結，<code>adb -s &lt;serial&gt; shell ls -l /sdcard</code> 印得出這個對應；後面〈狀態查詢〉查的 App 資料目錄在同一個掛載點底下。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb -s &lt;serial&gt; shell screencap -p /sdcard/screen.png
</span></span><span class="line"><span class="ln">2</span><span class="cl">adb -s &lt;serial&gt; pull /sdcard/screen.png ./device-screen.png
</span></span><span class="line"><span class="ln">3</span><span class="cl">adb -s &lt;serial&gt; shell rm /sdcard/screen.png</span></span></code></pre></div><h3 id="錄影">錄影</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb -s &lt;serial&gt; shell screenrecord --time-limit &lt;秒數&gt; /sdcard/demo.mp4
</span></span><span class="line"><span class="ln">2</span><span class="cl">adb -s &lt;serial&gt; pull /sdcard/demo.mp4 ./</span></span></code></pre></div><p><code>screenrecord</code> 的引數是錄影檔在裝置上的輸出路徑，所以錄完要 <code>pull</code> 回本機；<code>--time-limit</code> 接的是秒數。<code>pull</code> 的第二個引數寫 <code>./</code> 時，本機端沿用來源的檔名。時間上限由 <code>screenrecord</code> 自己定，<code>adb -s &lt;serial&gt; shell screenrecord --help</code> 會印出來。</p>
<h3 id="多螢幕機器的顯示器選擇">多螢幕機器的顯示器選擇</h3>
<p><code>screencap</code> 預設抓主顯示器。POS 機的客顯是對話框可能出現的地方之一，要加 <code>-d</code> 指定顯示器。</p>
<p><strong>編號要取自 SurfaceFlinger，不是 <code>dumpsys window</code> 的 <code>mDisplayId</code>。</strong> 兩套編號不相容，把 <code>mDisplayId</code> 餵給 <code>screencap -d</code> 的結果是成功退出、stderr 零行、檔案零位元組。<code>screencap --help</code> 自己就寫著要去 SurfaceFlinger 查。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb -s &lt;serial&gt; shell dumpsys display <span class="p">|</span> grep mViewports    <span class="c1"># 同一行印出兩套編號的對應</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 不加 -d：抓主顯示器</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">adb -s &lt;serial&gt; exec-out screencap -p &gt; screen.png
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 加 -d：抓指定的顯示器</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">adb -s &lt;serial&gt; exec-out screencap -p -d &lt;SurfaceFlinger 的編號&gt; &gt; screen.png</span></span></code></pre></div><p><code>dumpsys display</code> 的 <code>mViewports</code> 行裡 <code>displayId=</code> 是 <code>dumpsys window</code> 那一套，<code>uniqueId='local:&lt;數字&gt;'</code> 裡的數字是 <code>screencap -d</code> 要的那一套。</p>
<h2 id="前景元件查詢">前景元件查詢</h2>
<p><code>screencap</code> 的擷圖顯示畫面上寫了什麼；畫面由哪個元件繪製要用 <code>dumpsys window</code> 查：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb -s &lt;serial&gt; shell dumpsys window <span class="p">|</span> grep -iE <span class="s2">&#34;mCurrentFocus|mFocusedApp&#34;</span></span></span></code></pre></div><p>輸出帶套件名與 Activity 名。多螢幕的機器上，<code>dumpsys window</code> 替每個顯示器各印一組 <code>mCurrentFocus</code> 與 <code>mFocusedApp</code>，沒有焦點的顯示器這兩個欄位的值都是 <code>null</code>。讀輸出的人跳過值都是 <code>null</code> 的那幾組，值不是 <code>null</code> 的那一組就屬於有焦點的顯示器。</p>
<p>一個視窗可以畫在有焦點的視窗之上而不取得焦點——<code>APPLICATION_OVERLAY</code> 且 <code>NOT_FOCUSABLE</code> 的視窗就是這樣，<code>mCurrentFocus</code> 的值不變。任何持有 <code>SYSTEM_ALERT_WINDOW</code> 的第三方套件都畫得出來。排除 overlay 的方法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb -s &lt;serial&gt; shell appops query-op SYSTEM_ALERT_WINDOW allow     <span class="c1"># 誰有這個權限</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">adb -s &lt;serial&gt; shell dumpsys window windows <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  <span class="p">|</span> grep -E <span class="s2">&#34;Window #|package=|ty=|mViewVisibility=&#34;</span>                <span class="c1"># 當下的視窗堆疊</span></span></span></code></pre></div><p><code>appops query-op</code> 的兩個引數是 op 名稱與 mode，它列出在那個 op 上被設成該 mode 的套件。所以第一條列的是能力不是行為，有權限的套件不一定當下有視窗在畫面上。第二條的 <code>windows</code> 是傳給 window 服務的參數，要它印視窗清單那一段；輸出按 z-order 由上而下列出視窗，要同時看三個條件：排在目標之上、<code>ty=APPLICATION_OVERLAY</code>、且 <code>mViewVisibility=0x0</code>。第三個條件用來排除隱形視窗：<code>0x4</code> 是存在但不可見，系統自己就常駐幾個這樣的視窗（拖放目標、螢幕裝飾）。三個條件同時成立才代表有第三方視窗正在疊圖。從截圖分不出 overlay 與被改寫的元件——兩種在畫面上呈現相同的文字；分得開兩者的是 z-order 那份輸出。</p>
<h2 id="logcat">logcat</h2>
<p>logcat 讀的是裝置上的<a href="/blog/linux/dotfile/knowledge-cards/ring-buffer-log/" data-link-title="Ring Buffer（環狀緩衝區）" data-link-desc="撈 log 撈不到事發當下那幾行、或想知道一份 log 能回溯多久時讀 — 固定大小、寫滿之後從頭覆蓋的紀錄區">環狀緩衝區</a>，寫滿之後從頭覆蓋。時間窗由清空與倒出界定，中間那一段是重現問題時裝置寫進去的。</p>
<p>macOS 上常見的替代做法是用 <code>timeout</code> 控制收集時間，但 <code>timeout</code> 屬於 GNU coreutils，macOS 預設沒有（裝了 coreutils 之後叫 <code>gtimeout</code>）。<code>timeout 300 adb logcat &gt; install.log</code> 在 macOS 上留下的是 0 bytes 的 <code>install.log</code>——<code>command not found: timeout</code> 走 stderr、沒進重導檔。清空與倒出已經界定了時間窗，收集時間由操作本身決定，本機的計時程序沒有需要做的事。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb -s &lt;serial&gt; logcat -G 16M                          <span class="c1"># 放大緩衝區</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">adb -s &lt;serial&gt; logcat -c                              <span class="c1"># 清空，時間窗起點</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># （在機器上重現問題）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">adb -s &lt;serial&gt; logcat -d -v threadtime &gt; install.log    <span class="c1"># 倒出，時間窗終點</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">adb -s &lt;serial&gt; logcat -g                              <span class="c1"># 確認各緩衝區大小</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>參數</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>-G 16M</code></td>
          <td>放大緩衝區。POS 機、車機這類常駐服務多的機器，緩衝區在重現過程中就可能把最早的行覆蓋掉——關鍵行往往正是操作之前的那幾行。<code>16M</code> 是起點，對著 <code>-g</code> 的現況調</td>
      </tr>
      <tr>
          <td><code>-c</code></td>
          <td>清空</td>
      </tr>
      <tr>
          <td><code>-v threadtime</code></td>
          <td>每行帶 PID / TID 與精確時間戳</td>
      </tr>
      <tr>
          <td><code>-d</code></td>
          <td>一次倒出後結束，不停在前景等</td>
      </tr>
      <tr>
          <td><code>-g</code></td>
          <td>每個緩衝區各印一行，設完跑一次看 <code>-G</code> 涵蓋了哪幾個</td>
      </tr>
  </tbody>
</table>
<h3 id="pid-與-tag">PID 與 tag</h3>
<p><code>-v threadtime</code> 的欄位順序是日期、時間、PID、TID、層級、tag。PID 直接指出發出這一行的 process；tag 由各元件自行填寫、跨元件會重複，只憑 tag 把 log 行對應到 process 是推測。</p>
<h3 id="篩選">篩選</h3>
<p>時間排在第二欄，零填補定寬（<code>HH:MM:SS.mmm</code>），可以直接用字串比較篩。兩側都要寫——logcat 會插入 <code>--------- beginning of main</code> 這類分隔行，第二欄是 <code>beginning</code>，字串序大於任何時間值，只寫下界會把它放進來。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">grep -vE <span class="s2">&#34;&lt;佔多數的服務 tag&gt;&#34;</span> install.log <span class="p">|</span> tail -&lt;行數&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl">awk <span class="s1">&#39;$2 &gt;= &#34;16:26:00.000&#34; &amp;&amp; $2 &lt;= &#34;16:27:00.000&#34;&#39;</span> install.log <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  <span class="p">|</span> grep -viE <span class="s2">&#34;adbd|&lt;該機常駐服務的 tag&gt;&#34;</span></span></span></code></pre></div><p>找出佔多數的 tag：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">awk <span class="s1">&#39;{print $6}&#39;</span> install.log <span class="p">|</span> sort <span class="p">|</span> uniq -c <span class="p">|</span> sort -rn <span class="p">|</span> head</span></span></code></pre></div><p>logcat 把 tag 欄補寬到八個字元。短於八個字元的 tag 後面補了空白，<code>awk</code> 用空白切欄時那個冒號因此被切成獨立的一欄，所以長 tag 在第六欄帶冒號（<code>WifiVendorHal:</code>）、短 tag 不帶（<code>adbd</code>）。統計結果貼回 <code>awk</code> 做精確比對前，先確認有沒有尾冒號。</p>
<h3 id="找特定元件的啟動紀錄">找特定元件的啟動紀錄</h3>
<p>安裝器有沒有被啟動，看 <code>ActivityTaskManager</code> 的視窗切換紀錄：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">grep -E <span class="s1">&#39;ActivityTaskManager.*(START|Displayed)&#39;</span> install.log <span class="p">|</span> grep -i &lt;安裝器套件名&gt;</span></span></code></pre></div><p>這條 <code>grep</code> 的輸出裡有 <code>cmp=&lt;套件&gt;/.InstallStart</code> 這一行，代表 App 已經送出安裝意圖；輸出是空的，代表 App 沒有送出安裝意圖。</p>
<h2 id="狀態查詢">狀態查詢</h2>
<p><code>dumpsys &lt;服務&gt;</code> 把問題轉給那個服務自己的 dump 實作，<code>dumpsys window</code> 和 <code>dumpsys user</code> 的回答由不同的元件產生，格式各自決定。服務不存在時回一行 <code>Can't find service: &lt;名字&gt;</code> 並成功退出，<code>grep</code> 的結果是零行——先用 <code>dumpsys -l | grep &lt;服務&gt;</code> 確認服務在不在。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb -s &lt;serial&gt; shell pm list packages <span class="p">|</span> grep -i &lt;keyword&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl">adb -s &lt;serial&gt; shell appops get &lt;package&gt; REQUEST_INSTALL_PACKAGES
</span></span><span class="line"><span class="ln">3</span><span class="cl">adb -s &lt;serial&gt; shell dumpsys device_policy <span class="p">|</span> grep -iE <span class="s2">&#34;owner|admin|restriction&#34;</span> -A&lt;行數&gt;
</span></span><span class="line"><span class="ln">4</span><span class="cl">adb -s &lt;serial&gt; shell dumpsys user <span class="p">|</span> grep -i restriction -A&lt;行數&gt;
</span></span><span class="line"><span class="ln">5</span><span class="cl">adb -s &lt;serial&gt; shell settings list secure <span class="p">|</span> grep -i &lt;key&gt;
</span></span><span class="line"><span class="ln">6</span><span class="cl">adb -s &lt;serial&gt; shell settings get secure &lt;key&gt;
</span></span><span class="line"><span class="ln">7</span><span class="cl">adb -s &lt;serial&gt; shell pm resolve-activity -a android.settings.MANAGE_UNKNOWN_APP_SOURCES
</span></span><span class="line"><span class="ln">8</span><span class="cl">adb -s &lt;serial&gt; shell dumpsys package &lt;package&gt; <span class="p">|</span> grep -iE <span class="s2">&#34;versionCode|versionName|codePath|^Packages:|Hidden system packages:&#34;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">adb -s &lt;serial&gt; shell ls -l /storage/emulated/0/Android/data/&lt;package&gt;/</span></span></code></pre></div><h3 id="appops">appops</h3>
<p><code>appops get</code> 的兩個引數是套件名與 op 名稱，回報那個套件在那個 op 上目前的 mode。mode 是一組固定值（<code>allow</code> / <code>ignore</code> / <code>deny</code> / <code>default</code>，位置類另有 <code>foreground</code>）。<code>ignore</code> 與 <code>deny</code> 都讓呼叫拿不到值，差別在 <code>deny</code> 回報失敗、<code>ignore</code> 靜默回空。套件在那個 op 上從沒設定過 mode 時，<code>appops get</code> 印出這兩行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">No operations.
</span></span><span class="line"><span class="ln">2</span><span class="cl">Default mode: default</span></span></code></pre></div><h3 id="dumpsys-user">dumpsys user</h3>
<p>要讀的是目標使用者底下的 <code>Effective restrictions</code>。<code>grep -i restriction</code> 的輸出還包括 <code>Guest restrictions</code>（guest 使用者型別的預設清單）、<code>mDefaultRestrictions</code>、<code>Device policy global restrictions</code>，它們都不是當前使用者的有效值。判別看它落在哪個標題底下：<code>UserInfo</code> 區塊裡的屬於那個使用者，<code>Device properties</code> 或使用者型別定義裡的是型別預設。<code>Guest restrictions</code> 裡面本來就列著 <code>no_install_unknown_sources</code>——在沒有設定任何使用者限制的機器上，<code>grep</code> 的輸出照樣包含這一行，它與當前使用者的限制無關。</p>
<h3 id="dumpsys-device_policy">dumpsys device_policy</h3>
<p>device owner 管整台機器，profile owner 只管工作資料夾，兩者都能下安裝限制。排除管控層要兩個都是空的。</p>
<h3 id="settings">settings</h3>
<p><code>settings</code> 的引數依序是動作（<code>list</code> / <code>get</code> / <code>put</code>）、namespace、鍵名。namespace 分成 global / secure / system 三個，一個鍵通常只在其中一個裡面有值。跨版本搬過家的鍵會在舊 namespace 留下值為 <code>null</code> 的殘餘，<code>null</code> 因此蓋住三種情形：查錯 namespace、這台機器沒有這個設定、以及殘餘。<code>settings list &lt;ns&gt; | grep &lt;key&gt;</code> 命中不等於值在這裡——三個 namespace 都 <code>get</code> 過一輪，確認只有一個回非 <code>null</code>，才能確定值在哪。</p>
<h3 id="dumpsys-package">dumpsys package</h3>
<p>更新過的系統 App 有兩組值：出廠版在 <code>/system/app</code>、更新版在 <code>/data/app</code>。分辨靠 dumpsys 印的段標——<code>Packages:</code> 底下是目前生效的那一組、<code>Hidden system packages:</code> 底下是被蓋掉的出廠版。grep 要把段標一起收進來。</p>
<h3 id="pm-resolve-activity">pm resolve-activity</h3>
<p><code>-a</code> 後面接的是 intent action 的名稱，<code>pm resolve-activity</code> 回答系統會把這個 action 交給哪個元件處理。輸出的 <code>ActivityInfo</code> 區塊帶 <code>name=</code> 與 <code>packageName=</code>，指出 action 落在誰身上。下方巢狀的 <code>ApplicationInfo</code> 另有一組同名欄位，那是套件的 Application 類別，直接 <code>grep name=</code> 會拿到兩個不同的值。</p>
<h3 id="app-的外部儲存資料目錄">App 的外部儲存資料目錄</h3>
<p><code>ls -l</code> 後面那條路徑是套件在外部儲存上的私有資料目錄。App 把自己下載的檔案放在這裡時，這一條確認那個檔案在不在。<code>/storage/emulated/0</code> 是當前使用者的外部儲存掛載點，與前面擷圖用的 <code>/sdcard</code> 指向同一處。</p>
<h2 id="放寬安裝管控修改型指令">放寬安裝管控（修改型指令）</h2>
<p><code>REQUEST_INSTALL_PACKAGES</code> 是「這個套件可以觸發安裝其他套件」的授權。正常由使用者在設定頁逐一開啟，用指令設成 <code>allow</code> 等於跳過確認，改變的是安全設定，只適用自己擁有、可以重置的開發測試機。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 放寬（僅限自有開發測試機）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">adb -s &lt;serial&gt; shell appops <span class="nb">set</span> &lt;package&gt; REQUEST_INSTALL_PACKAGES allow
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 還原</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">adb -s &lt;serial&gt; shell appops <span class="nb">set</span> &lt;package&gt; REQUEST_INSTALL_PACKAGES default</span></span></code></pre></div><p>還原寫 <code>default</code> 而不是 <code>deny</code>：<code>default</code> 把判斷交還系統預設規則，<code>deny</code> 是主動拒絕，後續行為不同。驗證完還原，記錄改過哪一台——沒還原的機器在後續測試裡不再代表出廠狀態。</p>
<h2 id="相關指引">相關指引</h2>
<p>同一批 POS 排查的其餘指引：</p>
<ul>
<li><a href="../usb-class-code-lives-in-two-layers/">USB 類別代碼記在裝置層與介面層兩處</a></li>
<li><a href="../usb-claim-takes-the-hardware-away/">claim 一個 USB 介面等於接管它</a></li>
<li><a href="../filter-strategy-follows-asymmetric-failure-cost/">兩種錯法的代價不對稱時，過濾策略要讓失敗倒向可補救的那一邊</a></li>
<li><a href="../inventory-hardware-before-inferring-from-the-app/">陌生機器的硬體盤點比從應用端反推便宜</a></li>
</ul>
]]></content:encoded></item></channel></rss>