Android項目覆盤1

我的主頁:chengang.plus/java

文章將會同步到我的微信公衆號:Android部落格android

一、商城項目

1.1 RecyclerView首頁加載商品item內存佔用太高

  • 緣由:首頁包含了精選,banner,秒殺,熱賣列表,可是每個ViewType沒有在RecyclerView中設置各自的類型,致使緩存的時候當作一整ViewHolder緩存,從而總體內存佔用太高。尤爲底部的熱賣列表上拉加載的時候,顯得尤其顯著。

1.1.1 源碼追溯

RecyclerView.Recyclerc++

void recycleViewHolderInternal(ViewHolder holder) {
    boolean cached = false;
    boolean recycled = false;
    if (forceRecycle || holder.isRecyclable()) {
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            // Retire oldest cached view
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            int targetCacheIndex = cachedViewSize;
            if (ALLOW_THREAD_GAP_WORK
                    && cachedViewSize > 0
                    && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                // when adding the view, skip past most recently prefetched views
                int cacheIndex = cachedViewSize - 1;
                while (cacheIndex >= 0) {
                    int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                    if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                        break;
                    }
                    cacheIndex--;
                }
                targetCacheIndex = cacheIndex + 1;
            }
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    }
}
複製代碼

RecyclerView.RecycledViewPoolshell

public void putRecycledView(ViewHolder scrap) {
    final int viewType = scrap.getItemViewType();
    final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
    if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
        return;
    }
    scrap.resetInternal();
    scrapHeap.add(scrap);
}
SparseArray<ScrapData> mScrap = new SparseArray<>();

static class ScrapData {
    final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
    int mMaxScrap = DEFAULT_MAX_SCRAP;
    long mCreateRunningAverageNs = 0;
    long mBindRunningAverageNs = 0;
}

public ViewHolder getRecycledView(int viewType) {
    final ScrapData scrapData = mScrap.get(viewType);
    if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
        final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
        for (int i = scrapHeap.size() - 1; i >= 0; i--) {
            if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                return scrapHeap.remove(i);
            }
        }
    }
    return null;
}
複製代碼

緩存分兩個區域:json

  • mCachedViews是一個List類型;
  • SparseArray中的ScrapData包含了ArrayList,分ViewType存放ViewHolder,相同ViewType的ViewHolder放在一個List中,當取一個緩存出來的同時remove。

到這裏能夠明白,當首頁整個做爲一個Viewtype類型的時候,會緩存一個很大的ViewHolder對象到mCachedViews或RecycledViewPool中。canvas

1.1.2 解決辦法

  • 首頁的多個視圖類型拆分爲不一樣的ViewType,分不一樣的視圖類型加載。
  • 根據視圖中商品icon圖片大小,與服務端協商減少商品列表中icon圖片的分辨率;同時本地存放的圖片必須放置在合理的drawable文件夾中,由於文件夾對應的設備像素密度與機器屏幕像素密度越接近,內存佔用會越小。

下邊解釋緣由。緩存

1.1.2.1 Bitmap內存計算

本地加載圖片時的各類decode方法最終到了BitmapFactory.cpp的doDecode()方法中,以下:bash

BitmapFactory.cpp微信

static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, jobject padding, jobject options) {
    if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
       const int density = env->GetIntField(options, gOptions_densityFieldID);
       const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
       const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
       if (density != 0 && targetDensity != 0 && density != screenDensity) {
          scale = (float) targetDensity / density;
       }
    }

    // Determine the output size.
    SkISize size = codec->getSampledDimensions(sampleSize);
    
    int scaledWidth = size.width();
    int scaledHeight = size.height();
    bool willScale = false;
    // Apply a fine scaling step if necessary.
    if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
            willScale = true;
      scaledWidth = codec->getInfo().width() / sampleSize;
            scaledHeight = codec->getInfo().height() / sampleSize;
    }
    
    // Scale is necessary due to density differences.
    if (scale != 1.0f) {
       willScale = true;
       scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
       scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
    }
    
    const float sx = scaledWidth / float(decodingBitmap.width());
    const float sy = scaledHeight / float(decodingBitmap.height());
    
    SkCanvas canvas(outputBitmap);
    canvas.scale(sx, sy);
    canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
複製代碼

從源碼能夠看出scaledWidth通過兩次計算,一次是若是sampleSize不等於1的時候計算縮放寬高,等於原寬高分別除以採樣倍數;另一次是若是目標屏幕密度和當前圖片所處文件夾的密度不一致的話,計算出:框架

