Android單元測(cè)試 - Sqlite春叫、SharedPreference、Assets、文件操作 怎么測(cè)暂殖?

photo-1469521669194-babb45599def.jpg

前言

上篇《Android單元測(cè)試 - 幾個(gè)重要問(wèn)題》 講解了“何解決Android依賴价匠、隔離Native方法、靜態(tài)方法央星、RxJava異步轉(zhuǎn)同步”這幾個(gè)Presenter單元測(cè)試中常見(jiàn)問(wèn)題霞怀。如果讀者你消化得差不多,就接著看本篇吧莉给。

在日常開(kāi)發(fā)中毙石,數(shù)據(jù)儲(chǔ)存是必不可少的。例如颓遏,網(wǎng)絡(luò)請(qǐng)求到數(shù)據(jù)徐矩,先存本地,下次打開(kāi)頁(yè)面叁幢,先從本地讀取數(shù)據(jù)顯示滤灯,再?gòu)姆?wù)器請(qǐng)求新數(shù)據(jù)。既然如此重要曼玩,對(duì)這塊代碼進(jìn)行測(cè)試鳞骤,也成為單元測(cè)試的重中之重了。

筆者在學(xué)會(huì)單元測(cè)試前黍判,也像大多數(shù)人一樣豫尽,寫(xiě)好了sql代碼,運(yùn)行app顷帖,報(bào)錯(cuò)了....檢查代碼美旧,修改,再運(yùn)行app....這真是效率太低了贬墩。有了單元測(cè)試做武器后榴嗅,我寫(xiě)DAO代碼輕松了不少,不擔(dān)心出錯(cuò)陶舞,效率也高嗽测。

常用的數(shù)據(jù)儲(chǔ)存有:sqlite、SharedPreference肿孵、Assets论咏、文件。由于這前三種儲(chǔ)取數(shù)據(jù)方式颁井,都必須依賴android環(huán)境厅贪,因此要進(jìn)行單元測(cè)試,不能僅僅用junit & mockito了雅宾,需要另外的單元測(cè)試框架养涮。接下來(lái)葵硕,筆者介紹如何使用robolectric進(jìn)行DAO單元測(cè)試。

縮寫(xiě)解釋:DAO (Data Access Object) 數(shù)據(jù)訪問(wèn)對(duì)象


Robolectric配置

Robolectric官網(wǎng):http://robolectric.org/

Robolectric配置很簡(jiǎn)單的贯吓。

build.gradle

dependencies {
    testCompile "org.robolectric:robolectric:3.1.2"
}

然后在測(cè)試用例XXTest加上注解:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class XXTest {
}

配置代碼是寫(xiě)完了懈凹。

不過(guò),別以為這樣就完了悄谐。Robolectric最麻煩就是下載依賴介评! 由于我們生活在天朝,下載國(guó)外的依賴很慢爬舰,即使有了翻墻们陆,效果也一般。解決辦法《加速Robolectric下載依賴庫(kù)及原理剖析》


Sqlite

DbHelper:

public class DbHelper extends SQLiteOpenHelper {

    private static final int DB_VERSION = 1;

    public DbHelper(Context context, String dbName) {
        super(context, dbName, null, DB_VERSION);
    }
    ...
}

Bean:

public class Bean {
    int id;
    String name = "";

