編輯:關於Android編程
對於一款APP,用戶首先關注的是 app的性能,而不是APP本身的屬性功能,用戶不關心你是否是搞社交,是否搞電商,是否是一款強大的美圖濾鏡app,用戶首先關注的是 性能—-性能不好,用戶會直接卸載,在應用市場給一個惡狠狠得差評,小則影響產品口碑,大則影響公司的品牌和聲譽,作為程序員,app的性能更應該作為我們關注的一個功能,而不是出了問題 才去門頭苦惱加班加點的負擔。
老實說,提高app性能的確非常難,處理這些問題 你必須知道:
應用速度慢的原因 還必須正確使用分析工具來分析數據為此誕生了本文:
了解這些工具並用它們找到造成性能不好的原因 從理論角度了解它們性能優化聽上去是一項非常艱巨的功能,但仔細思考,其實非常簡單:
獲取信息,
有人說你應用慢,應用閃退(崩掉,crash掉)的時候你需要找到原因。
通過運行 分析和反饋工具軟件來收集應用相關的信息,我們需要明確哪些可以測量,哪些可以優化
也就是說任何應用開始優化時,整個過程取決於問題的可測性以及性能優化的可評價性。 開發中經常遇到的坎,問題不可復現,以及對於某一個細節是否需要優化 拿不定主意,這個需要我們自己身處其境 考慮分析各方面因素 得出結論,而不是純粹得靠感覺。
分析數據,
很多時候,我們並不能直接理解問題的原因,比如內存溢出(OOM)的error,判斷內存溢出需要計算很多個變量得內存大小,我們並不能直觀通過眼球看出來一個app 運行過程那些變量的內存,這裡我們就需要運行分析工具來幫助我們,將其轉化為可視化的圖表,
在這裡,我們可能還是看不懂那些圖表,橫線豎線,具體是個什麼玩意,
沒有關系,去弄懂它們!就可以成為性能大師了.
現在你看那些內存中的二進制轉換成圖表的過程,就類似於古代的算命大師,
步驟1和步驟2 會不斷的循環,搜集數據,分析數據···
有時候我們不只使用一種搜集工具和分析工具,這就需要自己針對性能得種類來深入研究了
Tack action!
發現了問題,找到了問題所在以及發生的原因,我們必須要恰當的去解決它,根據項目進度,該性能的優化成本,性能優先級,考慮項目中使用的java庫或者android開源框架,其中的一些嚴格限制,
在你的方案提出之前,這些因素都是我們需要考慮的,因為提出的優化方案,不一定會被公司高層接受(除非你就是高層)。
工具不是規則,理解事物的規則和流程更重要
內存大小屬於手機性能之一
舉個簡單的例子,內存就像你的臥室一樣,當你在老家住著動辄幾百平的村莊,舒服慣了,突然變賣家產一門心思想創業來到北京,家裡的老本只夠你住幾平米的衛生間的時候,你就會注意到內存【房間】大小的重要性了。
首先我們要知道內存是如何影響系統運行
通常我們認為代碼執行速度等同於物理硬件的執行速度,我們的代碼指令都是通過使用內存來完成的。通過為實例對象,常量,變量分配內存,來完成操作,但是如何釋放這些內存,通常我們並不清楚。
一旦分配出去的內存沒有及時回收,會引造成系統卡頓,執行操作緩慢現象,這種現象稱之為內存洩漏,Memory leak
java中的JVM就是一個抽象的計算機,和實際的計算機一樣,它具有指令集並使用不同的存儲區域,JVM負責執行代碼,管理數據,管理內存和寄存器。
垃圾回收機制只做兩件基本的事情:
發現無用的對象 回收被無用對象占用的內存空間,使得該空間可以被程序再次利用通常,垃圾回收具有如下特點:
垃圾回收機制的工作目標是回收無用對象的內存空間,這些內存空間都是JVM堆內存的內存資源,對於其他物理資源,比如數據庫連接,磁盤I/O 等資源無能為力
為了讓垃圾回收機制盡快回收那些對象,可以將該對象引用變量置為null,
垃圾回收發生的不可預知性,不同JVM 采用不同的算法和機制,有可能定時回收,有可能cpu空閒回收,也有可能內存消耗極限發生,即使通過Runtime對象的gc(),System.gc()來建議系統進行回收,但這之屬於建議,不能精確控制垃圾回收機制的執行,
意思就是說,垃圾回收機制什麼時候開始執行,並不是我們程序員能控制的,我們只能給予建議。
那麼問題來了,如何精確的進行垃圾回收呢?
回答很明確,確保每一個對象都進行了有效的釋放。對於不再需要的對象,不要引用他們,一旦在別的地方保持對這個對象的引用,垃圾回收機制 暫時不會回收該對象,則會導致嚴重得問題—-系統可用內存越來越少,垃圾回收執行的頻率越來越高,cpu全都被垃圾回收的操作占有了,系統性能自然而然就下降了!
java8 已經刪除了永生代內存,即一些常駐內存,不會回收的數據,而是改為使用本地內存來存儲類的元數據,稱之為元空間(Metaspace),不過貌似和Android開發沒關系(-__-)。
回顧完java垃圾回收,下面介紹
經由為數據分配內存的類型,以及系統如何有效的利用gc回收內存,並為新的對象分配內存。
所有要申請的內存都被劃分到內存空間中,根據這些特點,哪些數據分配到哪些內存中,取決於Android的版本,
最為重要的一點,Android系統為每個運行中的app分配了預設的內存通常為16m-32m之間,當分配的內存越多,系統內存不足時,系統就可能會執行內存清理,注意,是可能會執行,是否執行垃圾清理是由系統自己判斷的。
進行垃圾回收,以確保有足夠的內存分配給其他的應用操作,不同的Android版本,會有不同的gc操作,例如在davailk中,gc代表終止程序操作,
上圖是正常的界面刷新流程,<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxpbWcgYWx0PQ=="這裡寫圖片描述" src="/uploadfile/Collfiles/20160602/20160602091103271.png" title="\" />
上圖,gc占據了一大塊的時間,對於我們人類來說很短,但是對於系統來說很長了。
綜合三張圖分析:代碼質量很差,使得系統為我們的app分配了過多內存,而且沒有及時回收,系統需要更多的時間去執行gc回收,那麼系統就沒時間去保持界面的活躍,所以就造成了卡頓的現象。,
在一個循環體中,重復得創建對象,就會造成內存污染,馬上就會有很多gc
啟動,由於這一額外的內存壓力,內存洩漏仍然會產生,當可用內存降低到一定總量時,會強制系統gc執行,那循環體中的那部分操作會顯示出卡頓的情況,甚至有可能在內存極限的時候,我們開發的應用會閃退。
所以,唯一的解決辦法是:減少代碼申請的內存量,不使用的對象及時回收。
整個層疊圖,代表還有多少內存可用
深藍色區域:代表正在使用的內存大小
淺灰色區域:代表空閒未分配內存
這個紅色箭頭所指的坡度表示急需大量的內存,內存分配也急劇的增加。
上圖是一個內存管理良好的例子
下圖我們看一個內存糟糕的例子
分析
這裡有一部分代碼占用了大量的內存,然後又一下子釋放了內存,生成不斷重復又窄又長的曲線,這就是程序在花大量的時間在進行垃圾清理,運行垃圾清理的時間越多,其他操作可用的時間就越少,比如跟網絡交互數據,頁面刷新,打電話,聽歌等等,這樣就造成了卡頓
solution:Tools-Android-Android adb interact
最初並不會見效,重啟app即可,
在這裡我才開始引入內存洩漏的原因【雖然文章前面已經提到,但是在這裡才著重拿出來作為一節】
網絡上和一些書籍對內存洩漏解釋是 應該回收的對象沒有回收,有點不全面,我認為深一點來說,內存洩漏是針對系統而言的,內存洩漏指的是不能被使用的內存,但是垃圾回收器無法識別出來,對其進行回收,這些對象一直存在於堆中,並且持續占據著內存空間,無法被刪除,隨著不斷洩漏,系統可用的內存就越來越小,意味著系統又需要花更多的時間 去進行內存清理操作,進行垃圾回收操作的次數越來越多,
簡單的內存洩漏:對沒有使用的對象 循環引用
復雜一點的:在listview還沒有繪制完成時就添加到activity >
Heap viewer使用步驟,我錄制了gift圖,詳情請看:
了解Heap Viewer:
Heap Viewer可以有效的分析程序在堆中分配的數據類型及數量和 大小小
這裡表示 byte數組和boolean數組的數量為177,占用了1.423M的內存
綠色箭頭標出來的那部分 代碼就是有問題的部分,原因在於,可以內存幾乎為0,所有的內存已經被程序占用,首先記住,我們的代碼有問題,造成了內存洩漏,並且,垃圾回收機制無法回收那部分內存空間
下圖為30s之後的內存回收情況
啟動第二次gc,此時Android調整並提高應用的內存上限,這樣做的同時,如果漏洞沒有修復,表明內存洩漏仍然存在,那麼還會有第三次,第四次同樣的gc操作,直至系統無法調整提高給應用更高的內存上限,造成內存溢出,甚至可能死機,
Trace Viewer可以精確追蹤到代碼的位置,限於篇幅請按照上圖點擊 那幾個按鈕 自行摸索考功它的功能能
Android 加載圖片會創建Bitmap,drawable實例,占用內存空間,如果不進行高效處理,程序會很快達到 Android系統分配給APP的內存上限,直至掛掉
圖片資源相比文本資源,在內存中會占據更大的內存,從字節數就可以看出來
在我們的應用中正確恰當高效的加載 圖片資源 是一件非常棘手的事情
Android 系統會分配給單個APP至少 16M左右的內存,) Android Compatibility Definition Document (CDD)中,根據不同手機的尺寸和屏幕像素來要求應用最小內存,我們開發 的應用需要優化內存至最低內存限制,然而請記住,許多手機對內存有著更高的要求。 圖片消耗大量的內存,尤其是高像素的圖片,比如入門級單反相機拍攝出來的一張圖片,都有可能超出APP的最低內存限制 app 中一些常見的UI 比如 ListView, GridView and ViewPager,都需要立刻加載大量的圖片,注意是立刻,這對內存管理提出了很高的要求。
所以我們需要高效得加載圖片。
一張圖片的像素,尺寸,分別率,由可能超過Android的UI組件本身大小,UI組件的大小是由手機設備屏幕決定的,這些超出的部分,會消耗更多的內存。
應該讓圖片去匹配我們的手機設備,所以,我們需要對圖片進行處理:
1. 不能超過每個應用程序的內存限制
2. 用最小的內存加載圖片
1. 讀取內存中Bitmap的尺寸和類型
BitmapFactory類提供了很多解析Bitmap的方法(decodeByteArray(), decodeFile(), decodeResource(), etc.),每一種解析方法都有一個額外的參數 BitmapFactory.Options,設置inJustDecodeBounds 屬性為true可以禁止應用分配內存,此時bitmap返回為null,但是我們可以通過BitmapFactory.Options對象來獲取很多有用的參數
此時 你可以通過BitmapFactory.Options來讀取圖片的尺寸和類型
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
總結:為了避免java.lang.OutOfMemory ,在加載圖片之前需要檢查原圖的大小是否超出最低內存限制。
2. 加載一張小圖,使得系統分配較少的內存給它。
現在我們已經獲取到了圖片的尺寸,加載一張圖片之前,我們需要考慮:
計算整張圖片需要多大的內存
我們希望給它多大的內存
加載圖片的組件比如Imageview的尺寸是多大 當前手機設備的屏幕尺寸和分辨率例如一張1080*720的圖片要展示在一個128*72的Imageview上
實際 項目中,比如一張2048x1536的圖片,我們通過設置inSampleSize為4,來創建實際大小為512x384的bitmap,這樣需要的內存為0.75MB而不是之前的12MB(色彩模式都是ARGB_8888的情況下),
Google官方提供了兩個方法來供我們使用,可以封裝到自己的工具類中:
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 獲得內存中圖片的寬高
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 計算出一個數值,必須符合為2的冪(1,2,4,8,tec),賦值給inSampleSize
// 圖片寬高應大於期望的寬高的時候,才進行計算
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 第一次解析 inJustDecodeBounds=true 只是用來獲取bitmap在內存中的尺寸和類型,系統並不會為其分配內存,
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 計算出一個數值
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 根據inSampleSize 數值來解析bitmap
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
研究一個新的函數,我們先關注函數的輸入和輸出提高閱讀能力
3.接著為我們的UI組件ImageView設置一張縮略圖咯:
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
為了完全理解這一部分,初學者請自行查閱
BitmapFactory.decode BitmapFactory.Options當圖片資源來自網絡或者硬盤的時候,最好不要直接在主線程中加載它,例如IO資源或者數據庫資源都會占用CPU,CPU 要做的事情過多,Android手機會造成卡頓得現象,
好在Google 提供了解決辦法–AsyncTask異步加載工具
class BitmapWorkerTask extends AsyncTask {
private final WeakReference imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// 用弱引用確保能被垃圾回收機制回收
imageViewReference = new WeakReference(imageView);
}
//在後台解析bitmap
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// 一旦完成,imageView將會加載bitmap
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
接著我們在主線程中執行它即可
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
1. 為什麼要緩存圖片?
對於如何高效的加載一張圖片 ,我們似乎已經得心應手了,這裡要潑一盆涼水給大家,因為我們的應用不僅只是加載一張圖片這麼簡單,比如ListView, GridView or ViewPager,RecyclerView,需要立刻加載出大量的bitmap,滑動的過程不斷加載bitmap,還要求不卡頓,內存夠用,這似乎又是一件棘手的事情。
Google 又提供了一種解決思路:對於ListView,RecyclerView,有可見的item和不可見的item,回收不可見的item 內存,分配給可見的,這樣內存得到了重復利用,避免重復創建對象,不斷申請並分配新的內存空間,觸發最低內存限制的危險。
所以我們要管理 這些 已經創建好的內存。
2. 使用內存緩存
為什麼優先使用內存緩存?
答:相比硬盤緩存的讀取速度,讀取內存中的數據更快
Google官方有什麼建議?
答:Google推薦Lrucache類,底層使用用強引用封裝的LinkedHashMap,來存儲最近使用的對象,它自動會回收最近使用的對象當中,使用的最少的那一個對象的內存,這一點毋庸置疑值得推薦!
一種過時的做法,是用虛引用或者弱引用來標記bitmap,這種方法在Android 3.0以後已經不提倡了,因為類似JDK1.8那樣,bitmap的內存是放在本地內存中的,它的回收是不確定的,有可能導致APP掛掉,切勿使用。
Lrucache這麼棒,我們該如何用?
Lrucache就可以當作一種存儲數據的結構,類似list,set,可以存儲對象,獲取對象,對應的有add() 和get()方法,它與數組一樣,初始化的時候需要指定一個初始的大小。
那麼Lrucache實例 初始的大小該如何確定?
在計算大小之前,我們需要明確幾件事情:
cache大小是由內存大小決定的,而不是 它存儲數據的個數決定
這讓我想起一個在項目開發中常見的bug,use a bitmap which has bean recycler
3. Lrucache使用實例
private LruCache mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// 當Lrucache使用的內存大小超過虛擬機最大的可用內存時候,Android會拋出OutOfMemory exception
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 使用虛擬機可用1/8
final int cacheSize = maxMemory / 8;
// 在Lrucache構造函數中初始化它的大小
// int in its constructor
mMemoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
//cache大小是由內存大小決定的,而不是 它存儲數據的個數決定
return bitmap.getByteCount() / 1024;
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
loadBitmap的過程很簡潔:如果內存緩存中有這張bitmap,則直接刷新imageview,如果bitmap為空,則啟動後台線程去加載bitmap,接著刷新imageview
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
異步線程 BitmapWorkerTask
class BitmapWorkerTask extends AsyncTask {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}
4. 磁盤緩存
與內存緩存相結合的還有磁盤緩存,雖然磁盤讀取速度較慢,但是持久存儲的,不像內存緩存那樣,在內存極限情況下仍然會被清理,比如後台正在執行數據加載,突然打進來一個電話,內存不足系統可能會進行垃圾回收。緩存就沒有了
Google官方提供直接DiskLruCache 類
為什麼要使用DiskLruCache 很明了:就是解決當內存緩存不可用的情形
內當需要頻繁訪問緩存的圖片資源時,比如APP的畫廊功能,可以考慮使用ContentProvider解決更為妥當。
下面是它的源碼
private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Initialize memory cache
...
// Initialize disk cache on background thread
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
new InitDiskCacheTask().execute(cacheDir);
...
}
class InitDiskCacheTask extends AsyncTask {
@Override
protected Void doInBackground(File... params) {
synchronized (mDiskCacheLock) {
File cacheDir = params[0];
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
mDiskCacheStarting = false; // Finished initialization
mDiskCacheLock.notifyAll(); // Wake any waiting threads
}
return null;
}
}
class BitmapWorkerTask extends AsyncTask {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final String imageKey = String.valueOf(params[0]);
// Check disk cache in background thread
Bitmap bitmap = getBitmapFromDiskCache(imageKey);
if (bitmap == null) { // Not found in disk cache
// Process as normal
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
}
// Add final bitmap to caches
addBitmapToCache(imageKey, bitmap);
return bitmap;
}
...
}
public void addBitmapToCache(String key, Bitmap bitmap) {
// Add to memory cache as before
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
// Also add to disk cache
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
mDiskLruCache.put(key, bitmap);
}
}
}
public Bitmap getBitmapFromDiskCache(String key) {
synchronized (mDiskCacheLock) {
// Wait while disk cache is started from background thread
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
return mDiskLruCache.get(key);
}
}
return null;
}
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
5.處理運行時變更的緩存
當屏幕旋轉,或者其他原因導致Activity restart,這個時候難道又讓我們重新創建大量的圖像資源?
回答是否定的,Google提供了一種解決方案:
通過在Activity中使用fragment,構造Fragment時,通過設置 setRetainInstance(true))來設置緩存,
話不多說,直接上代碼:
private LruCache mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
RetainFragment retainFragment =
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
mMemoryCache = retainFragment.mRetainedCache;
if (mMemoryCache == null) {
mMemoryCache = new LruCache(cacheSize) {
... // Initialize cache here as usual
}
retainFragment.mRetainedCache = mMemoryCache;
}
...
}
class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
public LruCache mRetainedCache;
public RetainFragment() {}
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
fm.beginTransaction().add(fragment, TAG).commit();
}
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}
當我通宵趕完這篇博客的時候,我的計算機也提醒我內存不足了 (●’?’●)
現在市面上的很多的應用,都帶有下拉列表的功能,將所有選項都放在下拉列表中,當用戶點擊選擇的時候,彈出所有的選項,用戶選擇一項後,下拉列表自動隱藏,很多下拉列表都是用Lis
一、示意圖:1)開始畫面:2)游戲中畫面:3)結束畫面:二、分析:1、游戲中的每個元素都可封裝成對象,1)開始按鈕與結束按鈕可封裝成GameButton對象:屬性有:有坐
CoordinatorLayout 實現了多種Material Design中提到的滾動效果。目前這個框架提供了幾種不用寫動畫代碼就能工作的方法,這些效果包括: *讓浮動
優步在線客服怎麼聯系?優步怎麼聯系客服?如果你不在撥打優步客服電話沒人接時,是不是會很心煩呢?沒關系,優步不僅有客服電話,還提供在線客服咨詢哦!下載吧小編為