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月16日 星期一

[攝影] 光圈、ISO 跟快門

markdown ### 光圈 光圈大小影響進光量,大小用 F 值表示,值越小表示光圈越大,相同 ISO 與快門條件下,光圈越大會讓畫面越亮。 大光圈容易拍出背景模糊的淺景深效果,小光圈拍攝主體跟背景都會比較清晰。 ### 快門 快門速度表示曝光時間長短,通常在光線充足情況下,需要的曝光時間越短,光線不足則需要曝光時間越長,但是曝光時間越長則越容易讓圖象產生殘影。 ### ISO ISO 是感光度,數值越高表示接收的光量變多,相同的光圈跟快門條件下,ISO 越高畫面會越明亮,適合在黑暗的環境拍攝,但是畫面的噪聲,顆粒感也會增加,對色彩也會有影響。

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/)