    public Bean(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

Bean數(shù)據(jù)操作類 BeanDAO:

public class BeanDAO {
    static boolean isTableExist;
    
    SQLiteDatabase db;

    public BeanDAO() {
        this.db = new DbHelper(App.getContext(), "Bean").getWritableDatabase();
    }

    /**
     * 插入Bean
     */
    public void insert(Bean bean) {
        checkTable();

        ContentValues values = new ContentValues();
        values.put("id", bean.getId());
        values.put("name", bean.getName());

        db.insert("Bean", "", values);
    }

    /**
     * 獲取對(duì)應(yīng)id的Bean
     */
    public Bean get(int id) {
        checkTable();

        Cursor cursor = null;

        try {
            cursor = db.rawQuery("SELECT * FROM Bean", null);

            if (cursor != null && cursor.moveToNext()) {
                String name = cursor.getString(cursor.getColumnIndex("name"));

                return new Bean(id, name);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
            cursor = null;
        }
        return null;
    }
    
    /**
     * 檢查表是否存在情屹,不存在則創(chuàng)建表
     */
    private void checkTable() {
        if (!isTableExist()) {
            db.execSQL("CREATE TABLE IF NOT EXISTS Bean ( id INTEGER PRIMARY KEY, name )");
        }
    }

    private boolean isTableExist() {
        if (isTableExist) {
            return true; // 上次操作已確定表已存在于數(shù)據(jù)庫(kù)坪仇,直接返回true
        }
        
        Cursor cursor = null;
        try {
            String sql = "SELECT COUNT(*) AS c FROM sqlite_master WHERE type ='table' AND name ='Bean' ";

            cursor = db.rawQuery(sql, null);
            if (cursor != null && cursor.moveToNext()) {
                int count = cursor.getInt(0);
                if (count > 0) {
                    isTableExist = true; // 記錄Table已創(chuàng)建,下次執(zhí)行isTableExist()時(shí)垃你,直接返回true
                    return true;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
            cursor = null;
        }
        return false;
    }
}

以上是你在項(xiàng)目中用到的類椅文,當(dāng)然數(shù)據(jù)庫(kù)一般開(kāi)發(fā)者都會(huì)用第三方庫(kù),例如:greenDAO惜颇、ormlite皆刺、dbflow、afinal凌摄、xutils....這里考慮到代碼演示規(guī)范性羡蛾、通用性,就直接用android提供的SQLiteDatabase望伦。

大家注意到BeanDAO的構(gòu)造函數(shù):

public BeanDAO() {
    this.db = new DbHelper(App.getContext(), "Bean").getWritableDatabase();
}

這種在內(nèi)部創(chuàng)建對(duì)象的方式,不利于單元測(cè)試煎殷。App是項(xiàng)目本來(lái)的Application屯伞,但是使用Robolectric往往會(huì)指定一個(gè)測(cè)試專用的Application(命名為RoboApp,配置方法下面會(huì)介紹)豪直,這么做好處是隔離App的所有依賴劣摇。

隔離原Application依賴

項(xiàng)目原本的App:

public class App extends Application {

    private static Context context;

    @Override
    public void onCreate() {
        super.onCreate();
        context = this;
        
        // 各種第三方初始化,有很多依賴
        ...
    }

    public static Context getContext() {
        return context;
    }
}

而單元測(cè)試使用的RoboApp:

public class RoboApp extends Application {}

如果用Robolectric單元測(cè)試弓乙,不配置RoboApp末融,就會(huì)調(diào)用原來(lái)的App,而App有很多第三方庫(kù)依賴暇韧,常見(jiàn)的有static{ Library.load() }靜態(tài)加載so庫(kù)勾习。于是,執(zhí)行App生命周期時(shí)懈玻,robolectric就報(bào)錯(cuò)了巧婶。

正確配置Application方式,是在單元測(cè)試XXTest加上@Config(application = RoboApp.class)

改進(jìn)DAO類

public class BeanDAO {
    SQLiteDatabase db;

    public BeanDAO(SQLiteDatabase db) {
        this.db = db;
    }
    
    // 可以保留原來(lái)的構(gòu)造函數(shù)艺栈,只是單元測(cè)試不用這個(gè)方法而已
    public BeanDAO() {
        this.db = new DbHelper(App.getContext(), "Bean").getWritableDatabase();
    }

單元測(cè)試

DAOTest

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class)
public class DAOTest {

    BeanDAO dao;

    @Before
    public void setUp() throws Exception {
        // 用隨機(jī)數(shù)做數(shù)據(jù)庫(kù)名稱英岭,讓每個(gè)測(cè)試方法,都用不同數(shù)據(jù)庫(kù)湿右,保證數(shù)據(jù)唯一性
        DbHelper       dbHelper = new DbHelper(RuntimeEnvironment.application, new Random().nextInt(1000) + ".db");
        SQLiteDatabase db       = dbHelper.getWritableDatabase();

        dao = new BeanDAO(db);
    }

    @Test
    public void testInsertAndGet() throws Exception {
        Bean bean = new Bean(1, "鍵盤(pán)男");

        dao.insert(bean);

        Bean retBean = dao.get(1);

        Assert.assertEquals(retBean.getId(), 1);
        Assert.assertEquals(retBean.getName(), "鍵盤(pán)男");
    }
}

DAO單元測(cè)試跟Presenter有點(diǎn)不一樣诅妹,可以說(shuō)會(huì)更簡(jiǎn)單、直觀毅人。Presenter單元測(cè)試會(huì)用mock去隔離一些依賴吭狡,并且模擬返回值,但是sqlite執(zhí)行是真實(shí)的堰塌,不能mock的赵刑。

正常情況,insert()get()應(yīng)該分別測(cè)試场刑,但這樣非常麻煩般此,必然要在測(cè)試用例寫(xiě)sqlite語(yǔ)句,并且對(duì)SQLiteDatabase 操作牵现☆戆茫考慮到數(shù)據(jù)庫(kù)操作的真實(shí)性,筆者把insertget放在同一個(gè)測(cè)試用例:如果insert()失敗瞎疼,那么get()必然拿不到數(shù)據(jù)科乎,testInsertAndGet()失敗贼急;只有insert()get()代碼都正確茅茂,testInsertAndGet()才能通過(guò)

Insert and Get Pass

由于用Robolectric太抓,所以單元測(cè)試要比直接junit要慢空闲。僅junit跑單元測(cè)試,耗時(shí)基本在毫秒(ms)級(jí)走敌,而robolectric則是秒級(jí)(s)碴倾。不過(guò)怎么說(shuō)也比跑真機(jī)、模擬器的單元測(cè)試要快很多掉丽。


SharedPreference

其實(shí)跌榔,SharedPreference道理跟sqlite一樣,也是對(duì)每個(gè)測(cè)試用例創(chuàng)建單獨(dú)SharedPreference捶障,然后保存僧须、查找一起測(cè)。

ShareDAO:

public class ShareDAO {
    SharedPreferences        sharedPref;
    SharedPreferences.Editor editor;

    public ShareDAO(SharedPreferences sharedPref) {
        this.sharedPref = sharedPref;
        this.editor = sharedPref.edit();
    }

    public ShareDAO() {
        this(App.getContext().getSharedPreferences("myShare", Context.MODE_PRIVATE));
    }

    public void put(String key, String value) {
        editor.putString(key, value);
        editor.apply();
    }

    public String get(String key) {
        return sharedPref.getString(key, "");
    }
}

單元測(cè)試ShareDAOTest

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class)
public class ShareDAOTest {

    ShareDAO shareDAO;

    @Before
    public void setUp() throws Exception {
        String name = new Random().nextInt(1000) + ".pref";

        shareDAO = new ShareDAO(RuntimeEnvironment.application.getSharedPreferences(name, Context.MODE_PRIVATE));
    }

    @Test
    public void testPutAndGet() throws Exception {
        shareDAO.put("key01", "stringA");

        String value = shareDAO.get("key01");

        Assert.assertEquals(value, "stringA");
    }
}
SharePreference單元測(cè)試通過(guò)

測(cè)試通過(guò)了项炼。是不是很簡(jiǎn)單皆辽?

請(qǐng)繼續(xù)看《SharePreference單元測(cè)試超級(jí)簡(jiǎn)單柑蛇!》,不需要robolectric驱闷,單元測(cè)試跑更快耻台!


Assets

Robolectric對(duì)Assets支持也是相當(dāng)不錯(cuò)的,測(cè)Assets道理也是跟sqlite空另、sharePreference相同盆耽。

/assets/test.txt:

success
public class AssetsReader {

    AssetManager assetManager;

    public AssetsReader(AssetManager assetManager) {
        this.assetManager = assetManager;
    }

    public AssetsReader() {
        assetManager = App.getContext()
                          .getAssets();
    }

    public String read(String fileName) {
        try {
            InputStream inputStream = assetManager.open(fileName);

            StringBuilder sb = new StringBuilder();

            byte[] buffer = new byte[1024];

            int hasRead;

            while ((hasRead = inputStream.read(buffer, 0, buffer.length)) > -1) {
                sb.append(new String(buffer, 0, hasRead));
            }

            inputStream.close();

            return sb.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }
}

單元測(cè)試AssetsReaderTest:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class)
public class AssetsReaderTest {

    AssetsReader assetsReader;

    @Before
    public void setUp() throws Exception {
        assetsReader = new AssetsReader(RuntimeEnvironment.application.getAssets());
    }

    @Test
    public void testRead() throws Exception {
        String value = assetsReader.read("test.txt");

        Assert.assertEquals(value, "success");
    }
}
Assets單元測(cè)試通過(guò)

通過(guò)了通過(guò)了,非常簡(jiǎn)單扼菠!


文件操作

日常開(kāi)發(fā)中摄杂,文件操作相對(duì)比較少。由于通常都在真機(jī)測(cè)試循榆,有時(shí)目錄析恢、文件名有誤導(dǎo)致程序出錯(cuò),還是挺煩人的秧饮。所以映挂,筆者教大家在本地做文件操作單元測(cè)試。

Environment.getExternalStorageDirectory()

APP運(yùn)行時(shí)盗尸,通過(guò)Environment.getExternalStorageDirectory()等方法獲取android儲(chǔ)存目錄柑船,因此,只要我們改變Environment.getExternalStorageDirectory()返回的目錄泼各,就可以在單元測(cè)試時(shí)鞍时,讓jvm寫(xiě)操作指向本地目錄。

《Android單元測(cè)試 - 幾個(gè)重要問(wèn)題》 介紹過(guò)如何解決android.text.TextUtils依賴扣蜻,那么android.os.Environment也是故伎重演:

test/java目錄下逆巍,創(chuàng)建android/os/Environment.java

package android.os;

public class Environment {
    public static File getExternalStorageDirectory() {
        return new File("build");// 返回src/build目錄
    }
}
Environment.java

Context.getCacheDir()

如果你是用contexnt.getCacheDir()getFilesDir()等莽使,那么只需要使用RuntimeEnvironment.application就行锐极。

代碼

寫(xiě)完android.os.Environment,我們離成功只差一小步了吮旅。FileDAO:

public class FileDAO {

    Context context;

    public FileDAO(Context context) {
        this.context = context;
    }
    
    public void write(String name, String content) {
        File file = new File(getDirectory(), name);

        if (!file.getParentFile().exists()) {
            file.getParentFile().mkdirs();
        }
        try {
            FileWriter fileWriter = new FileWriter(file);

            fileWriter.write(content);
            fileWriter.flush();
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String read(String name) {
        File file = new File(getDirectory(), name);

        if (!file.exists()) {
            return "";
        }

        try {
            FileReader reader = new FileReader(file);

            StringBuilder sb = new StringBuilder();

            char[] buffer = new char[1024];
            int    hasRead;

            while ((hasRead = reader.read(buffer, 0, buffer.length)) > -1) {
                sb.append(new String(buffer, 0, hasRead));
            }
            reader.close();

            return sb.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

    public void delete(String name) {
        File file = new File(getDirectory(), name);

        if (file.exists()) {
            file.delete();
        }
    }

    protected File getDirectory() {
        // return context.getCacheDir();
        return Environment.getExternalStorageDirectory();
    }
}

FileDAO單元測(cè)試

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class)
public class FileDAOTest {

    FileDAO fileDAO;

    @Before
    public void setUp() throws Exception {
        fileDAO = new FileDAO(RuntimeEnvironment.application);
    }

    @Test
    public void testWrite() throws Exception {
        String name = "readme.md";

        fileDAO.write(name, "success");

        String content = fileDAO.read(name);

        Assert.assertEquals(content, "success");

        // 一定要?jiǎng)h除測(cè)試文件溪烤,保留的文件會(huì)影響下次單元測(cè)試
        fileDAO.delete(name);
    }
}
File單元測(cè)試通過(guò)

注意味咳,用Environment.getExternalStorageDirectory()是不需要robolectric的庇勃,直接junit即可;而context.getCacheDir()需要robolectric槽驶。


小技巧

如果你嫌麻煩每次都要寫(xiě)@RunWith(RobolectricTestRunner.class)&@Config(...)责嚷,那么可以寫(xiě)一個(gè)基類:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class)
public class RoboCase {

    protected Context getContext() {
        return RuntimeEnvironment.application;
    }
}

然后,所有使用robolectric的測(cè)試用例掂铐,直接繼承RoboCase即可罕拂。


小結(jié)

我想揍异,大家應(yīng)該感覺(jué)到,Sqlite爆班、SharedPreference衷掷、Assets、文件操作幾種單元測(cè)試柿菩,形式都差不多戚嗅。有這種感覺(jué)就對(duì)了,舉一反三枢舶。

本篇文字描述不多懦胞,代碼比例較大,相信讀者能看懂的凉泄。

如果讀者對(duì)Presenter躏尉、DAO單元測(cè)試運(yùn)用自如涯保,那應(yīng)該跟筆者水平相當(dāng)了弊决,哈哈哈。下一篇會(huì)介紹如何優(yōu)雅地測(cè)試傳參對(duì)象缕坎,敬請(qǐng)期待吼具!


關(guān)于作者

我是鍵盤(pán)男僚纷。
在廣州生活,在創(chuàng)業(yè)公司上班拗盒,猥瑣文藝碼農(nóng)怖竭。喜歡科學(xué)、歷史陡蝇,玩玩投資痊臭,偶爾獨(dú)自旅行。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末登夫,一起剝皮案震驚了整個(gè)濱河市广匙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌恼策,老刑警劉巖鸦致,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異涣楷,居然都是意外死亡分唾,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)狮斗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)绽乔,“玉大人,你說(shuō)我怎么就攤上這事碳褒≌墼遥” “怎么了看疗?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)睦授。 經(jīng)常有香客問(wèn)我两芳,道長(zhǎng),這世上最難降的妖魔是什么去枷? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任盗扇,我火速辦了婚禮,結(jié)果婚禮上沉填,老公的妹妹穿的比我還像新娘疗隶。我一直安慰自己,他們只是感情好翼闹,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布斑鼻。 她就那樣靜靜地躺著,像睡著了一般猎荠。 火紅的嫁衣襯著肌膚如雪坚弱。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,785評(píng)論 1 314
  • 那天关摇,我揣著相機(jī)與錄音荒叶,去河邊找鬼。 笑死输虱,一個(gè)胖子當(dāng)著我的面吹牛些楣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播宪睹,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼愁茁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了亭病?” 一聲冷哼從身側(cè)響起鹅很,我...
    開(kāi)封第一講書(shū)人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎罪帖,沒(méi)想到半個(gè)月后促煮,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡整袁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年菠齿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片葬项。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泞当,死狀恐怖迹蛤,靈堂內(nèi)的尸體忽然破棺而出民珍,到底是詐尸還是另有隱情襟士,我是刑警寧澤,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布嚷量,位于F島的核電站陋桂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蝶溶。R本人自食惡果不足惜嗜历,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望抖所。 院中可真熱鬧梨州,春花似錦、人聲如沸田轧。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)傻粘。三九已至每窖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間弦悉,已是汗流浹背窒典。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留稽莉,地道東北人瀑志。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像污秆,于是被迫代替她去往敵國(guó)和親后室。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,336評(píng)論 25 707
  • afinalAfinal是一個(gè)android的ioc混狠,orm框架 https://github.com/yangf...
    passiontim閱讀 15,441評(píng)論 2 45
  • 前言 在之前的系列博客中岸霹,主要圍繞的是測(cè)試工具的介紹與使用。經(jīng)過(guò)幾個(gè)月的沉寂将饺,在項(xiàng)目中摸索與實(shí)踐單元測(cè)試贡避,曾經(jīng)踩坑...
    水木飛雪閱讀 2,843評(píng)論 0 8
  • 今天讀了一篇文章,講的是關(guān)于產(chǎn)品如何做用戶激勵(lì)予弧。作者用一句話把用戶激勵(lì)總結(jié)的非常到位:用戶使用產(chǎn)品刮吧,如果說(shuō)用戶需求...
    人間四月天zjs閱讀 264評(píng)論 0 0
  • 清明節(jié)放假回家,這兩天天氣還不錯(cuò)掖蛤,第一天大晴天杀捻,自己本想出去逛逛,卻發(fā)現(xiàn)沒(méi)地方去蚓庭,也沒(méi)有人出去玩致讥,后面就這樣呆在家...
    陌如煙雨淡如塵閱讀 266評(píng)論 0 0