scale = targetDensity / density

(好比機器當前是xxhdpi,對應480,而圖片放置在xhdpi中,對應320,就會算出一個大於1的拉伸係數)

若是scale不等於1,用第一次計算的

scaledWidth * scale + 0.5,scaledHeight * scale + 0.5

能夠看到分兩步,一步是用最初的圖片大小除以採樣係數;一步是根據屏幕密度計算出來的拉伸係數而後乘以這個係數

不過具體在作縮放操做的時候縮放因子等於兩次計算以後的寬高分別處以原始寬高。可見對於設置採樣率能夠節省部份內存。

最後實際的佔用大小:

width = (originWidth / sampleSize) * (targetDensity / density) + 0.5

height = (originHeight / sampleSize) * (targetDensity / density) + 0.5

totalSize = width * height * 像素位

(targetDensity是手機實際密度,等於寬平方 + 高平方開根號,處於屏幕對角線長度,density是圖片在App所處文件的密度。)

  • ARGB_8888: 每一個像素4字節. 共32位,默認設置。
  • Alpha_8: 只保存透明度,共8位,1字節。
  • ARGB_4444: 共16位,2字節。
  • RGB_565:共16位,2字節,只存儲RGB值。

getRowBytes()返回的是每行的像素值,乘以高度就是總的像素數,也就是佔用內存的大小。

getAllocationByteCount()與getByteCount()的返回值通常狀況下都是相等的。只是在圖片 複用的時候,getAllocationByteCount()返回的是複用圖像所佔內存的大小,getByteCount()返回的是新解碼圖片佔用內存的大小。

1.1.2.2 Bitmap內存模型
  • Android 3.0 (API level 11)

從這個版本開始,bitmap的ARGB數據(像素數據)和bitmap對象一塊兒存在Dalvik的堆裏了。這樣bitmap對象和它的ARGB數據就能夠同步回收了。

後續Android又引入了BitmapFactory.Options.inBitmap字段。

若是設置了這個字段,bitmap在加載數據時能夠複用這個字段所指向的bitmap的內存空間。新增的這種內存複用的特性,能夠優化掉因舊bitmap內存釋放和新bitmap內存申請所帶來的性能損耗。

可是,內存可以複用也是有條件的。好比,在Android 4.4(API level 19)以前,只有新舊兩個bitmap的尺寸同樣才能複用內存空間。Android 4.4開始只要舊bitmap的尺寸大於等於新的bitmap就能夠複用了。

這樣GC沒法知道當前的內存狀況是否樂觀,大量建立bitmap可能不會觸發到GC,而Native中bitmap的像素數據可能已經佔用了過多內存,這時候就會OOM,因此推薦在bitmap使用完以後,調用recycle釋放掉Native的內存。

  • Android 8.0以前

Bitmap的內存分配在dalvik heap,Bitmap中有個byte[] mBuffer,其實就是用來存儲像素數據的,它位於java heap中,經過在native層構建Java Bitmap對象的方式,將生成的byte[]傳遞給Bitmap.java對象。

像素數據就和bitmap對象一塊兒都分配在堆中了,一塊兒接受GC管理,只要bitmap置爲null沒有被強引用持有,GC就會把它回收掉,和普通對象同樣。

  • Android 8.0以後

Bitmap像素內存的分配是在native層直接調用calloc,因此其像素分配的是在native heap上,而且還引入了NativeAllocationRegistry機制。

Bitmap引入了NativeAllocationRegistry這樣一種輔助自動回收native內存的機制,依然不須要用戶主動回收了,當bitmap的Java對象被回收後,NativeAllocationRegistry輔助回收這個對象所申請的native內存。

  • 在RecyclerView Adapter的onViewRecycled方法中,釋放圖片:
@Override
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
    try {
        if (!((Activity) context).isDestroyed() && !((Activity) context).isFinishing()) {
            ImageView img = holder.itemView.findViewById(R.id.goods_img);
            img.setImageDrawable(null);
            Glide.with(context).clear(img);
        }
    } catch (Exception e) {
        MyLog.d(TAG, "recycle fail:" + e.getLocalizedMessage());
    }
}
複製代碼

1.2 引導頁到首頁中間過渡時間長

developer.android.com/topic/perfo…

zhuanlan.zhihu.com/p/91226153

juejin.im/entry/5b813…

1.2.1 Application到Activity加載流程

