編輯:關於Android編程
作為Android開發,日常的開發工作中或多或少要接觸到性能問題,比如我的Android程序運行緩慢卡頓,並且常常出現ANR對話框等等問題。既然有性能問題,就需要進行性能優化。正所謂工欲善其事,必先利其器。一個好的工具,可以幫助我們發現並定位問題,進而有的放矢進行解決。本文主要介紹StrictMode 在Android 應用開發中的應用和一些問題。
什麼是StrictMode
StrictMode意思為嚴格模式,是用來檢測程序中違例情況的開發者工具。最常用的場景就是檢測主線程中本地磁盤和網絡讀寫等耗時的操作。
嚴在哪裡
既然叫做嚴格模式,那麼又嚴格在哪些地方呢?
在Android中,主線程,也就是UI線程,除了負責處理UI相關的操作外,還可以執行文件讀取或者數據庫讀寫操作(從Android 4.0 開始,網絡操作禁止在主線程中執行,否則會拋出NetworkOnMainThreadException)。使用嚴格模式,系統檢測出主線程違例的情況會做出相應的反應,如日志打印,彈出對話框亦或者崩潰等。換言之,嚴格模式會將應用的違例細節暴露給開發者方便優化與改善。
具體能檢測什麼
嚴格模式主要檢測兩大問題,一個是線程策略,即TreadPolicy,另一個是VM策略,即VmPolicy。
ThreadPolicy
線程策略檢測的內容有
自定義的耗時調用 使用detectCustomSlowCalls()開啟
磁盤讀取操作 使用detectDiskReads()開啟
磁盤寫入操作 使用detectDiskWrites()開啟
VmPolicy
虛擬機策略檢測的內容有
Activity洩露 使用detectActivityLeaks()開啟
未關閉的Closable對象洩露 使用detectLeakedClosableObjects()開啟
洩露的Sqlite對象 使用detectLeakedSqlLiteObjects()開啟
工作原理
其實StrictMode實現原理也比較簡單,以IO操作為例,主要是通過在open,read,write,close時進行監控。libcore.io.BlockGuardOs文件就是監控的地方。以open為例,如下進行監控。
1
2
3
4
5
6
7
8
@Override
public
FileDescriptor open(String path,
int
flags,
int
mode)
throws
ErrnoException {
BlockGuard.getThreadPolicy().onReadFromDisk();
if
((mode & O_ACCMODE) != O_RDONLY) {
BlockGuard.getThreadPolicy().onWriteToDisk();
}
return
os.open(path, flags, mode);
}
其中onReadFromDisk()方法的實現,代碼位於StrictMode.java中。
1
2
3
4
5
6
7
8
9
10
11
public
void
onReadFromDisk() {
if
((mPolicyMask & DETECT_DISK_READ) ==
0
) {
return
;
}
if
(tooManyViolationsThisLoop()) {
return
;
}
BlockGuard.BlockGuardPolicyException e =
new
StrictModeDiskReadViolation(mPolicyMask);
e.fillInStackTrace();
startHandlingViolationException(e);
}
如何使用
關於StrictMode如何使用,最重要的就是如何啟用嚴格模式。
放在哪裡
嚴格模式的開啟可以放在Application或者Activity以及其他組件的onCreate方法。為了更好地分析應用中的問題,建議放在Application的onCreate方法中。
簡單啟用
以下的代碼啟用全部的ThreadPolicy和VmPolicy違例檢測
1
2
3
4
if
(IS_DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
StrictMode.setThreadPolicy(
new
StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
StrictMode.setVmPolicy(
new
VmPolicy.Builder().detectAll().penaltyLog().build());
}
嚴格模式需要在debug模式開啟,不要在release版本中啟用。
同時,嚴格模式自API 9 開始引入,某些API方法也從 API 11 引入。使用時應該注意 API 級別。
如有需要,也可以開啟部分的嚴格模式。
查看結果
嚴格模式有很多種報告違例的形式,但是想要分析具體違例情況,還是需要查看日志,終端下過濾StrictMode就能得到違例的具體stacktrace信息。
1
adb logcat | grep StrictMode
解決違例
如果是主線程中出現文件讀寫違例,建議使用工作線程(必要時結合Handler)完成。
如果是對SharedPreferences寫入操作,在API 9 以上 建議優先調用apply而非commit。
如果是存在未關閉的Closable對象,根據對應的stacktrace進行關閉。
如果是SQLite對象洩露,根據對應的stacktrace進行釋放。
舉個例子
以主線程中的文件寫入為例,引起違例警告的代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public
void
writeToExternalStorage() {
File externalStorage = Environment.getExternalStorageDirectory();
File destFile =
new
File(externalStorage,
"dest.txt"
);
try
{
OutputStream output =
new
FileOutputStream(destFile,
true
);
output.write(
"droidyue.com"
.getBytes());
output.flush();
output.close();
}
catch
(FileNotFoundException e) {
e.printStackTrace();
}
catch
(IOException e) {
e.printStackTrace();
}
}
引起的警告為
1
2
3
4
5
6
7
8
D/StrictMode(
9730
): StrictMode policy violation; ~duration=
20
ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=
31
violation=
2
D/StrictMode(
9730
): at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:
1176
)
D/StrictMode(
9730
): at libcore.io.BlockGuardOs.open(BlockGuardOs.java:
106
)
D/StrictMode(
9730
): at libcore.io.IoBridge.open(IoBridge.java:
390
)
D/StrictMode(
9730
): at java.io.FileOutputStream.(FileOutputStream.java:
88
)
D/StrictMode(
9730
): at com.example.strictmodedemo.MainActivity.writeToExternalStorage(MainActivity.java:
56
)
D/StrictMode(
9730
): at com.example.strictmodedemo.MainActivity.onCreate(MainActivity.java:
30
)
D/StrictMode(
9730
): at android.app.Activity.performCreate(Activity.java:
4543
)
因為上述屬於主線程中的IO違例,解決方法就是講寫入操作放入工作線程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public
void
writeToExternalStorage() {
new
Thread() {
@Override
public
void
run() {
super
.run();
File externalStorage = Environment.getExternalStorageDirectory();
File destFile =
new
File(externalStorage,
"dest.txt"
);
try
{
OutputStream output =
new
FileOutputStream(destFile,
true
);
output.write(
"droidyue.com"
.getBytes());
output.flush();
output.close();
}
catch
(FileNotFoundException e) {
e.printStackTrace();
}
catch
(IOException e) {
e.printStackTrace();
}
}
}.start();
}
然而這並非完善,因為OutputStream.write方法可能拋出IOException,導致存在OutputStream對象未關閉的情況,仍然需要改進避免出現Closable對象未關閉的違例。改進如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public
void
writeToExternalStorage() {
new
Thread() {
@Override
public
void
run() {
super
.run();
File externalStorage = Environment.getExternalStorageDirectory();
File destFile =
new
File(externalStorage,
"dest.txt"
);
OutputStream output =
null
;
try
{
output =
new
FileOutputStream(destFile,
true
);
output.write(
"droidyue.com"
.getBytes());
output.flush();
output.close();
}
catch
(FileNotFoundException e) {
e.printStackTrace();
}
catch
(IOException e) {
e.printStackTrace();
}
finally
{
if
(
null
!= output) {
try
{
output.close();
}
catch
(IOException e) {
e.printStackTrace();
}
}
}
}
}.start();
}
檢測內存洩露
通常情況下,檢測內存洩露,我們需要使用MAT對heap dump 文件進行分析,這種操作不困難,但也不容易。使用嚴格模式,只需要過濾日志就能發現內存洩露。
這裡以Activity為例說明,首先我們需要開啟對檢測Activity洩露的違例檢測。使用上面的detectAll或者detectActivityLeaks()均可。其次寫一段能夠產生Activity洩露的代碼。
1
2
3
4
5
6
7
8
public
class
LeakyActivity
extends
Activity{
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
MyApplication.sLeakyActivities.add(
this
);
}
}
MyApplication中關於sLeakyActivities的部分實現
1
2
3
4
public
class
MyApplication
extends
Application {
public
static
final
boolean
IS_DEBUG =
true
;
public
static
ArrayList sLeakyActivities =
new
ArrayList();
}
當我們反復進入LeakyActivity再退出,過濾StrictMode就會得到這樣的日志
1
2
3
E/StrictMode(
2622
):
class
com.example.strictmodedemo.LeakyActivity; instances=
2
; limit=
1
E/StrictMode(
2622
): android.os.StrictMode$InstanceCountViolation:
class
com.example.strictmodedemo.LeakyActivity; instances=
2
; limit=
1
E/StrictMode(
2622
): at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:
1
)
分析日志,LeakyActivity本應該是只存在一份實例,但現在出現了2個,說明LeakyActivity發生了內存洩露。
嚴格模式除了可以檢測Activity的內存洩露之外,還能自定義檢測類的實例洩露。從API 11 開始,系統提供的這個方法可以實現我們的需求。
1
public
StrictMode.VmPolicy.Builder setClassInstanceLimit (Class klass,
int
instanceLimit)
舉個栗子,比如一個浏覽器中只允許存在一個SearchBox實例,我們就可以這樣設置已檢測SearchBox實例的洩露
1
StrictMode.setVmPolicy(
new
VmPolicy.Builder().setClassInstanceLimit(SearchBox.
class
,
1
).penaltyLog().build());
noteSlowCall
StrictMode從 API 11開始允許開發者自定義一些耗時調用違例,這種自定義適用於自定義的任務執行類中,比如我們有一個進行任務處理的類,為TaskExecutor。
1
2
3
4
5
public
class
TaskExecutor {
public
void
execute(Runnable task) {
task.run();
}
}
先需要跟蹤每個任務的耗時情況,如果大於500毫秒需要提示給開發者,noteSlowCall就可以實現這個功能,如下修改代碼
1
2
3
4
5
6
7
8
9
10
11
12
public
class
TaskExecutor {
private
static
long
SLOW_CALL_THRESHOLD =
500
;
public
void
executeTask(Runnable task) {
long
startTime = SystemClock.uptimeMillis();
task.run();
long
cost = SystemClock.uptimeMillis() - startTime;
if
(cost > SLOW_CALL_THRESHOLD) {
StrictMode.noteSlowCall(
"slowCall cost="
+ cost);
}
}
}
執行一個耗時2000毫秒的任務
1
2
3
4
5
6
7
8
9
10
11
TaskExecutor executor =
new
TaskExecutor();
executor.executeTask(
new
Runnable() {
@Override
public
void
run() {
try
{
Thread.sleep(
2000
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
});
得到的違例日志,注意其中~duration=20 ms並非耗時任務的執行時間,而我們的自定義信息msg=slowCall cost=2000才包含了真正的耗時。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
D/StrictMode(
23890
): StrictMode policy violation; ~duration=
20
ms: android.os.StrictMode$StrictModeCustomViolation: policy=
31
violation=
8
msg=slowCall cost=
2000
D/StrictMode(
23890
): at android.os.StrictMode$AndroidBlockGuardPolicy.onCustomSlowCall(StrictMode.java:
1163
)
D/StrictMode(
23890
): at android.os.StrictMode.noteSlowCall(StrictMode.java:
1974
)
D/StrictMode(
23890
): at com.example.strictmodedemo.TaskExecutor.executeTask(TaskExecutor.java:
17
)
D/StrictMode(
23890
): at com.example.strictmodedemo.MainActivity.onCreate(MainActivity.java:
36
)
D/StrictMode(
23890
): at android.app.Activity.performCreate(Activity.java:
4543
)
D/StrictMode(
23890
): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:
1071
)
D/StrictMode(
23890
): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:
2158
)
D/StrictMode(
23890
): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:
2237
)
D/StrictMode(
23890
): at android.app.ActivityThread.access$
600
(ActivityThread.java:
139
)
D/StrictMode(
23890
): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:
1262
)
D/StrictMode(
23890
): at android.os.Handler.dispatchMessage(Handler.java:
99
)
D/StrictMode(
23890
): at android.os.Looper.loop(Looper.java:
156
)
D/StrictMode(
23890
): at android.app.ActivityThread.main(ActivityThread.java:
5005
)
D/StrictMode(
23890
): at java.lang.reflect.Method.invokeNative(Native Method)
D/StrictMode(
23890
): at java.lang.reflect.Method.invoke(Method.java:
511
)
D/StrictMode(
23890
): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:
784
)
D/StrictMode(
23890
): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:
551
)
D/StrictMode(
23890
): at dalvik.system.NativeStart.main(Native Method)
其他技巧
除了通過日志查看之外,我們也可以在開發者選項中開啟嚴格模式,開啟之後,如果主線程中有執行時間長的操作,屏幕則會閃爍,這是一個更加直接的方法。
問題來了
日志的時間靠譜麼
在下面的過濾日志中,我們看到下面的一個IO操作要消耗31毫秒,這是真的麼
1
2
3
4
5
6
7
8
9
10
11
D/StrictMode(
2921
): StrictMode policy violation; ~duration=
31
ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=
31
violation=
2
D/StrictMode(
2921
): at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:
1176
)
D/StrictMode(
2921
): at libcore.io.BlockGuardOs.read(BlockGuardOs.java:
148
)
D/StrictMode(
2921
): at libcore.io.IoBridge.read(IoBridge.java:
422
)
D/StrictMode(
2921
): at java.io.FileInputStream.read(FileInputStream.java:
179
)
D/StrictMode(
2921
): at java.io.InputStreamReader.read(InputStreamReader.java:
244
)
D/StrictMode(
2921
): at java.io.BufferedReader.fillBuf(BufferedReader.java:
130
)
D/StrictMode(
2921
): at java.io.BufferedReader.readLine(BufferedReader.java:
354
)
D/StrictMode(
2921
): at com.example.strictmodedemo.MainActivity.testReadContentOfFile(MainActivity.java:
65
)
D/StrictMode(
2921
): at com.example.strictmodedemo.MainActivity.onCreate(MainActivity.java:
28
)
D/StrictMode(
2921
): at android.app.Activity.performCreate(Activity.java:
4543
)
從上面的stacktrace可以看出testReadContentOfFile方法中包含了文件讀取IO操作,至於是否為31毫秒,我們可以利用秒表的原理計算一下,即在方法調用的地方如下記錄
1
2
3
4
long
startTime = System.currentTimeMillis();
testReadContentOfFile();
long
cost = System.currentTimeMillis() - startTime;
Log.d(LOGTAG,
"cost = "
+ cost);
得到的日志中上述操作耗時9毫秒,非31毫秒。
1
D/MainActivity(
20996
): cost =
9
注:通常情況下StrictMode給出的耗時相對實際情況偏高,並不是真正的耗時數據。
注意
在線上環境即Release版本不建議開啟嚴格模式。
嚴格模式無法監控JNI中的磁盤IO和網絡請求。
應用中並非需要解決全部的違例情況,比如有些IO操作必須在主線程中進行。
普通按鈕也就那麼幾種樣式,看著都審美疲勞,先放效果圖: 你會不會以為這個按鈕是集結了很多動畫的產物,我告訴你,並沒有。所有的實現都是基於自定義View,采用最底
1 背景去年有很多人私信告訴我讓說說自定義控件,其實通觀網絡上的很多博客都在講各種自定義控件,但是大多數都是授之以魚,卻很少有較為系統性授之於漁的文章,同時由
先看看效果圖:停在中間自動翻頁序言:最近接到一個任務,做一個類似上面自動翻頁的功能。可以看到,這一屏中有三張圖片顯示出來了,有兩張沒有顯示完全,看到設計圖的時候第一反應是
Android中的測試無非是分為兩種:一、在一個工程裡面寫測試代碼。二、專門新建一個工程寫測試代碼。一、在一個工程裡面寫測試代碼步驟:1、寫一個類繼承AndroidTes