1. 背景介紹:
在系統monkey測試和案例測試中,我們在dropbox發現了大量的關於strictmode嚴苛模式的報錯,爲了增強系統穩定性,我們打算在項目初期就把這些類型的報錯提給開發,來解決。對此本文寫了StrictMode(VmPolicy)類型的demo以供大家粗粗略瞭解StrictMode。
2. StrictMode 詳解:
StrictMode 通過策略方式來讓你自定義需要檢查哪方面的問題。 主要有兩中策略,一個時線程方策略- (ThreadPolicy),一個是VM方面的策略(VmPolicy)。
ThreadPolicy 主要用於發現在UI線程中是否有讀寫磁盤的操作,是否有網絡操作,以及檢查UI線程中調用的自定義代碼是否執行得比較慢。
VmPolicy,主要用於發現內存問題,比如 Activity內存泄露, SQL 對象內存泄露, 資源未釋放,能夠限定某個類的最大對象數。
3. StrictMode(VmPolicy)默認檢查的錯誤類型主要有以下五種可以自由組合:
.detectAll()
.detectActivityLeaks()
.detectFileUriExposure()
.detectLeakedClosableObjects()
.detectLeakedRegistrationObjects()
.detectLeakedSqlLiteObjects()
4.StrictMode(VmPolicy)Penalty 違規後的懲罰方式有以下三種可以自由組合:
.penaltyDropBox()
.penaltyDeath()
.penaltyLog()
5. 現寫了上述檢查的五種錯誤類型的demo,界面如下:
6. 下面介紹主要的代碼:
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.StrictMode;
import android.os.SystemClock;
import android.provider.Contacts;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
@SuppressLint("HandlerLeak")
@SuppressWarnings("deprecation")
public class StrictModeDemo extends Activity implements View.OnClickListener {
private static final String TAG = "StrictModeActivity";
private static final boolean DEBUG_MODE = true;
private static final int MSG_GET_DATA_OK = 100;
private Context mContext;
private TextView mTextView;
private Button mCursorLeakButton;
private Button mClosableLeakButton;
private Button mActivityLeakButton;
private Button mRegistrationsLeakButton;
private Button mClassInstanceLimitButton;
private Thread mThread = null;
private TestStrictModeBroadcast mReceiver = null;
private List<ClassCounts> mClassList = new ArrayList<StrictModeDemo.ClassCounts>();
private static class ClassCounts{
/**No Content, Only used test!*/
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_GET_DATA_OK:
String result = (String) msg.obj;
if (TextUtils.isEmpty(result)) {
result = "抱歉,手機沒有聯繫人!";
}
mTextView.setText(result);
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//開啓StrictMode模式
useVMStrictMode();
init();
}
@Override
protected void onDestroy() {
// unregisterReceiver(this.receiver);我們在這裏故意沒有釋放掉receiver
mHandler.removeCallbacksAndMessages(null);
if (mThread != null && mThread.isAlive()) {
try {
mThread.stop();
} catch (Exception e) {}
}
super.onDestroy();
}
//使用線程,可能耗時
Runnable runnable = new Runnable() {
@Override
public void run() {
//檢查cursorLeak,註冊SQlite Cursor,沒有調用close
String result = cursorLeakUncloseTest();
mHandler.sendMessage(mHandler.obtainMessage(MSG_GET_DATA_OK, result));
}
};
//初始化一些按鍵之類
private void init() {
mContext = this;
mTextView = (TextView) this.findViewById(R.id.content);
mCursorLeakButton = (Button)this.findViewById(R.id.cursorLeak);
mClosableLeakButton = (Button)this.findViewById(R.id.closableLeak);
mActivityLeakButton = (Button)this.findViewById(R.id.activityLeak);
mRegistrationsLeakButton = (Button)this.findViewById(R.id.RegistrationObjectsLeak);
mClassInstanceLimitButton = (Button)this.findViewById(R.id.setClassInstanceLimit);
mCursorLeakButton.setOnClickListener(this);
mClosableLeakButton.setOnClickListener(this);
mActivityLeakButton.setOnClickListener(this);
mRegistrationsLeakButton.setOnClickListener(this);
mClassInstanceLimitButton.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.cursorLeak:
Log.i(TAG,"----------->onClick");
mTextView.setText("正在讀取聯繫人數據庫,測試Cursor泄露,測試結果請查看: data/system/dropbox請等待...");
mThread = new Thread(runnable);
mThread.start();
break;
case R.id.closableLeak:
mTextView.setText("測試closableLeak,測試結果請查看data/system/dropbox");
File newxmlfile = new File(Environment.getExternalStorageDirectory(), "hello.txt");
try {
newxmlfile.createNewFile();
FileWriter fw = new FileWriter(newxmlfile);
fw.write("hellohellohello");
//fw.close(); 我們在這裏故意沒有關閉 fw
} catch (IOException e) {
e.printStackTrace();
}
break;
case R.id.activityLeak:
mTextView.setText("測試Activity泄漏,請點擊物理Back退出應用或者旋轉屏幕(由於嚴苛模式所以可能會異常崩潰),該項測試完畢後需要殺掉進程重新開啓,測試結果請查看: data/system/dropbox");
new Thread() {
@Override
public void run() {
while (true) {
SystemClock.sleep(1000);
}
}
}.start();
break;
case R.id.RegistrationObjectsLeak:
mTextView.setText("測試RegistrationObjects泄露,檢查BroadcastReceiver 或者 ServiceConnection 註冊類對象是否被正確釋放。測試結果請查看data/system/dropbox");
this.mReceiver = new TestStrictModeBroadcast();
IntentFilter filter = new IntentFilter();
filter.addAction("android.intent.action.STRICT_MODE");
registerReceiver(this.mReceiver, filter);
break;
case R.id.setClassInstanceLimit:
mTextView.setText("測試setClassInstanceLimit,設置某個類的同時處於內存中的實例上限,可以協助檢查內存泄露。測試結果請查看data/system/dropbox");
for (int index=0; index<8; index++) {
mClassList.add(new ClassCounts());
}
break;
}
}
//檢查cursorLeak,註冊SQlite Cursor,沒有調用close
private String cursorLeakUncloseTest() {
StringBuilder builder = new StringBuilder("");
ContentResolver resolver = getContentResolver();
//插入20個聯繫人
for (int i = 1; i < 20; i++) {
insertContact("contact" + i, "email" + i, "123" + i);
}
//搜索聯繫人
Cursor cursor = resolver.query(Contacts.Phones.CONTENT_URI, null, null,null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndex(Contacts.Phones.NAME));
builder.append(name);
builder.append("\n");
}
//cursor.close();我們在這裏故意沒有關閉 cursor
}
return builder.toString();
}
//開啓StrictMode模式
private void useVMStrictMode() {
if(DEBUG_MODE){
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyDropBox()
.penaltyDeath()
.penaltyLog()
.setClassInstanceLimit(ClassCounts.class, 2)
.build());
}
}
// 插入聯繫人
protected boolean insertContact(String given_name, String work_email,String mobile_number) {
try {
ContentValues values = new ContentValues();
Uri rawContactUri = getContentResolver().insert(RawContacts.CONTENT_URI, values);
long rawContactId = ContentUris.parseId(rawContactUri);
// 向data表插入姓名數據
if (given_name != "") {
values.clear();
values.put(Data.RAW_CONTACT_ID, rawContactId);
values.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
values.put(StructuredName.GIVEN_NAME, given_name);
getContentResolver().insert(ContactsContract.Data.CONTENT_URI,values);
}
if (mobile_number != null) {
values.clear();
values.put(Data.RAW_CONTACT_ID, rawContactId);
values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
values.put(Phone.TYPE, Phone.TYPE_MOBILE);
values.put(Phone.NUMBER, mobile_number);
getContentResolver().insert(ContactsContract.Data.CONTENT_URI,values);
}
// 向data表插入Email數據
if (work_email != "") {
values.clear();
values.put(Data.RAW_CONTACT_ID, rawContactId);
values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
values.put(Email.DATA, work_email);
values.put(Email.TYPE, Email.TYPE_WORK);
getContentResolver().insert(ContactsContract.Data.CONTENT_URI,values);
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
上面代碼需要如下權限:
android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
上面就是整個測試Demo的主要代碼,可以發現,主要是打開了嚴苛模式,然後模擬了上述背景介紹中存在的五種錯誤,譬如查詢數據庫用完Cursor後忘了關閉、註冊廣播後沒有取消註冊等,相對來說都是些比較常見卻很容易漏寫的錯誤。
7.Demo運行分析
有了上面Demo以後我們就可以運行程序進行嚴苛模式發現問題的模擬測試,下面我們主要以寫入dropbox和log打印來簡要介紹如何查看嚴苛模式探測的問題。
下面主要介紹下五種錯誤類型在dropbox裏的log打印和解決方法。
7.1 點擊第一個按鈕->檢查CursorLeak錯誤:
當點擊Demo第一個按鈕後由於我們在代碼中模擬沒有關閉Cursor,所以此時嚴苛模式會在dropbox中進行如下log打印:
System-App: false
Uptime-Millis: 14075239//!!!!!!!!嚴苛模式最核心的關注點log!!!!!!!!
java.lang.Throwable: Explicit termination method 'close' not called
at dalvik.system.CloseGuard.open(CloseGuard.java:184)
at java.io.FileOutputStream.<init>(FileOutputStream.java:89)
at java.io.FileOutputStream.<init>(FileOutputStream.java:72)
at java.io.FileWriter.<init>(FileWriter.java:42)
at com.meizu.strictmode.StrictModeActivity.onClick(StrictModeActivity.java:133)
at android.view.View.performClick(View.java:4851)
at android.view.View$PerformClick.run(View.java:20016)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5424)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:947)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:742)
通過上面Log就可以很容易發現問題所在點,不僅給出了潛在錯誤描述,還給出了問題出現的大致位置,這樣一來就很容易定位問題,而不至於浪費很多時間去盲目查找,或者壓根沒發現自己忘了調close方法。
通過Log可以發現該類問題的解決方法就是成對出現,關閉Cursor;關閉後再此運行該問題不會再被嚴苛模式發現。
點擊第二個按鈕->檢查closableLeak
7.2 當點擊Demo第二個按鈕時我們模擬了文件讀寫,但是沒有調運colse方法的情況,此時會得到如下主要log(獲取方法同上):
java.lang.Throwable: Explicit termination method 'close' not called
at dalvik.system.CloseGuard.open(CloseGuard.java:184)
at java.io.FileOutputStream.<init>(FileOutputStream.java:89)
at java.io.FileOutputStream.<init>(FileOutputStream.java:72)
at java.io.FileWriter.<init>(FileWriter.java:42)
at com.meizu.strictmode.StrictModeActivity.onClick(StrictModeActivity.java:133)
at android.view.View.performClick(View.java:4851)
at android.view.View$PerformClick.run(View.java:20016)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5424)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:947)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:742)
可以發現,上面也給出了類似第一個按鈕的錯誤Log,我們一樣可以通過Log分析很快定位解決潛在的問題。
7.3 點擊第三個按鈕->檢查activityLeak:
和上面兩種類似,我們直接給出錯誤log,如下:
Instance-Count: 2android.os.StrictMode$InstanceCountViolation: class com.meizu.strictmode.StrictModeActivity; instances=2; limit=1
at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
可以看見,這類潛在錯誤出現後嚴苛模式一樣可以給出相關信息,只是沒有上面幾種明顯,但是最起碼可以告訴我們哪個Activity存在內存泄漏,這已經幫忙我們縮小了問題範圍,接下來我們就可以藉助其他內存泄漏檢測工具進行詳細的定位分析解析即可。
7.4 點擊第四個按鈕->檢查RegistrationObjectsLeak:
與上面類似,直接給出錯誤log,如下:
android.app.IntentReceiverLeaked: Activity com.meizu.strictmode.StrictModeActivity has leaked IntentReceiver com.meizu.strictmode.TestStrictModeBroadcast@345916f7 that was originally registered here. Are you missing a call to unregisterReceiver()?
分析方法與上面類似,不在累贅。
7.5 點擊第五個按鈕->檢查setClassInstanceLimit :
同上,錯誤log如下:
android.os.StrictMode$InstanceCountViolation: class com.meizu.strictmode.StrictModeActivity$ClassCounts; instances=16; limit=2
at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
可以看見,錯誤原因是開啓了嚴苛模式,我們設置了一個類創建對象個數的上限,但卻沒有遵守,所以解決方案就是設置了一個類創建對象個數的上限後就不要超出上限。
8、嚴苛模式總結
有了上面的背景知識和Demo演示及Demo的問題Log分析後我們可以歸納總結如下:
嚴苛模式的意義:
我們平時寫代碼可能會由於一時疏忽等造成了代碼存在潛在問題,但是我們代碼又能正常執行,直觀感覺沒啥問題,所以就沒再搭理,但是我們又不能保證終 端用戶和我們一樣用不出問題(譬如我們直接在UI線程查一個理論認爲很快的東西,我們測試環境可能又沒能覆蓋到這種查詢的極限情況—-一直查不到,終端用 戶偶爾卻有達到了這種極限,這種情況就是我們考慮疏忽導致功能ANR的潛在原因。),所以我們必須要想辦法將代碼儘量性能與穩定性達標,嚴苛模式的開啓就 是讓一些潛在問題暴漏在開發階段的利器。
嚴苛模式的使用:
上面背景與Demo都有介紹,基本就是在開發階段的代碼中進行嚴苛模式的配置,然後勤看logcat打印與dropbox下的文件,發現一個消滅一個即可