Activity啓動流程

總結上圖的流程就是:

Application的構造器方法——>attachBaseContext()——>onCreate()——>Activity的構造方法——>onCreate()——>配置主題中背景等屬性——>onStart()——>onResume()——>測量佈局繪製顯示在界面上。

1.2.2 啓動分析

  • 冷啓動。冷啓動是指應用從頭開始啓動:系統進程在冷啓動後才建立應用進程。發生冷啓動的狀況包括應用自設備啓動後或系統終止應用後首次啓動。這種啓動給最大限度地減小啓動時間帶來了最大的挑戰,由於系統和應用要作的工做比在其餘啓動狀態下更多。

  • 熱啓動。應用的熱啓動比冷啓動簡單得多,開銷也更低。在熱啓動中,系統的全部工做就是將您的 Activity 帶到前臺。若是應用的全部 Activity 都還駐留在內存中,則應用能夠無須重複對象初始化、佈局擴充和呈現。

  • 溫啓動。溫啓動涵蓋在冷啓動期間發生的操做的一些子集;同時,它的開銷比熱啓動多。有許多潛在狀態可視爲溫啓動。例如:

用戶退出您的應用,但以後又從新啓動。進程可能已繼續運行,但應用必須經過調用 onCreate() 從頭開始從新建立 Activity。 系統將您的應用從內存中逐出,而後用戶又從新啓動它。進程和 Activity 須要重啓,但傳遞到 onCreate() 的已保存實例狀態包對於完成此任務有必定助益。

1.2.3 測量啓動時間

adb shell am start -W [packageName]/[packageName.MainActivity]

輸出以下:

E:\data_parse>adb shell am start -S -W com.xx.xx.xx/.activity.MainActivity
Stopping: com.xx.xx.xx
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xx.xx.xx/.activity.MainActivity }
Status: ok
Activity: com.xx.xx.xx/.activity.MainActivity
ThisTime: 1136
TotalTime: 75246
WaitTime: 1179
Complete
複製代碼
  • ThisTime : 最後一個 Activity 的啓動耗時(例如從 LaunchActivity --> MainActivity「adb命令輸入的Activity」 , 只統計 MainActivity 的啓動耗時)
  • TotalTime : 啓動一連串的 Activity 總耗時.(有幾個Activity 就統計幾個)
  • WaitTime : 應用進程的建立過程 + TotalTime .

圖片來源juejin.im/entry/5b813…

  • 在第①個時間段內,AMS 建立 ActivityRecord 記錄塊和選擇合理的 Task、將當前Resume 的 Activity 進行 pause.
  • 在第②個時間段內,啓動進程、調用無界面 Activity 的 onCreate() 等、 pause/finish 無界面的 Activity.
  • 在第③個時間段內,調用有界面 Activity 的 onCreate、onResume.

1.2.4 解決問題

從兩方面入手,想辦法縮短Application消耗的時間;縮短Activity消耗的時間。

1.2.4.1 請求數據統一整合

咱們的項目中有各類SDK的初始化,包括友盟,百川,開普勒,Glide,分享等。

  • 第一步,將以前各個負責人的數據請求框架集合到一個類中,綜合統一調度。統一Json解析方法,全部的json解析挪入IO線程處理,序列化成類以後統一返回。

json解析的過程存在json字符遍歷,而商城類項目從服務端返回的數據上百k,有些json結構很是複雜,比較耗時

  • 第二步,首頁數據分多個接口提供,每個接口一個負責人,致使每個負責人在首頁啓動的時候都去請求數據,沒有作到統一調度,並且部分頁面的數據在IO線程請求完畢以後,調度到UI線程作解析,明顯拖慢了加載速度
  • 第三步,對首頁統一返回的數據作拆分,對優先級比較低的數據,單獨作一個接口,待首頁加載完成以後或頁面展現時再展示
  • 第四步,將在Application中預先加載首頁數據挪到閃屏頁加載,充分利用閃屏頁延遲等待的2000ms的時間
  • 第五步,將沒必要要的SDK初始化挪到首頁展現完成以後初始化或使用前初始化
1.2.4.2 視圖xml優化
  • 將閃屏頁的圖片從xml的ImageView android:src中移除,加快xml inflate速度,並在Activity的onCreate方法中經過圖片工具類加載,這樣有必定機率通緩存中加載,避免每次都須要解碼。而解碼又須要消耗一些內存,還可能致使OOM。
  • 檢查閃屏頁和首頁的xml視圖層級,將過渡繪製的頁面提出來重點優化,對沒必要要的層級刪除,對沒必要要的背景設置爲null或透明。
  • 對於自定義的View,重點檢查onDraw方法,避免對象的建立
