顯示具有 android 標籤的文章。 顯示所有文章
顯示具有 android 標籤的文章。 顯示所有文章

2021年9月26日 星期日

[Android] Android studio emulator 踩坑

markdown 老樣子,android 踩坑日常。 在 android studio 中下載了 android 10 的模擬器,將模擬器開起來時出現了 error,原本出現的 error 是 Android Emulator closed because of an internal error,如下圖 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgch0e8QV6PJuyNFlUlvP2PR_BmKBayzYjot301i039aufPZQTvDZOVVfd-4UQnOKRHlrECkXjyhwwIZ6jeDQHp7k3OUG7IDxzMsY2DmzwNfvV-ccmNcWdJnOlaQN-0TZyRvGFFSoFkRmsQ/s0/emulator_error.png) 在 [SO](https://stackoverflow.com/questions/67316340/android-emulator-closed-because-of-an-internal-error-gpu-found) 找到一篇解,在 C:\users\[user name]\.android 中新增一個檔案 advancedFeatures.ini,然後問題就變成開模擬器時跳出一個小 window 進度條然後卡住,基本上跟[這篇](https://stackoverflow.com/questions/66250313/android-emulator-not-responding-frozen)的問題一樣。 照著那篇的解去看 intel HAXM 有沒有安裝,是有安裝但是 android studio 有提示現在安裝的不是最新版,我更新到最新版,版本為 7.6.5。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi1IJlfRrn_k3w2zpmcRsGs2RhQig991ctFQnahl0JGUvufPID9gGuB6HTymtWgi_fVQ8OzQqAoFtqRuZFoC2v8MQipy92p7AcyfSrEIQknPxN6SVru9M2A0vyg1RE3mHFU3_C5Sc40BSsp/s0/haxm.png) 更新到最新版後,`C:\Users\[user_name]\AppData\Local\Android\sdk\extras\intel\Hardware_Accelerated_Execution_Manager` 目錄裡的 intelhaxm-android 檔案不見了,但是模擬器可以開起來了,所以 intel HAXM 跟模擬器也有匹配性問題,版本不對也會造成模擬器卡住開不起來。 最後,我的模擬器原本是開得起來的,不知道為什麼突然開不起來了,猜可能是因為 android studio 更新造成的。 --- * [Android Emulator not responding - frozen](https://stackoverflow.com/questions/66250313/android-emulator-not-responding-frozen) * [Android Emulator closed because of an internal error: GPU Found](https://stackoverflow.com/questions/67316340/android-emulator-closed-because-of-an-internal-error-gpu-found)

2021年6月12日 星期六

[Android] Dialog 背景 DIM 效果

markdown FAB 有個常見的效果時,顯示 FAB 菜單的時候,background 會變暗,想研究一下這個效果要怎麼達成,第一個想到的是,這個效果跟 Android 顯示 Dialog 的時候很像,就想看一下 Dialog 是怎麼做到這件事的,看能不能將 Dialog 的作法借用過來。 經過一些 study 後,發現 activity 其實會對應到一個 window(window 的意義是甚麼以後有機會再說吧),系統會將 window 關連到的 View 繪製在螢幕上,而 Dialog 會在其內部建立一個新的 window,這個 window 位置會疊在 activity 的 window 之上(造成 Dialog 的懸浮效果),Dialog window 的 background 是透明的,並且將這個 window 的 dimAmount 設置為 0.6 來達成背景變暗的效果。 這個方法是蠻方便的,不過不適合用在 FAB 菜單顯示,總覺得不需要再建一個 window,而且還要處理 window 間的事件傳遞,也比較麻煩吧。 --- * [關於給Dialog設置setCanceledOnTouchOutside(true)後如何監聽Dialog消失](https://blog.csdn.net/qq_16247851/article/details/72794409)

2021年5月29日 星期六

[Android] SharedPreferences string set 的坑

markdown 之前曾經遇過一個問題就是用 SharedPreferences 儲存 string set,我已經呼叫完 commit,但是 app 關掉重開之後,卻發現 string set 卻沒有寫入到,當時一直沒搞清楚是甚麼問題,最近偶然看到原來用 string set 有這樣一個坑,值得在這裡紀錄一下。 當時一直在想是 putStringSet 出了甚麼問題,但問題是出在 getStringSet return 的 string set,不能直接將資料加到這個 string set 傳給 putStringSet,因為 putStringSet 會比較傳進來的 string set 與 getStringSet return 的是不是同一個 object,如果是則不做任何動作。 這可能是為了效能考量,但其實挺違反直覺的,不注意真的很容易踩坑。 --- * [Misbehavior when trying to store a string set using SharedPreferences](https://stackoverflow.com/questions/14034803/misbehavior-when-trying-to-store-a-string-set-using-sharedpreferences/14034804#14034804) * [Android: String set preference is not persistent](https://stackoverflow.com/questions/16820252/android-string-set-preference-is-not-persistent) * [What's the difference between commit() and apply() in SharedPreferences](https://stackoverflow.com/questions/5960678/whats-the-difference-between-commit-and-apply-in-sharedpreferences)

2020年12月22日 星期二

[Android] Google 取消與西聯匯款的合作

markdown Google 廣告收入要取消跟西聯匯款的合作了,到今年底為止還可以用西聯匯款領取,明年就不行了,所以最近就來做第一次跟最後一次的西聯匯款領取廣告收入。 實際試著領取,基本上步驟跟網路上講的大同小異,只要有 Google 匯款記錄的 MTCN 碼,在京城銀行的網銀點一點,匯款就直接即時入到你的帳戶了,超級方便。 西聯匯款不能用後,現存能用的方法就是用電匯,但是電匯手續好像比較麻煩,而且還要付給銀行比較多的手續費,這個就之後等匯款累積到一定量要領取時再來研究了。 --- * [Google廣告收入的銀行電匯費用比較](https://max-everyday.com/2020/08/google-adsense-payment-bank-fee/) * [Google Adsense 停止西聯匯款(Western Union)付款服務!改支票或電匯](https://www.vedfolnir.com/google-adsense-%E5%81%9C%E6%AD%A2%E8%A5%BF%E8%81%AF%E5%8C%AF%E6%AC%BE%EF%BC%88western-union%EF%BC%89%E4%BB%98%E6%AC%BE%E6%9C%8D%E5%8B%99%EF%BC%81%E6%94%B9%E6%94%AF%E7%A5%A8%E6%88%96%E9%9B%BB%E5%8C%AF-41163.html) * [Google Admob透過HSBC Direct電匯方式請款](https://noter.tw/2256/google-admob%E9%80%8F%E9%81%8Ehsbc-direct%E9%9B%BB%E5%8C%AF%E6%96%B9%E5%BC%8F%E8%AB%8B%E6%AC%BE/) * [Google Adesnse請款以電匯方式取款心得](https://kk665403.pixnet.net/blog/post/405026836-googleadsense-wiretransfer) * [Google停止西聯匯款支付款項一定要知道的事](https://newguest88.pixnet.net/blog/post/350915383-google-adsense%E6%94%B6%E6%AC%BE%E6%96%B9%E5%BC%8F%E6%87%B6%E4%BA%BA%E5%8C%85-%E8%A8%AD%E5%AE%9A%E6%95%99%E5%AD%B8%E6%94%BB%E7%95%A5-%E6%94%B9) * [Google Adsense廣告費 「電匯收款」設定流程及電匯手續費說明(Youtube及Blog皆適用)](https://goodideamin.com.tw/blog/post/googleadsensewiretransfer)

2020年11月12日 星期四

[Android] Android 8.0 APP adaptive icon 圖標設計

markdown Android 8.0 推出了 adaptive icon 格式,希望能讓手機廠商可以自訂 APP icon 圖型,讓整體畫面看起來保持一致性。 adaptive icon 的設計其實很簡單,就是用 background 跟 foreground 構成圖案,手機廠商再自己自訂遮罩讓手機上的圖案都保持一樣的形狀。看下面的示意圖就很清楚了。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhZfC8cCNypUU5Ekv7tYrECbdlT-l5DjbSIVbbkCui00Iil1WwT0ySK_CUHu4nae3DR9b9iyom4QxxxVPrRhFk2jqvKGf1CUon9Smf1xc5STFjRjHcBIKzAVKCj4WVcBzfhJkXLj6Te2XXV/s1600/NB_Icon_Layers_3D_03_ext.gif) 開發者要做的,就是準備好 foreground 跟 background,foreground 格式可以是 SVG 的 xml 檔,或是用圖片也可以。background 也是一樣,甚至可以更簡單單純指定一個色號,通常就能滿足大部分 icon 需求。 不過要做出一個不帶背景的 foreground 並不是那麼簡單,現在網上的 icon generation tool 大多都是會自帶純色背景,若要做 adaptive icon,可能要自己學一下設計軟體做出不帶背景的圖片了。 --- * [application中 android:icon 和 android:roundIcon 的区别](https://blog.csdn.net/CheacK66/article/details/81541632)

2020年10月29日 星期四

[Android] Activity Layout 層級與 DecorView

markdown 我們為 activity 編寫的 layout 層級都在 android.id.content 這個 Android 內建的 layout id 之下,那 android.id.content 是不是 Activity 的 頂層 view 了呢?其實並不是的,它只是我們編寫的 layout 的 parent view 而已,當我們呼叫 [setContentView](https://developer.android.com/reference/android/app/Activity#setContentView(int)) 的時候,API 內部就將傳入的參數 layout id 放在 android.id.content 之下。 實際上 activity 的頂層 view 是一個叫 Decorview 的 FrameLayout,可以通過 API 取得。
protected void onCreate(Bundle savedInstanceState) {
    View decorView = getWindoe().getDecorView();
}
Decorview 會在 Activity initialize 的時候被創建出來,不過它也不是被 Activity 直接持有,而是 Activity 的 Window 成員持有 Decorview,看下面這張圖會比較清楚它們之間的關係。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjjhrQ4YxP_wJRxZjtCCW_pFg2Mxmap7vF2SvD9p5_f29SDRjMzbWkFWAzX5HPJ5fvPwLdjhM4wS0CjoKkKit717nkn8Gfm2jJy3gwlHpIiSKAP87XFkj0kDw2RIEWb6BKDZMF6bTyWM5V5/s0/1.png) 雖然近來 Android 不斷提倡將 layout 層級扁平化以改善效能,但是你可以看到,就算我們寫的 layout 沒有任何嵌套,它也已經是第三層了,上面還有 android.id.content 和 Decorview 呢。 關於減少 layout 嵌套層級,在這裡順便提到使用 Fragment 時的一個小技巧,網路上很多範例,會在 activity layout xml 中放一個 FrameLayout 當成 fragment container。
FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
然後在 activity 中將 fragment 放入 container。
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;

public class MainActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState?) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);

        HeadlinesFragment firstFragment = new HeadlinesFragment();
        // Add the fragment to the 'fragment_container' FrameLayout
        getSupportFragmentManager().beginTransaction()
                .add(R.id.fragment_container, firstFragment).commit();
    }
}
其實並不需要自己再建立一個 FrameLayout 當作 container,android.id.content 已經是一個 FrameLayout,直接將 fragment 放到 android.id.content 就好,這樣可以減少一層 layout 嵌套。
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;

