Android CR 案例集 & 最佳實踐

最近兩個月一直在做團隊 CR Owner 機制的落地隔盛,以及 CR 氛圍和文化的提升,對于 CR 逐漸有了一些更深的理解以及可落地的方案

個人理解弥雹,Code Review 是為了找出代碼中「理想」和「現(xiàn)實」之間的差距焕盟,所以如何把 CR 做好铝阐,其實就可以拆解成兩個問題

  • 理想的代碼究竟是怎樣的,也就是所謂的最佳實踐
  • 如何找出代碼中理想和現(xiàn)實的差距删壮,我給出的答案是從日常的 CR 活動中形成一份 CR 案例集

于是便有了這篇文章贪绘,希望從平常的 CR 活動中收集最常見問題和改進方案,以及 Android 中可落地的最佳實踐央碟,形成一份極佳的 CR 案例集供開發(fā)者和 reviewer 參考税灌,并給新同學一些指引和借鑒

一、CR 中常見的問題

1亿虽、 代碼規(guī)范

建議閱讀:Java 編碼規(guī)范

1. 代碼之間沒有適當?shù)目崭?/h4>

代碼之間需要有適當?shù)目崭窳獾樱磥硪哺邮娣ㄗh寫完隨手格式化一下

// Don't
public static void startIMMessageListActivity(Context context){
    if (context!= null){
        Intent intent =new Intent(context, IMMessageListActivity.class);
        PluginLoader.getInstance().startPluginActivity(context,enterCallback);
    }
}

// Do
public static void startIMMessageListActivity(Context context) {
    if (context != null) {
        Intent intent = new Intent(context, IMMessageListActivity.class);
        PluginLoader.getInstance().startPluginActivity(context, enterCallback);
    }
}

2. 使用魔法數(shù)

魔法數(shù)字(魔法數(shù)值)是代碼中未經(jīng)預先定義而直接出現(xiàn)的數(shù)值
(1)盡量避免使用魔法數(shù)字洛勉,應代之有名字的常量或枚舉
(2)原則上代碼中直接出現(xiàn)的數(shù)值就是魔法數(shù)字粘秆, 經(jīng)常被用作下標和初始值的 0 除外
(3)禁止出現(xiàn)相同數(shù)值的魔法數(shù)字兩次

// Don't
private fun initLoadingView() {
    with(binding.qqGroupLoadingView) {
        if (qqGroupSize > 4) {
            layoutParams.height = DensityUtils.dp2px(context, 277f)
        } else {
            val height = qqGroupSize * 65f
            layoutParams.height = DensityUtils.dp2px(context, height)
        }
        this.layoutParams = layoutParams
    }
}

// Do
private fun initLoadingView() {
    with(binding.qqGroupLoadingView) {
        if (qqGroupSize > THRESHOLD_QQ_GROUP_LIST) {
            layoutParams.height = DensityUtils.dp2px(context, MAX_HEIGHT_QQ_GROUP_LIST)
        } else {
            val height = qqGroupSize * HEIGHT_QQ_GROUP_ITEM
            layoutParams.height = DensityUtils.dp2px(context, height)
        }
        this.layoutParams = layoutParams
    }
}

3. 大塊的注釋代碼

廢棄代碼建議直接刪除掉,后續(xù)想找回收毫,回溯 Git 的 history 即可

// Don't
fun onPersonProfileEvent(event: PersonProfileEvent) {
    when (event.code) {
        PersonProfileEvent.EVENT_UPDATE_QQ_GROUP -> refresh()
//        PersonProfileEvent.EVENT_UPDATE_QQ_GROUP_NAME -> {
//            joinSlogan = event.qqGroupName
//            setQQGroupIntroduction()
//        }
        PersonProfileEvent.EVENT_TOGGLE_QQ_GROUP_LIST -> {
            dismissAllowingStateLoss()
        }
    }
}

// Do
fun onPersonProfileEvent(event: PersonProfileEvent) {
    when (event.code) {
        PersonProfileEvent.EVENT_UPDATE_QQ_GROUP -> refresh()
        PersonProfileEvent.EVENT_TOGGLE_QQ_GROUP_LIST -> {
            dismissAllowingStateLoss()
        }
    }
}

4. 代碼中存在大量的 warning