1.2.4.3 其餘一些優化
  • SharedPreference不能寫入大量數據做爲value,只能寫入一些flag等標識性變量,由於SharedPreference在初始化的時候會從Disk加載數據,這裏是阻塞式的。而若是以前存有大量的數據在裏面會致使阻塞主線程。在獲取Editor往裏面提交數據的時候,又會去等待SharedPreference初始化完成。因此這裏一方面致使SharedPreference初始化慢,IO操做會阻塞主線程;另外一方面又有可能由於等待引發ANR。

SharedPreferencesImpl

private final Object mLock = new Object();

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

    @Override
public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked();
    }

    return new EditorImpl();
}

public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}
複製代碼

在這裏能夠看到,加鎖的對象是mLock,當loadFromDisk方法執行完畢以後,纔會執行mLock.notifyAll();,至此,其餘的代碼纔會得到執行時機。尤爲是後續edit,以及put/get操做的時候。

1.3 flutter版本Widget刷新時間長,刷新頻繁

fluttersamples.com/

當在StatefulWidget中調用setState的時候,會致使當前Widget下全部Widget樹刷新,這種狀況若是趕上覆雜的佈局,確定是不可想象的,先看看調用setState的時候發生了什麼,僞代碼以下:

@protected
void setState(VoidCallback fn) {
    final dynamic result = fn() as dynamic;
    _element.markNeedsBuild();
    
     scheduleFrame();
     
     void handleDrawFrame() {};
     
     void drawFrame() {};
     
     rebuild();
     
     preformRebuild();
     
     build();
     
     updateChild();
     
     update();
}
複製代碼

能夠看到最終會致使從新請求渲染幀,更新視圖。

解決方案是:

  • 將在頂級視圖的setState方法下放到各個須要更新的子視圖中,由子視圖控制刷新
  • 將沒必要要的StatefulWidget改爲StatelessWidget,避免沒必要要的刷新
  • 當子視圖須要狀態更新可是層級較多時,引入InheritedWidget。看看InheritedWidget的大體工做流程:
//第一步
@override
void _updateInheritance() {
    final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
    if (incomingWidgets != null)
      _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
    else
      _inheritedWidgets = HashMap<Type, InheritedElement>();
    _inheritedWidgets[widget.runtimeType] = this;
}

//第二步
@override
T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    if (ancestor != null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
}

@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
}

@override
void updateDependencies(Element dependent, Object aspect) {
    setDependencies(dependent, null);
}

@protected
void setDependencies(Element dependent, Object value) {
    _dependents[dependent] = value;
}

//第三步
@protected
void updated(covariant ProxyWidget oldWidget) {
    notifyClients(oldWidget);
}

@override
void notifyClients(InheritedWidget oldWidget) {
    for (final Element dependent in _dependents.keys) {
      notifyDependent(oldWidget, dependent);
    }
}

void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    dependent.didChangeDependencies();
}

@mustCallSuper
void didChangeDependencies() {
    markNeedsBuild();
}
複製代碼
  • 第一步,Element初始化階段。

在這個階段每個Element在mount的過程當中會調用_updateInheritance方法,生成一個HashMap _inheritedWidgets。這裏比較取巧的是,當父類已經存在的時候,直接在父類的_inheritedWidgets裏面追加,而runType就是他的key,因此能夠輕鬆找到InheritedWidget。

  • 第二步,從InheritedWidget獲取數據階段。

InheritedWidget的子Widget調用它對外暴露的of方法時,經過調用dependOnInheritedWidgetOfExactType方法返回InheritedWidget自身。這裏從第一步的_inheritedWidgets中經過runType找到這個對象,而後調用它的setDependencies方法,將子Widget的Element做爲依賴項加入到一個HashSet _dependents中。

  • 第三步,通知子Element更新。

當InheritedWidget的數據發生變化時,會觸發渲染樹更新,當調用它的update方法更新Element的時候,會遍歷上一步_dependents中保存的依賴Element,並重建這些Element。

透過以上步驟,咱們能夠發現不論InheritedWidget與須要依賴它數據的Widget中間隔了多少層級,只要InheritedWidget數據發生變化,都能通知依賴它的Widget重繪。

相關文章
相關標籤/搜索