public class MainActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState?) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);

        HeadlinesFragment firstFragment = new HeadlinesFragment();
        // Add the fragment to the 'fragment_container' FrameLayout
        getSupportFragmentManager().beginTransaction()
                .add(android.id.content, firstFragment).commit();
    }
}
--- * [Android: Full Screen UI with Transparent Status Bar](https://proandroiddev.com/android-full-screen-ui-with-transparent-status-bar-ef52f3adde63) * [Activity 原理剖析 ](https://juejin.im/post/6844903978057072648) * [android.R.id.content as container for Fragment](https://stackoverflow.com/questions/24712227/android-r-id-content-as-container-for-fragment)

2020年9月27日 星期日

[Android] 動畫簡介

markdown 簡單介紹一下 Android 上面的動畫,主要分為兩類,第一種是 Android 一開始提供的 [View Animation](https://developer.android.com/guide/topics/resources/animation-resource#View),第二種是 Android 3.0 之後提供的 [Property Animation](https://developer.android.com/guide/topics/resources/animation-resource#Property)。 ## View Animation View Animation 又細分為兩種,Tween animation 跟 Frame animation。 大多數情況我們會使用 Tween animation,這種動畫可以用來做旋轉、淡入淡出、移動以及縮放,而且可以定義在 xml 中。 Frame animation 就是通過顯示一張張圖片而成的動畫,就像播放影片一樣,也可以定義再 xml 中,不過這種方式使用情境較少。 View animation 的 class 定義在 [android.view.animation](https://developer.android.com/reference/android/view/animation/package-summary) package 中,主要的 class 有 * [AlphaAnimation](https://developer.android.com/reference/android/view/animation/AlphaAnimation) * [AnimationSet](https://developer.android.com/reference/android/view/animation/AnimationSet) * [RotateAnimation](https://developer.android.com/reference/android/view/animation/RotateAnimation) * [ScaleAnimation](https://developer.android.com/reference/android/view/animation/ScaleAnimation) * [TranslateAnimation](https://developer.android.com/reference/android/view/animation/TranslateAnimation) 那既然 Android 已經支持了這些動畫功能,為什麼要在之後引入 Property Animation 呢?一個是因為 View Animation 如同它的名字一樣,只能作用在 View 上面,假如我們有一個 custom view,而它的繪製是基於某個物件,我們希望實現的動畫是去操作這個物件讓 custom view 重繪,這時 View Animation 就派不上用場。 另外就是 View Animation 的動畫顯示的改變沒有實際作用到 View 上面,例如 View Animation 的動畫把一個左側的 Button 平滑移動到右側,此時你去點擊右側的 Button,是沒有作用的,Button 實際上還在左側,只是在畫面上將它繪製在右側而已。 當然可能還有其它各種 View Animation 不好發揮用處的場景,於是後來 Android 推出了 Property Animation。 ## Property Animation Property Animation 如同名字一樣,在動畫中做的改變會實際影響到 View 的 property 例如位置,大小等等。 核心的 class 有 * [ObjectAnimator](https://developer.android.com/reference/android/animation/ObjectAnimator) * [ValueAnimator](https://developer.android.com/reference/android/animation/ValueAnimator) 可以看到,跟 View animation 相比,Property Animation 的 class 是以 animator 結尾,這可以讓人比較好一眼就分辨出現在在使用的是 View Animation 還是 Property Animation。 ValueAnimator 的使用,下面有一個簡單範例。
ValueAnimator anim = ValueAnimator.ofInt(1, 10);
anim.setDuration(1000);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        int v = (int) animation.getAnimatedValue();
        // Redraw the view according to the value...
    }
});
anim.start();
ValueAnimator 的最簡單用法,就是讓你指定一個的初始值跟結束值,還有時間長度,然後當 Animator 開始後,就會不斷呼叫你實作的 listener,讓你可以重繪 View 來達成動畫的效果。 不過 ValueAnimator 還要自己手刻重繪製的動作,有點麻煩,另一個 ObjectAnimator 可以更簡單的實現動畫效果,看下面的範例。
ObjectAnimator animator = ObjectAnimator.ofInt(button, "width", 200, 300);
animator.setDuration(1000);
animator.start();
ObjectAnimator 是繼承 ValueAnimator 的,所以像 ofInt 跟 ofFloat 這類的 函式,ObjectAnimator 也是有的,但跟 ValueAnimator 不一樣的是,ObjectAnimator 可以讓你傳入任意的 View 還有你想操作的 View 屬性, 讓 ObjectAnimator 自動幫你設定屬性。例如上面的範例,呼叫 start 後,就會看到為時 1 秒的動畫顯示 button 的 width 從 200 變成 300。 這些需要提一下的是,ObjectAnimator 不是去看 button 物件有沒有 width 這個屬性,而是去看 button 有沒有 setWidth、getWidth 方法,所以,只要你想操作的值在物件中有對應的 set、get 方法,就都可以使用 ObjectAnimator。 這些 ValueAnimator 跟 ObjectAnimator 的基本用法,可以了解 Property Animation 的概念,也可以實現很多簡單的動畫了,進階的用法,就等需要用到時再去研究。 --- * [属性动画 ValueAnimator 运行原理全解析](https://www.jianshu.com/p/46f48f1b98a9) * [Android属性动画完全解析(上),初识属性动画的基本用法](https://blog.csdn.net/sinyu890807/article/details/43536355)

2020年6月25日 星期四

[Android] RecyclerView load 耗時造成動畫丟幀

markdown 又是 RecyclerView 造成的問題分析,這次問題的情境是,在進入一個 fragment 時會帶有一些動畫效果同時載入一個 RecyclerView,但是動畫會發生 junk。 因為這個 fragment 中一些比較 heavy 的 IO 操作都被移到 background thread 中去做了,所以一開始真的想不到是甚麼原因造成的,code review 了很久始終沒有頭緒,只好又請出 systrace 這神器來檢查。然後才發現,原來兇手是 RecyclerView。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiy8qqyLsUxRX1pdMilL3aV2ccGN4FUPZg4wtYPtQffP-rDtXKykiheyn81jgzy9smuq6_c6-iFv5BfDvJ8yBvuHAApSud86qwi5tHzwscLqi5i_KBENjNi0uypMMKOYqmr1Ll80SXkJShu/s1600/Image+1.png =900x*) 一查才知道,原來 RecyclerView Load 花了這麼久的時間,圖中顯示一目了然,左上方紅色的 F 表示這個 Frame 被 delay 了,就是被 RecyclerView 拖累到,所以動畫會有 lag 的感覺。 後來想辦法要縮減 RecyclerView 的載入時間,想說最花時間的應該是 layout inflate,就把 inflate 改成在 background thread 去做,甚至用 custom view 就為了減少 view measure 時間,但是雖然症狀有減輕,但是還是會有 lag 的感覺,達不到預想中滑順的動畫效果。 隨著查了越來越多資料,發現動畫顯示實際上是一個非常嚴苛的條件,想一下,動畫中每個 frame 每 16ms 要顯示一次,你的 RecyclerView 載入要壓在 16ms 之內,而即使已經用了上述技巧減少每個 item load 的時間,但有很多時間是花在 RecyclerView 內部的運作,這時你再怎麼減少 item 的 onCreateViewHolder 跟 onBindViewHolder 也無濟於事。 最後面用了一個折衷的辦法,先讓動畫執行完,再來做載入 RecyclerView,這樣可以保證動畫流暢的執行完。當然,這是因為目前對 RecyclerView 還不夠了解,若是之後對 RecyclerView 了解的足夠深入,有能力改寫它裡面對 item 的載入流程,也許是可以將載入壓到 16ms 的。 --- * [The battle for jank*-less UI on Android](https://medium.com/swlh/the-battle-for-junk-less-ui-713d7680aebc) * [Developing for Android, III: The Rules: Performance](https://medium.com/google-developers/developing-for-android-iii-2efc140167fd)

2020年5月3日 星期日

[Android] system tracing

markdown systrace 是一個用來量測 Android 系統層級執行時間的 command line tool,位置在 android-sdk/platform-tools/systrace/systrace.py,是一個 python script。它的一些參數可以看[官方文件](https://developer.android.com/topic/performance/tracing/command-line)的說明。 在講 systrace 的用法之前,先來講一下為什麼它可以收集 Android 系統的執行時間,Android 本身會在很多重要的函數前後插入 Label,用這個資訊讓 systrace 可以知道某個函數執行了多久。所謂插入 Label,其實就是呼叫 [TraceCompat](https://developer.android.com/reference/android/support/v4/os/TraceCompat) 的 beginSection 和 endSection。例如在 RecyclerView 的 onLayout 就有插入 Label。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEibRjV9ccicC5xhih9wBTdxMnDKqq73mg38BG6AOVv5Mrlc6bE4JCggMTFcgaCnHfVl8JMiVpUWt_grolqxF3dnXxctDSb3isDC12L-ZNN4gBzsD1ChuN2bwz-p5A27z1Oe-586ff037nw3/s1600/Image+2.png) 這樣的 Label 被插入到 Android 系統的各個關鍵 call path 中,讓你可以知道你的 app 在這些系統函數到底花了多久時間。 執行 systrace 時會喂給它時間參數,表示你希望它收集多久的資料,然後是各個模組名稱,表示你希望它收集哪些模組的資料。還有你自己 app 的 package name,這樣 systrace 也會抓你自己在 app code 中插入的 Label,例如下面是我用來 debug UI 卡頓時常用的命令。
systrace.py -t 3 sched gfx view -a myapp.package.name --no-compress
systrace 執行完之後,就會在目錄中產生一個 trace.html 檔案,這個檔案可以用 chrome 開啟,用圖形化的界面顯示執行時間,如下圖,你可以用 WASD 按鍵做移動還有放大跟縮小 timeline。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhnOkhF7X1mkYZ5Cr5Qw8UxuPo3cp6d4PT48hyphenhyphenz9odjEj7penLMDnCukpVxoAJgYG182-Y03XD7OlUu5ExZfdivVGJz-6rlyC94E2fVkFyKVZepgiiZId_fJQ4kxJczHO4KVF4_Rul5Wv0u/s1600/Image+1.png) 跟 method trace 不同,systrace 只記錄某些關鍵 call path 的執行時間,不過這也正好是他的優點,因為 method trace 記錄的 method 太多,除非有某個函數真的明顯跟其他函數比起來花了太多時間,不然在調查效能瓶頸時很容易被淹沒在函數海中,找不到該優化的地方。用 systrace 可以先從大方向找出在哪些系統函式比較耗時,接著再用 method trace 做深入追查,這樣會比較有效率。 --- * [Overview of system tracing](https://developer.android.com/topic/performance/tracing/)

2020年3月21日 星期六

[Android] 一次 RecyclerView 效能分析

markdown 最近遇到了一點效能問題,所以開始研究了一下要如何增進 app 效能,那要改善效能,首先要知道 app 的 bottleneck 到底是甚麼,最經典的方法就是用 profiler 看一下效能都花到哪去了。 圖形化的 profiler 最方便直觀,早期都是推薦用 [Traceview](https://developer.android.com/studio/profile/traceview),但是現在已經被 Google 列為不建議使用的工具,因為 Android Studio 在 3.0 之後推出了 [Android Profiler](https://developer.android.com/studio/profile/android-profiler) 可以讓你來觀察 CPU 與 memory 的使用量,因為我是遇到畫面 lag 的問題,就用裡面的 CPU profiler 研究了一下看 CPU 花在哪個 method 里。 在用 profiler 之前之前,其實我預期會看到的是 [setText](https://developer.android.com/reference/android/widget/TextView#setText(java.lang.CharSequence)) 這個 method 吃掉了 CPU resource,因為我有一個 RecyclerView 會頻繁刷新並且呼叫 setText。而之前曾經在 RecyclerView 最佳化的文章中看到過 setText 是很耗時的。 的確在每次畫面刷新的時候就看到 CPU 瞬間飆起來一下,但是從 Call Chart 看到了一堆 method,但都是 Android 系統的函式,看不到 setText 的耗時,後來自己在程式里計算 setText 的時間,發現才不到 1ms。耗時這麼少,被淹沒在 call stack 茫茫大海中,難怪 Call Chart 中找不到,而且也不會是 lag 的原因。後來把 TextView 的 layout class name 印出來看,發現使用了 BoringLayout,所以 setText 的耗時非常短。 後來無意中發現,當我呼叫 notifyDataSetChanged 時,onCreateViewHolder 被呼叫了很多次,這跟我原本對 RecyclerView 的認知有出入,在我原本認知下,應該只有 onBindViewHolder 會頻繁呼叫到才對,然後看了原始碼才發現,原來 RecyclerView 預設 view pool 只會暫存 5 個 item。
public static class RecycledViewPool {
    private ArrayList<ViewHolder>[] mScrap;
    private int[] mMaxScrap;
    private static final int DEFAULT_MAX_SCRAP = 5;
    // Ignore......
}
我的 list item 有 30 幾個,所以會頻繁的呼叫到 onCreateViewHolder。 看來我的 RecyclerView 有一些不必要的耗時操作,接下來就是要對 RecyclerView 作一些優化了,首先記錄一下沒優化的前的 CPU 使用率,如下圖,可以看到稍微有點高了。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhlJ-h1DXYl6FIXK2bSB4q4tz1xntgF4yzjKwvDcqh1w3nyKx_tBEFZcm2pCS2SDLkoC_ddH8y8QSOiQuh8y4ZxcJtQoJn7ByOGGXAGCL5mc9v9MhzpyhtAn4Hn0vyjDAdSzk440dLyiMez/s1600/no_optimize_real.png) create view 是蠻耗時的操作,將 view pool 的 cache 數量設成 36 之後 觀察 CPU 使用率如下。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgJ12RtxDTTUC-Cjs6MRoPxff9B_4XdBmvaexscZxfwXXsEmkeyG68OlUKASqQO89k0rfzxCOaQY9kLOpeppj-d_tTFFuo6TY2u6sjfRU7Bu5He1nZ8olvp3R6evHdpI9aRL3lUsPvFlq_/s1600/view_pool_real.png) 之前 RecyclerView 最佳化的文章中還有看到 DiffUtil 這個工具類,可以進一步減少 CPU 耗時,使用後 CPU 使用率如下。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiF-ZxJ9MgLQBDdey5NBxGHHsf7vtanX9f_TEdR1Fk5B_gnBIE3Ge17cED7yzqPmHaTcxdPLQEP57etV92sLpUIowdHrRZS7V8yc5oTMHsGeJGZW-TaesXX9AQQIxA69ULTFOAx304eF4_f/s1600/diffutil.png) 從圖中可以看到,CPU 使用率降低了,優化是有效果的,之後使用 RecyclerView 時可以考慮一下用這些優化方法。 --- * [Android - 性能优化方案分享](https://cloud.tencent.com/developer/article/1415758) * [Android性能优化之CPU Profiler](https://juejin.im/entry/5c0daf65f265da6150644a1d)

2020年3月15日 星期日

[Android] RecyclerView 頻繁刷新效能最佳化

markdown 有頻繁刷新 RecyclerView 的需求時,有時候會產生一些性能問題,那 RecyclerView 最佳化的方向有幾個,大概整理如下,供以後需要時備查。 ### DiffUtil [DiffUtil](https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/DiffUtil) 是 Google 開發出來跟 RecyclerView 搭配使用的一個工具類,用途是計算出新舊 list item 中有差異的項目,以求最小化更新 list 節省 CPU 時間。 使用方法非常簡單,只需要實做 [DiffUtil.Callback](https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/DiffUtil.Callback) 介面,剩下找出 list 差異項目的工作就交給 DiffUtil 實現,套用這個最佳化方法 effort 很小。 但是要注意的是,若是 list 很大而且內部資料幾乎每次全部都會變動的話則不適合使用 DiffUtil,反而會造成耗時增加,還是要看一下實際場景,再決定要不要使用。 ### StaticLayout 若是 list 中有 TextView 的話,可以看一下是不是花了很多時間在 setText method 上,因為在套用 DynamicLayout 的時候 setText 是很耗時的,此時可以參考 Instagram 的最佳化 TextView 文章,使用 StaticLayout 來節省 CPU 時間。 這個最佳化方法比較複雜,所以在決定套用前,要先確定 setText 是不是效能瓶頸,TextView 也有可能自己就選擇使用 BoringLayout 或 StaticLayout,要看實際場景以及 TextView 內部演算法決定。 ### View Pool RecyclerView 預設 View Pool 的數量是 5,也就是超過 5 個 list item 之後,使用 notifyDataSetChanged 更新 list 會常常呼叫到 onCreateViewHolder,因為 inflate view 也算是有點耗時的操作,所以可以視需求決定要不要增加 view pool 容量,以減少 onCreateViewHolder 呼叫。 --- * [RecyclerView 配合 DiffUtil,好用到飞起](https://juejin.im/entry/5996977e518825242860f251) * [RecycleView性能优化](https://github.com/ChenSiLiang/android-toy/blob/master/RecycleView%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%EF%BC%88%E6%8C%81%E7%BB%AD%E6%9B%B4%E6%96%B0%E4%B8%AD%EF%BC%89/RecycleView%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.md) * [TextView性能瓶颈,渲染优化,以及StaticLayout的一些用处](https://www.jianshu.com/p/9f7f9213bff8) * [Instagram是如何提升TextView渲染性能的 ](http://codethink.me/2015/04/23/improving-comment-rendering-on-android/) * [Text rendering on Android](https://medium.com/@Cuong.Le/text-rendering-on-android-9a27fc59c8a6) * [StaticLayout 源码分析](https://jaeger.itscoder.com/android/2016/08/05/staticlayout-source-analyse.html) * [RecyclerView item optimizations](https://medium.com/@programmerr47/recyclerview-item-optimizations-cae1aed0c321) * [PrecomputedText New API in Android Pie](https://medium.com/mindorks/precomputedtext-new-api-in-android-pie-74eb8f420ee6)

[Android] 從紅米 note5 中取得 MIUI 原始碼

markdown 網路上有很多 android 反編譯的教學文章,教你怎麼反編譯 android 手機的 rom,不過手機廠的 rom 檔案格式日新月異,反編譯工具未必能跟得上 rom 格式改版的速度,像我剛買紅米 note 5 遇到 app 開發問題時,想反編譯 note 5 的 rom 來看,就遇到問題卡關了很久。 後來想到了新的思路,如果能直接從手機裡抓 binary 出來,不是就不用解包 rom 了?後來進手機裡看了一下,真的有我要找的檔案。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg_1aSy-8Q1k8LrOGXUPnn0I2CRe6wOJtMMrgXbjoqq_5Y4CfaqZNhOSrEftLLIWiZs9wM6C_CkogYfbrCTAjU8c8yxxNy2zmZtZiHQ6Z4on8y91HDyVYmgUmryIKIlmBaSp9POv9rf2i0t/s1600/Image+1.png) 後來將這包 jar 檔反編譯,也順利找到了問題點,是紅米對 Android 客製化的機制造成 app 的問題(Android 碎片化,又是另一個頭痛的議題)。 其實系統裡面有些部分不一定要反編譯 rom 才能得到,像我是想要看的 service.jar 裡面的 code,因為這包其實有在紅米手機裡,直接從手機裡抓出來就好了。既方便不用解包 rom,又能確保反編譯出來的一定是手機上在運行的 code。若是要查看系統 app 的 code,因為無法從手機直接抓系統 app 的 apk 出來使用,這時就再去抓 rom 來反編譯就好。 --- * [將.apk 和.odex 合併的簡單記錄](https://www.icka.org/1426/how-to-install-apk-with-odex-file) * [android-反編譯工具教學-dex2jar 和jd-gui](https://zpspu.pixnet.net/blog/post/330274885-android-%E5%8F%8D%E7%B7%A8%E8%AD%AF%E5%B7%A5%E5%85%B7%E6%95%99%E5%AD%B8-dex2jar-%E5%92%8Cjd-gui) * [人人都會的 apk 反編譯](http://huli.logdown.com/posts/661513-android-apk-decompile) * [去你妹的廠商改固件,看我逆向小米rom層應用做碎片化適配](https://www.jianshu.com/p/6f313b4876ab)

2020年3月13日 星期五

[Android] Fragment transition 動畫在 onAnimationEnd 呼叫時動畫並未真正結束

markdown 最近遇到的一個情況是,我在 fragment 進入動畫結束後要初始化一個 RecyclerView,在 fragment 的 onCreateAnimation 中用將 animation listener 監聽 animation end 事件,並且在該事件 callback onAnimationEnd 中進行初始化 list。
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
    Animation animation = super.onCreateAnimation(transit, enter, nextAnim);
    if (!enter)
        return animation;

    if (animation == null)
        animation = AnimationUtils.loadAnimation(getActivity(), nextAnim);

    animation.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {}

        @Override
        public void onAnimationEnd(Animation animation) {
            initialListView();
        }

        @Override
        public void onAnimationRepeat(Animation animation) {}
    });
    return animation;
}
但是發現了一個問題,就是在進入動畫執行完成前,list 就被更新了,而且因為初始化 list 是一個稍微有點耗時的動作,於是就看到進入 fragment 時動畫在快完成時頓住一下,然後動畫才完成並且 list 顯示出來。 因為網路上找不太到答案,就自己看一下 Android 原始碼,發現原來在 onAnimationEnd 呼叫後,確實還會執行最後一幀動畫。下面是 Android Animation class 中的 getTransformation method。
public boolean getTransformation(long currentTime, Transformation outTransformation) {
    // Ignore unrelated code......

    final boolean expired = normalizedTime >= 1.0f || isCanceled();
    mMore = !expired;
    if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
    if ((normalizedTime >= 0.0f || mFillBefore) && 
        (normalizedTime <= 1.0f || mFillAfter)) {
        if (!mStarted) {
            fireAnimationStart();
            mStarted = true;
            if (NoImagePreloadHolder.USE_CLOSEGUARD) {
                guard.open("cancel or detach or getTransformation");
            }
        }
        if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
        if (mCycleFlip) {
            normalizedTime = 1.0f - normalizedTime;
        }
        final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
        applyTransformation(interpolatedTime, outTransformation);
    }
    if (expired) {
        if (mRepeatCount == mRepeated || isCanceled()) {
            if (!mEnded) {
                mEnded = true;
                guard.close();
                // Invoke onAnimationEnd
                fireAnimationEnd();
            }
        } else {
            if (mRepeatCount > 0) {
                mRepeated++;
            }
            if (mRepeatMode == REVERSE) {
                mCycleFlip = !mCycleFlip;
            }
            mStartTime = -1;
            mMore = true;
            fireAnimationRepeat();
        }
    }
    // If mOneMoreTime is true, return true
    if (!mMore && mOneMoreTime) {
        mOneMoreTime = false;
        return true;
    }
    return mMore;
}
從程式碼中可以看到,在呼叫完 onAnimationEnd 後,會去檢查 mOneMoreTime 欄位,如果該欄位是 true,則 return true 告知 caller 動畫還未結束,然後執行最後一幀動畫。為了確認 mOneMoreTime 在 onAnimationEnd 呼叫時是不是 true,我還用反射去讀取這個欄位印出來驗證,結果證實了 mOneMoreTime 的確是 true。
@Override
public void onAnimationEnd(Animation animation) {
    try {
        Field field = animation.getClass().getSuperclass().getDeclaredField("mOneMoreTime");
        field.setAccessible(true);
        boolean b = field.getBoolean(animation);
        mLogger.logd("mOneMoreTime: %b", b);
    } catch (NoSuchFieldException e) {
        mLogger.loge("NoSuchFieldException Get field fail: %s", e.getMessage());
    } catch (IllegalAccessException e) {
        mLogger.loge("IllegalAccessException Get field fail: %s", e.getMessage());
    }

    initialListView();
}
雖然不知道這是 Android 故意這樣設計的還是 Bug,但是從原始碼中可以確認,這個問題跟 app 設計無關,而是系統造成的。也只能找一些 workaround 去繞過他了。 --- * [Android Animation 执行原理](https://juejin.im/post/5acb95b16fb9a028dc4152ba) * [Android Animations Tutorial 7: The secret of fillBefore, fillAfter and fillEnabled](http://graphics-geek.blogspot.com/2011/08/mysterious-behavior-of-fillbefore.html)

2020年3月10日 星期二

[Admob] 在 Admob 中啟用 app-ads.txt 檔案

markdown 之前收到 Google 的通知說我的 app-ads.txt 沒有啟用,因為沒聽過這個東西,所以大概查了一下是甚麼。簡單說其實這就是一個廣告商認證機制所需要的一個檔案,只有通過認證的廣告商可以使用你的廣告空間,最大好處是可以避免廣告詐欺,從而保住你的廣告收入。 要設定這個東西其實很簡單,app-ads.txt 的內容只有一行,照著 admob 的提示很容易就做好,麻煩的地方是他要求這個檔案要放到某個網域的根目錄下。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgURnVsVCMiEuqXb7LNyJeDtN6wlI06E-EljNNciU1cuWA9qhoRDB5hkN9jBH010d_GGLeSHN92d7qEpK08xct2md2KqDJarZSFIsWS3OCJbLS_ARpzslmSNM-tBP4A-rIsNlFJ9xqdmcUf/s1600/Image+2.png) 一般的開發者應該都不會有網域的根目錄控制權,除非是跟服務商買網路空間。那時找了幾個方法,例如網路上有免費服務 app-ads-txt.com 專門做出來讓你放 app-ads.txt,但是看到[有人說](https://markappdesign.blogspot.com/2019/09/admib-app-adstxt.html)這個服務會直接竄改你的 app-ads.txt,因此不可使用。[reddit](https://www.reddit.com/r/androiddev/comments/cp5n2m/the_developerfriendly_guide_to_appsadstxt_admobs/) 也有一篇文整理了很多方法,那時在那篇文最可行的方法是將 app-ads.txt 放到 github 然後用轉址服務 [dot.tk](http://www.dot.tk) 就可以做出檔案放在根目錄的效果。 但是我覺得用轉址的方法,不太穩定又麻煩,因此還是沒有這樣用,停了一陣子之後再上網找找,就發現 reddit 那篇文更新了,有人用 github.io 來放 app-ads.txt,看了一下,這應該就是最佳解了。將 app-ads.txt 放到我的 github.io 後,過了一天,admob 就顯示 app-ads.txt 成功啟用了。 --- * [Admob的 app-ads.txt終於成功動作了](https://markappdesign.blogspot.com/2019/09/admib-app-adstxt.html) * [The developer-friendly guide to 'apps-ads.txt' (Admob's recent e-mail to app developers)](https://www.reddit.com/r/androiddev/comments/cp5n2m/the_developerfriendly_guide_to_appsadstxt_admobs/)

2020年2月16日 星期日

[Android] 移除 DialogFragment 上方空白

markdown 在使用 DialogFragment 的時候會發現上方有一個空白的區域,這是預留給 fragment title 的,若是 DialogFragment 不需要有 title 的話,title 部分空白會讓畫面看起來怪怪的。例如在 [SO](https://stackoverflow.com/questions/28528121/remove-white-background-in-dialogfragment) 上看到的範例圖。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi52qzzySoZL1Wr3dLZw-Os5_0C-m17KJ3PDHN_17yh3l8sehGE_bqZJd4eXMpvw0uaoz3CxI2d4A_0rRlIh0y60cEGkFMrsbwbh2n8NrY8hUeQCLkGWJktlHWJnY_zuqR0gFEfYTM3Omcj/s1600/gfo3j.png) 在網路上有看到兩個方法可以移除 title 留白區域。 1. Window.FEATURE\_NO\_TITLE 2. STYLE\_NO\_TITLE 不知為什麼 Window.FEATURE\_NO\_TITLE 我試過但沒有用,不過用 STYLE\_NO\_TITLE 確實可以消除 title 留白,如下 sample code。
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setStyle(DialogFragment.STYLE_NO_TITLE, 0);
}

2020年2月10日 星期一

[Android] 螢幕旋轉時 child fragment 和 parent fragment onCreate 執行順序

markdown 最近遇到問題的場景是這樣的,在某個 parent fragment 中用 ViewPager 包含了兩個 child fragment,在 parent fragment 中有儲存了一些變數供給 child fragment 使用,這些變數在 parent fragment 中的 onCreate 初始化,在 child fragment 的 onCreate 中使用。 我預設 parent fragment 的 onCreate 會在 child fragment 的 onCreate 之前執行,這很合理阿,畢竟我們總是先載入 parent fragment 不是嗎? 原本這段程式碼運作的很好,直到有一天測試螢幕轉向,child fragment 的 onCreate 中拋出了 NullPointerException,顯示 parent fragment 中的變數並未初始化,該變數是 null。我發現當螢幕轉向時,child fragment 的 onCreate 會在 parent fragment 的 onCreate 之前執行,導致了這個 exception,這與我先前的認知相反,讓我很驚訝。 查了資料後才發現,原因是當螢幕轉向時,fragment manager 會重新 create 它管理的 fragment,而重新 create 的時間點就是在 [Fragment class](https://developer.android.com/jetpack/androidx/releases/fragment) 的 onCreate 裡面。因此,在我的 parent fragment onCreate 裡面呼叫 super.onCreate 的時候,child fragment 就在這時被 create 出來,並且呼叫了 child fragment 的 onCreate。 仔細想想,這個行為也不是沒有道理,如果 fragment manager 不在 Fragment class 的 onCreate 中去重建 child fragment,它還能在哪個時間點做這件事呢?但是我懷疑有多少人在一開始就查覺到這件事,而不是像我一樣遇到 crash 了才發現。 在查詢 fragment lifecycle 資料時,也看到 [square](https://developer.squareup.com/blog/advocating-against-android-fragments/) 有文章對 fragment 相當感冒,表示應該也有很多人對 fragment lifecycle 的複雜度覺得很難搞吧。 --- Reference: * [After the rotate, onCreate() Fragment is called before onCreate() FragmentActivity](https://stackoverflow.com/questions/14093438/after-the-rotate-oncreate-fragment-is-called-before-oncreate-fragmentactivi) * [Advocating Against Android Fragments](https://developer.squareup.com/blog/advocating-against-android-fragments/) * [Flow和Mortar的调查](https://github.com/hehonghui/android-tech-frontier/tree/master/androidweekly/Square%20%E5%BC%80%E6%BA%90%E5%BA%93Flow%E5%92%8CMortar%E7%9A%84%E4%BB%8B%E7%BB%8D)

2019年12月31日 星期二

[AdMob] 收取廣告收益方式

markdown 最近在 AdMob 上累積的收益達到付費標準 100 美金了,於是開始準備領取這筆費用,在這邊記錄一下需要做的設定還有開戶過程,基本上在 AdMob 收益達到 10 美金時,Google 就會用郵件寄送 PIN 碼到你家,在 AdMob 中將 PIN 碼填入,就可以開始設定付款方式。 網路上大家都推薦西聯匯款,所以我也是選擇這種方式,AdMob 上設定付款方式超簡單,只要選好你要的匯款方式並且填入英文姓名然後儲存就好了,我是用護照上的英文姓名,並且都是用大寫。 原本台灣有支援的西聯匯款的銀行有元大銀行跟京城銀行,但元大已經在去年取消這個服務,所以只有去京城銀行辦理開戶了。 我是到新竹火車站附近的京城銀行辦理開戶,鄰櫃被告知要先在網路上填申請表才能開戶,銀行也沒有紙本申請表,於是用手機花了一些時間填完申請表。然後行員會說最好有 MTCN code 再來辦,基本上跟行員強力要求就是要辦應該就可以了,加上我已經有收到過匯款所以有 MTCN code,這部分沒遇到太多刁難。 接著行員問我要不要直接從銀行領現金,當然選擇不要,跟他說要回去用網路轉回帳戶,然後就是等行員的流程作業,完成後要求我下載京城銀行的 APP 做設定,基本上跟其他銀行開戶都是大同小異,整個過程還算是順利,行員的態度也很好,剩下就是等甚麼時候美金漲高一點,就可以來試著領一次匯款了。 *** Reference: * [[懶人包]Google AdSense廣告如何領錢?電匯、西聯匯款、支票。](https://sofree.cc/google-adsense-payment/) * [Admob / Adsence 西聯付款](http://development-ls.blogspot.com/2014/07/admob-adsence.html) * [全台僅存的京城銀行西聯匯款收款](https://blog.user.today/ktb-western-union/) * [免跑銀行,直接線上領取 Google AdSense 廣告收入 (西聯匯款)](http://www.vixual.net/blog/archives/149)

2019年12月9日 星期一

[Android] APP 配色法則

markdown 寫 APP 最困難的部分除了 Android 坑爹的機制之外,剩下就是 GUI 介面的設計,顏色搭配就是其中一環。尤其是對毫無美感的程式工程師來說,如何選擇 APP 的顏色真的是一大難關。 這裡收集一些配色相關的教學資源,供以後要選擇配色時參考 * [學會這幾種顏色搭配方案,讓APP UI設計配色遊刃有餘](https://kknews.cc/zh-hk/design/m2j8b9z.html) * [配色,配色,配色!看完你就懂了](https://kknews.cc/news/vm93nr4.html)

2019年12月2日 星期一

[Android] Crashlytics 在 debug build 時不工作

markdown 最近想要在新的 app 上測試 Crashlytics,在 debug 中不管怎麼 crash 都沒有在 firebase 看到 crash report,後來才發現當初在 AndroidManifest.xml 裡面 Crashlytics 開關的設定是變數。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMPayCgXXzQ1Km_0CyxRaETHWSiyBuQs4aq-aYFiw7lvfrOKMF_UCtQ5ZfKWSKoJp7KBUZ7WlpJot4lG97HOvE0a9siDASEXyR_wBUEfFuV398De7TyA-OSDFSXn0_Lkrfw5Zljy3JA6K-/s1600/Image+1.png) 而 crashlyticsEnabled 這個值在 app level 的 build.gradle 中設定,並且在 debug mode 中是設定為 false 的。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmTlgNNe6R07dvpS5-ShbUf_J3tw1Ht13Y5GwPhXnJUITHT-qi2lU08fWLo3_ei5oNeypkniLHKiz4HHOy62E77aY2D67m4Mcq_n_5rbAwHPwe5BtkF9KUFvRSCT5fDKRv7WCPb7uzIg2w/s1600/Image+2.png) 在這邊記錄一下以免以後又忘記。 --- Reference: * [Firebase Crashlytics ](https://ithelp.ithome.com.tw/articles/10197520)

2019年11月30日 星期六

[Android] Android Studio 的 product flavor soruce set 目錄無法執行新增 package 操作

markdown 在 Android Studio 中可以新增 product flavor 來定義不同的 Build Variant,例如我新增的 ad 跟 noAd 兩個 product flavor,就會有四個 Build Variant adDebug、adRelease、noAdDebug 與 noAdRelease。 接著照 [Android 官網的文件](https://developer.android.com/studio/build/build-variants#sourcesets)操作就可以新增 source set,但是我接著要在 source set 中新增 package 的時候,發現 source set 不是 java 目錄,如下圖,正常 java 目錄是藍色,灰色是一般目錄。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi83T3qIARHbjH0N3hS_vqNVMm1te_biOn3jfynUoP8twTJSRSzOV2QrtEOFD_drX8QS3SdIcl0tVVufOzcO6yWd7Ll_Z_aWa2cVWLJ6XokzIBsEoQDINgVYDW1rbE_7rvgKYQLmYmB-cva/s1600/Image+1.png) 但是另一個 Build Variant noAd 卻又正常,接著就 clean build,重開 Android Studio 試了各種方法,都沒用,最後才發現,我的 Active Build Variant 是 noAdDebug,當切到 adDebug 後,ad 下面的 java 目錄就變成藍色,可以執行新增 package 操作了。 同理,在左側 Project view 切換到 Android 的時候,也只有 Active Build Variant 的目錄才會顯示出來。 ![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtiwvF7j6PiElrz86_ymy7g3WlVwPYZtfDhz10hOiTSDY_uKgmI_22QisMVWddYZqd2O32CcaWfnIKRF5s0En1bpz7QqFtFYz1AzIgRAI0AlssOOR2oXcGsVuwilZz7-coadnLobkqcfzG/s320/Image+2.png) 為了這個蠢問題浪費了幾個小時,其實不太懂這個設計的用意是為什麼阿。