代碼開發(fā)完成后攻走,建議 check 下增量代碼中所有的 warning殷勘,盡量做到 0 warning

// Don't
android:layout_marginLeft="22dp"
android:layout_marginRight="15dp"

moduleDirector.getSubModuleVideoSelector().setOnBackListener(new OnBackListener() {
    @Override
    public void onBackClick() {
        moduleDirector.openCollectionVideo(getContext(), getCurrentViewHolder());
    }
});

StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("mInsertFeed.id=" + mInsertFeed.id);
stringBuilder.append(", mInsertFeed.feed_desc=" + mInsertFeed.feed_desc);

// Do
android:layout_marginStart="22dp"
android:layout_marginEnd="15dp"

moduleDirector.getSubModuleVideoSelector().setOnBackListener(() ->
        moduleDirector.openCollectionVideo(getContext(), getCurrentViewHolder()));

StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("mInsertFeed.id=").append(mInsertFeed.id)
        .append(", mInsertFeed.feed_desc=").append(mInsertFeed.feed_desc)     

2、 業(yè)務邏輯

1. 異常邏輯沒有處理

異常邏輯建議增加日志昔搂,方便后續(xù)定位問題玲销,或者對異常邏輯進行上報,觀察問題的數(shù)量級

// Don't
private fun onStartProfileActivity(personId: String?) {
    if (personId.isNullOrEmpty()) {
        return
    }
    ...
}

private fun onCreate() {
    if (Router.getService(LoginService.class).getCurrentUser() == null ) {
        return;
    }
    ...
}

// Do
private fun onStartProfileActivity(personId: String?) {
    if (personId.isNullOrEmpty()) {
        Logger.i(TAG, "personId is null or empty")
        return
    }
    ...
}

private fun onCreate(Bundle savedInstanceState) {
    if (Router.getService(LoginService.class).getCurrentUser() == null) {
        WSErrorReporter.reportError(module, subModule, errorName);
        return;
    }
    ...
}

2. 重復造輪子

大部分的工具類端內(nèi)基本都有摘符,開發(fā)需求之前建議先搜索一波痒玩,直接使用或者進行拓展

// Don't
class Utils {
    public int dp2px(Float dp) {
        return (int) (dp * sDensity);
    }
}

class DisplayUtils {
    public float dpToPx(Context context, float dp) {
        float density = context.getResources().getDisplayMetrics().density;
        return dp * density;
    }
}

class ViewUtils {
    public static int dpToPx(float dp) {
        return DensityUtils.dp2px(GlobalContext.getContext(), dp);
    }
}

// Do
class DensityUtils {
    public static int dp2px(Context context, float dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal,
               context.getResources().getDisplayMetrics());
    }
}

3、 性能影響

1. entryKey 進行遍歷

如果需要對 map 進行遍歷并獲取 value议慰,建議直接通過 map.entries蠢古,而不是獲取 map.keys 之后,再遍歷獲取 value

// Don't
val map = mapOf< String, String>()
val keySet = map.keys
for (key in keySet) {
    Log.i(TAG, "value: ${map[key]}")
}

// Do
val map = mapOf< String, String>()
for (entry in map.entries) {
    Log.i(TAG, "value: ${entry.value}")
}

2. 使用 ?. 替代 !!

在 Kotlin 中盡量少使用 !!别凹,建議可以用 ?. 避免空指針異常

// Don't
ivAvatar = getChildView("single_feed_iv_avatar")!!.viewNative as AvatarViewV2
tvName = getChildView("single_feed_tv_name")!!.viewNative as TextView

// Do
ivAvatar = getChildView("single_feed_iv_avatar")?.viewNative as? AvatarViewV2
tvName = getChildView("single_feed_tv_name")?.viewNative as? TextView

3. 頻繁的進行日志打印

雖然進行日志打印是個好習慣草讶,頻繁的進行日志打印則會影響 App 的流暢度

// Don't
@Override
fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
    val layoutManager = recyclerView.layoutManager
    Log.i(RecommendPageFragment.TAG, "onScrolled: dx $dx dy $dy")
}

4. 直接 import *

不要出現(xiàn)類似這樣的 import 語句:import java.util.* ,保持 import 的整潔并盡可能避免歧義

// Don't
import android.os.*

// Do
import android.os.Bundle;
import android.os.Message; 

4炉菲、 單測相關

1. 沒有進行規(guī)范命名

  • 測試類命名:ClassNameTest
  • 測試方法命名:testClassMethodName_CaseName
// Don't
class TransferMonitor {
    @Test
    fun needNetProbe_when_network_error() {}
}


// Do
class TransferMonitorTest {
    @Test
    fun testNeedNetProbe_WhenNetworkError() {}
}

2. mock 之后堕战,沒有在 @AfterAll 中進行 unmock

// Don't
@AfterAll
fun tearDown() {
    // nothing
}

// Do
@AfterAll
fun tearDown() {
    unmockkAll()
    Router.unregisterAllService()
}

3. 使用 Kotlin assert 或 Junit4 / 5 assert 進行測試

單元測試,建議統(tǒng)一使用 Kotlin + Junit 5 + Truth拍霜,代碼簡潔嘱丢、可讀性高而且運行速度快

  • Kotlin assert:接口單一、失敗信息可讀性差
  • Junit4 / 5 assert:接口使用不清晰祠饺、失敗信息可讀性一般越驻、易誤解
  • Truth:接口豐富、一致性高道偷、失敗信息可讀性高
// Don't
val actual = "foo"
assert(actual == "bar")

val actual = "foo"
Assertions.assertEquals(actual, "bar")

// Do
val actual = "foo"
Truth.assertThat(actual).hasLength(3)

4. 測試用例里面測試多種條件

每個測試用例只測一種條件缀旁,如果有比較多的 case,建議使用分組測試勺鸦、參數(shù)化測試

// Don't
@Test
fun testNeedNetProbe() {
    var task = TransferMonitorTask(1, "cmd", 10, Job())
    task.addStage(TransferStageFlag.STAGE_TRANSFER_START, System.currentTimeMillis())
    assertTrue(monitor.needNetProbe(task, false))
    assertTrue(!monitor.needNetProbe(task, true))
}

// Do
@Nested
inner class NeedNetProbe {
    @Test
    fun testNeedNetProbe_WhenNetworkError() {}
    
    @Test
    fun testNeedNetProbe_WhenNonNetworkError() {}
    
    @Test
    fun testNeedNetProbe_WhenNetworkTakeHugeTime() {}
}

5. 使用接口隔離依賴接口而不是具體的類

使用接口隔離可以使我們的代碼可測性更強并巍,而且有效減少 mock,降低單測耗時

// Don't
public WnsEnvironmentSubServiceImpl(WnsClient wnsClient) {
    mWnsClient = wnsClient;
}

@Test
fun testGetIpString() {
    val sp = mock<SharedPreferences>().apply {
        every { edit() } returns mockk()
    }
    mockkStatic(Global::class)
    every { Global.currentProcessName() } retusn ""
    every { Global.getSharedPreferences(any(), any()) } returns sp
    
    val impl = WnsEnvironmentSubServiceImpl(WnsClient(Client()))
    ...
}

// Do
public WnsEnvironmentSubServiceImpl(IWnsClientWrapper wnsClientWrapper) {
    mWnsClient = wnsClient;
}

class WnsClientWrapperStub : IWnsClientWrapper {
    ...
}

@Test 
fun testGetIpString() {
    val impl = WnsEnvironmentSubServiceImpl(WnsClientWrapperStub())
    ...
}

二换途、Android 最佳實踐

1懊渡、異常處理

1. 【強制】可以通過預檢查規(guī)避的 RuntimeException 不應該通過 catch 方式來處理

例如,NullPointerException军拟,IndexOutOfBoundsException 不要用 try catch 來進行處理剃执。無法通過預檢查的異常除外,比如吻谋,在解析字符串形式的數(shù)字時忠蝗,可能存在數(shù)字格式錯誤,不得不通過 catch NumberFormatException 來實現(xiàn)漓拾。

// Don't
try { 
    obj.method(); 
} catch (NullPointerException e) {
    ...
}

// Do
f (obj != null) {
    ...
}

2. 【強制】異常不能用于流程控制

不建議使用異常作為流程控制的原因有兩點:

① 影響函數(shù)的易用性
反例:使用中臺播放器進行 seek 的時候阁最,播放器對當前的狀態(tài)機進行了校驗戒祠,如果不符合預期,直接拋出了異常速种,這種處理方案看起來也比較合理姜盈,進行了嚴格的狀態(tài)校驗,但是過于生硬了配阵,在 crash 與 seek 失敗兩種情況下馏颂,顯然 crash 的后果要嚴重的多。并且此時 seek 失敗可能是用戶無感知的棋傍。所以比較推薦的方法救拉,是打印 seek 失敗日志,然后進行 return瘫拣。

@Override
public void seekTo(int positionMs) throws IllegalStateException {
    TPLogUtil.i(TAG, "seekTo:" + positionMs);

    throwExceptionIfPlayerReleased();
    
    int ret = mPlayer.seekToAsync(positionMs, TPNativePlayerSeekMode.PREVIOUS_KEY_FRAME, 0);
    if (ret != TPGeneralError.OK) {
        throw new IllegalStateException("seek to position:" + positionMs + " failed!!");
    }
}

② 效率低
異常處理的效率會遠低于條件判斷亿絮,使用小米 10Pro 進行測試,正例的時間消費大約在 0-1ms麸拄,反例的時間消費大約在 44-50ms派昧。

// Don't
private void handleOnClickTryCatch() {
    for (int i = 0; i < 10000; i++) {
        try {
            seekTo(0);
        } catch (Exception e) {
            //ignore,避免影響性能拢切,對測試產(chǎn)生干擾
        }
    }
}

private void seekTo(int pos) throws Exception {
    throw new Exception();
}

// Do
private void handleOnClickCondition() {
    for (int i = 0; i < 10000; i++) {
        seekToNoEx(0);
    }
}

private void seekToNoEx(int pos) {
    currentPos = pos;
}

3. 【強制】不要對?段代碼進? try catch

對大段代碼進行 try-catch 程序無法根據(jù)不同的異常做出正確的應激反應蒂萎,也不利于定位問題,這是一種不負責任的表現(xiàn)淮椰。

4. 【強制】異常捕獲必須處理

5. 【強制】不要在 fina中 使用 return

try 塊中的 return 語句成功后五慈,并不馬上返回,而是繼續(xù)執(zhí)行 finally 塊中的語句实苞,如果此處存在 return 語句豺撑,則在此直接返回,無情丟棄掉 try 塊中的返回點黔牵。

// Don't
private int x = 0;
public int checkReturn() { 
    try {
        // x 等于 1,此處不返回
        return ++x; 
    } finally {
        // 返回的結果是 2
        return ++x; 
    }
}

6. 【強制】?nally 中必須對資源進?釋放

在 finally 中釋放資源時爷肝,可以使用函數(shù)封裝優(yōu)雅猾浦。并且對于嵌套流,不必層層關閉灯抛,只需關閉最外層的流金赦。Exception 不要使用 print StackTrace 直接輸出,使用 log 進行封裝对嚼,最好標記這個 Exception 是已經(jīng)捕獲的夹抗。

// Do
private User readUser() {
    FileInputStream fileStream = null;
    ObjectInputStream input = null;
    User user = null;
    try {
        fileStream = new FileInputStream("Object.txt");
        input = new ObjectInputStream(fileStream);
        user = (User) input.readObject();
    } catch (Exception e) {
        logger.info("exception catched:" + Log.getStackTraceString(e));
    } finally {
        closeSafe(input);
    }
    return user;
}

如果 JDK7 及以上彬檀,可以使用 try-witesources谷婆。AutoCloseable 需要繼承 AutoCloseable墓造。

// Do
try(Resource resource = new Resource()) {
    resource.sayHello();
} catch (Exception e) {
    e.printStackTrace();
}

2媒区、插件開發(fā)

1. 插件中不要引?主?程中的 ?nal 變量

除非你確定它不會變化,因為在插件編譯時這個值就會被固定已脓,并不會隨著主工程中該final變量值的更改而變化珊楼。

反例:

在插件中希望能獲取 GlobalConfig.SDK_VERSION 這個值,這塊在編譯的時候會被直接賦予一個固定的值度液,并不會隨著主工程變量值的更改而變化厕宗。我們反編譯后可以發(fā)現(xiàn)

3、安全規(guī)約

1. 用戶敏感數(shù)據(jù)禁止直接展示堕担,必須對展示數(shù)據(jù)進?脫敏已慢。

說明:中國大陸個人手機號碼顯示為:158****9119,中間 4 位霹购,防止隱私泄露佑惠。

2. 盡量使組件禁止外部訪問

當 Android 四大組件不需其他應用訪問時,顯示注明 android:exported=false厕鹃,因為 exported 的默認值可能發(fā)生變化兢仰。
當組件包含 <intent-filter> 時,exported 默認為 true剂碴,否則默認為 false把将。

3. 避免使用全局廣播傳遞敏感信息

可以使用 LocalBrdcastManager 進行替代,LocalBroadcastManager 基于 Handler忆矛,擁有更好的效率察蹲,但是只能在同進程內(nèi)傳遞。
對于使用全局廣播催训,可以通過 Intent.setPackage 來限制接收方包名洽议,來保證安全。
然而尷尬的是 LocalBroadcastManager 在新的版本中已經(jīng)廢棄漫拭,取而代之的是 LiveData 和 Reactive streams亚兄。用法后續(xù)更新...

4、進程相關

1. Binder 傳輸數(shù)據(jù)大小限制為 1M

所以基于 Binder 的通信方式都會收到此限制采驻,例如使用 Intent 在組件中傳遞數(shù)據(jù)审胚。

2. 禁止使用 New Thread 方式創(chuàng)建線程

因為會有明顯的延遲,?量使?后會對系統(tǒng)性能造成額外的開銷礼旅。

3. 使用廣播跨進城通信時膳叨,防止出現(xiàn)廣播震蕩

使?名為 caller 的 int 值來表示啟動類型,存在多個進程中痘系,當值發(fā)?變化時菲嘴,通知其他進程跟隨變化。當 caller 值在兩個進程中同時變化時,就可能觸發(fā)?播震蕩龄坪,產(chǎn)?死循環(huán)昭雌。

解決方案:
使用時間戳來表示最近的一次修改,或者使用 ContentProvider 來進行值的跨進程傳輸悉默。

5城豁、性能優(yōu)化

1. 合理使用 LAYER_TYPE_HARDE 提高動畫性能

通過 View.setLayerType 接? View 的繪制?式,默認值是 LAYER_TYPE_NONE抄课。 如果設置參數(shù)為 LAYER_TYPE_HARDWARE唱星,并且打開硬件加速,就會產(chǎn)?離屏緩沖跟磨,若 View 內(nèi)部元素不更新间聊,這時對 View 做動畫效率會?很多,例如桌?左右翻屏時抵拘。
LAYER_TYPE_SOFTWAR E會將 View 繪制到 Bitmap 中哎榴,一般不會使用。

2. 使用 Printer 監(jiān)控線程卡頓

使? Android 現(xiàn)有的機制 Printer僵蛛,在 Looper 執(zhí)?單個任務前后打印尚蝌,就可以知道任務的執(zhí)?時間,我們設置?個閾值充尉,然后打印線程堆棧飘言,就知道哪個任務卡頓了。

    /**
     * Run the message queue in this thread. Be sure to call * {@link #quit()} to end the loop.
     */
    public static void loop() {
         ...
        for (; ; ) {
            Message msg = queue.next(); // might block ...
            // This must be in a local variable, in case a UI event sets the logger 
            final Printer logging = me.mLogging;
            if (logging != null) { 
            logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": "      + msg.what); }

            ...
            try {
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            ...
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
            ...
        }
    }

3. 不要使用 SharePreference 進行跨進程通信

雖然 Google 提供了 MODE_MULTI_PROCESS 模式驼侠,但是其原理只是在 getSharedPreferences 時姿鸿,重新加載了 xml,所以性能很差倒源,跨進程數(shù)據(jù)傳輸請使? ContentProvider苛预。

@Override 
public SharedPreferences getSharedPreferences(String name, int mode) {

    SharedPreferencesImpl sp; 
    ...

    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||               getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) { 
        // If somebody else (some other process) changed the prefs 
        // file behind our back, we reload it. This has been the 
        // historical (if undocumented) behavior.
        sp.startReloadIfChangedUnexpectedly();
    } 
    return sp;
}

4. 序列化場景最好使用 FlatBuffer

FlatBu?ers 是?個開源的、跨平臺的笋熬、?效的热某、提供了 C++/Java 接?的序列化?具庫。 它是 Google 專?為游戲開發(fā)或其他性能敏感的應?程序需求?創(chuàng)建的胳螟。尤其適?于移動平臺苫拍。

主要優(yōu)點:

● 對序列化數(shù)據(jù)的訪問不需要打包和拆包,它將序列化數(shù)據(jù)存儲在緩存中旺隙,這些數(shù)據(jù)既
可存儲在文件中,又可以通過網(wǎng)絡原樣傳輸骏令,而沒有任何解析開銷蔬捷;
● 內(nèi)存效率和速度:訪問數(shù)據(jù)時的唯一內(nèi)存需求就是緩沖區(qū),不需要額外的內(nèi)存分配。
這可查看詳細的基準測試周拐。
● 擴展性铡俐、靈活性:它支持的可選字段意味著不僅能獲得很好的前向/后向兼容性(對
于生命周期的游戲來說尤其重要,因為不需要每個新版本都更新所有數(shù)據(jù))妥粟。
● 最小代碼依賴:僅僅需要自動生成的少量代碼和一個單一的頭文件依賴审丘,很容易集成
到有系統(tǒng)中。
● 強類型設計:盡可能使錯誤出現(xiàn)在編譯期勾给,而不是等到運行期才手動檢查和修正滩报。
● 使用簡單:生成的 C++ 代碼提供了簡單的訪問和構造接口;而且如果需要播急,通過一個可選功能可以在運行時脓钾,高效解析 Schema 和類 JSON 格式的文本。
● 跨平臺:支持 C++11桩警、Java可训,而不需要任何依賴庫;在最新的 gcc捶枢、clng握截、vs2010 等編譯器上工作良好。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末烂叔,一起剝皮案震驚了整個濱河市谨胞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌长已,老刑警劉巖畜眨,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異术瓮,居然都是意外死亡康聂,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門胞四,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恬汁,“玉大人,你說我怎么就攤上這事辜伟∶ゲ啵” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵导狡,是天一觀的道長约巷。 經(jīng)常有香客問我,道長旱捧,這世上最難降的妖魔是什么独郎? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任踩麦,我火速辦了婚禮,結果婚禮上氓癌,老公的妹妹穿的比我還像新娘谓谦。我一直安慰自己,他們只是感情好贪婉,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布反粥。 她就那樣靜靜地躺著,像睡著了一般疲迂。 火紅的嫁衣襯著肌膚如雪才顿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天鬼譬,我揣著相機與錄音娜膘,去河邊找鬼。 笑死优质,一個胖子當著我的面吹牛竣贪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播巩螃,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼演怎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了避乏?” 一聲冷哼從身側響起爷耀,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拍皮,沒想到半個月后歹叮,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡铆帽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年咆耿,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片爹橱。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡萨螺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出愧驱,到底是詐尸還是另有隱情慰技,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布组砚,位于F島的核電站吻商,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏糟红。R本人自食惡果不足惜手报,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一蚯舱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧掩蛤,春花似錦、人聲如沸陈肛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽句旱。三九已至阳藻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谈撒,已是汗流浹背腥泥。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留啃匿,地道東北人蛔外。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像溯乒,于是被迫代替她去往敵國和親夹厌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

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

  • Android 開發(fā)最佳實踐 轉載于https://github.com/futurice/android-bes...
    Di_xin閱讀 680評論 0 12
  • 從Futurice公司Android開發(fā)者中學到的經(jīng)驗裆悄。遵循以下準則矛纹,避免重復發(fā)明輪子。若您對開發(fā)iOS或Wind...
    兔子吃過窩邊草閱讀 937評論 0 7
  • 轉載文章光稼,Android開發(fā)過程中的經(jīng)驗總結或南。原文鏈接遵循以下準則,避免重復發(fā)明輪子艾君。若您對開發(fā)iOS或Windo...
    王鵬程Orange閱讀 1,187評論 0 22
  • 轉載:https://github.com/futurice/android-best-practices 從Fu...
    溺水dei魚閱讀 154評論 0 0
  • 久違的晴天采够,家長會。 家長大會開好到教室時腻贰,離放學已經(jīng)沒多少時間了吁恍。班主任說已經(jīng)安排了三個家長分享經(jīng)驗。 放學鈴聲...
    飄雪兒5閱讀 7,495評論 16 22