NSLog 和 print 源碼閱讀和捕獲方案

前言

NSLog 作為 iOS開(kāi)發(fā)常用的調(diào)試和日志打印方法,大家都是很熟悉了氢烘,
開(kāi)源社區(qū)也為我們貢獻(xiàn)了很多非常優(yōu)秀的日志框架杭朱,比如OC中大名鼎鼎的CocoaLumberjack,有興趣的同學(xué)可以移步https://github.com/CocoaLumberjack/CocoaLumberjack
在Swift語(yǔ)言下我們還有另外一種選擇,那就是print

如果要自己做日志監(jiān)控的話(huà)甚脉,就需要就需要自己重定向NSLogprint方法了

網(wǎng)絡(luò)上大概有如下幾種方法

  • asl讀取日志(iOS10以后已經(jīng)棄用)
    NSLog默認(rèn)的輸出到了系統(tǒng)的 /var/log/syslog這個(gè)文件,asl框架能從syslog中讀取日志勉盅,此種方法對(duì)系統(tǒng)無(wú)侵入佑颇,可惜iOS10后已經(jīng)獲取不到日志
  • 采用dup2的重定向方式
    NSLog最后重定向的句柄是STDERR,NSLog輸出的日志內(nèi)容菇篡,最終都通過(guò)STDERR句柄來(lái)記錄漩符,使用dup2重定向STDERR句柄一喘,可以將內(nèi)容重定向指定的位置驱还,但是重定向之后
  • 采用fishhook方式
    采用facebook的開(kāi)源框架 fishhook來(lái)動(dòng)態(tài)替換NSLogprint方法

本文選擇基于fishhook的方式捕獲 NSLog 和 print 方法
福利:https://github.com/madaoCN/Supervisor已實(shí)現(xiàn)輕量級(jí)的日志打印,使用fishhook hook了 NSLog 和 print方法

前期準(zhǔn)備

  • fishhook原理
    網(wǎng)上有很多寫(xiě)的很好的文章凸克,這里就不獻(xiàn)丑啦议蟆,大家自行百度/谷歌哈

  • 編譯swift源碼
    swift已經(jīng)開(kāi)源了,我們可以閱讀源碼來(lái)一窺究竟
    項(xiàng)目地址 https://github.com/apple/swift
    編譯也很簡(jiǎn)單萎战,就是比較耗時(shí)

brew install cmake ninja
mkdir swift-source
cd swift-source
git clone https://github.com/apple/swift.git
./swift/utils/update-checkout --clone
cd swift
utils/build-script --release-debuginfo
image.png

最終的編譯文件可能會(huì)比較大咐容,我的大概是 44.18GB, 編譯前請(qǐng)預(yù)留足夠的空間

搞定了的話(huà),黑喂狗~

NSLog源碼閱讀

NSLog的源碼位置在 你的編譯工程目錄/Swift-Build/swift-corelibs-foundation

/* Output from NSLogv is serialized, in that only one thread in a process can be doing 
 * the writing/logging described above at a time. All attempts at writing/logging a 
 * message complete before the next thread can begin its attempts. The format specification 
 * allowed by these functions is that which is understood by NSString's formatting capabilities.
 * CFLog1() uses writev/fprintf to write to stderr. Both these functions ensure atomic writes.
 */

public func NSLogv(_ format: String, _ args: CVaListPointer) {
    let message = NSString(format: format, arguments: args)
    CFLog1(CFLogLevel(kCFLogLevelWarning), message._cfObject)
}

public func NSLog(_ format: String, _ args: CVarArg...) {
    withVaList(args) { 
        NSLogv(format, $0) 
    }
}

我們可以看到NSLog調(diào)用

NSLog ------> NSLogv ------> CFLog1

結(jié)合源碼中的注釋CFLog1() uses writev/fprintf to write to stderr基本可以猜到NSLog最終會(huì)調(diào)用writev 和 fprintf方法蚂维,接下來(lái)我們順騰摸瓜看下 CFLog1的邏輯

void CFLog1(CFLogLevel lev, CFStringRef message) {
#if TARGET_OS_ANDROID
    if (message == NULL) message = CFSTR("NULL");

    ...

    CFStringEncoding encoding = kCFStringEncodingUTF8;
    CFIndex maxLength = CFStringGetMaximumSizeForEncoding(CFStringGetLength(message), encoding) + 1;

    ...

    if (maxLength == 1) {
        // was crashing with zero length strings
        // https://bugs.swift.org/browse/SR-2666
        strcpy(buffer, " "); // log empty string
    }
    else
        CFStringGetCString(message, buffer, maxLength, encoding);
    
        __android_log_print(priority, tag, "%s", buffer);
        // ======= 注意這里 =======
        fprintf(stderr, "%s\n", buffer);
    
    if (buffer != &stack_buffer[0]) free(buffer);
#else
    // ======= 注意這里 =======
    CFLog(lev, CFSTR("%@"), message);
#endif
}

可以看到如果是安卓環(huán)境下戳粒,會(huì)調(diào)用 fprintf, 否則會(huì)調(diào)用 CFLog方法

  NSLog 
    ↓
  NSLogv
    ↓
  CFLog1
    ↓
  CFLog

現(xiàn)在調(diào)用的順序是這樣滴,接下來(lái)往下走, 我們看看CFLog

void CFLog(int32_t lev, CFStringRef format, ...) {
    va_list args;
    va_start(args, format); 
    _CFLogvEx3(NULL, NULL, NULL, NULL, lev, format, args, __builtin_return_address(0));
    va_end(args);
}

// 調(diào)用了_CFLogvEx3
CF_EXPORT void _CFLogvEx3(CFLogFunc logit, CFStringRef (*copyDescFunc)(void *, const void *), CFStringRef (*contextDescFunc)(void *, const void *, const void *, bool, bool *), CFDictionaryRef formatOptions, int32_t lev, CFStringRef format, va_list args, void *addr) {
    _CFLogvEx2Predicate(logit, copyDescFunc, contextDescFunc, formatOptions, lev, format, args, _cf_logging_style_legacy);
    
}

// 調(diào)用了_CFLogvEx2Predicate
static void _CFLogvEx2Predicate(CFLogFunc logit, CFStringRef (*copyDescFunc)(void *, const void *), CFStringRef (*contextDescFunc)(void *, const void *, const void *, bool, bool *), CFDictionaryRef formatOptions, int32_t lev, CFStringRef format, va_list args, _cf_logging_style loggingStyle) {
#if TARGET_OS_MAC
    uintptr_t val = (uintptr_t)_CFGetTSD(__CFTSDKeyIsInCFLog);
    if (3 < val) return; // allow up to 4 nested invocations
    _CFSetTSD(__CFTSDKeyIsInCFLog, (void *)(val + 1), NULL);
#endif
    CFStringRef str = format ? _CFStringCreateWithFormatAndArgumentsAux2(kCFAllocatorSystemDefault, copyDescFunc, contextDescFunc, formatOptions, (CFStringRef)format, args) : 0;
    CFIndex blen = str ? CFStringGetMaximumSizeForEncoding(CFStringGetLength(str), kCFStringEncodingUTF8) + 1 : 0;
    char *buf = str ? (char *)malloc(blen) : 0;
    if (str && buf) {
    Boolean converted = CFStringGetCString(str, buf, blen, kCFStringEncodingUTF8);
    size_t len = strlen(buf);
    // silently ignore 0-length or really large messages, and levels outside the valid range
    if (converted && !(len <= 0 || (1 << 24) < len) && !(lev < ASL_LEVEL_EMERG || ASL_LEVEL_DEBUG < lev)) {
            if (logit) {
                logit(lev, buf, len, 1);
            }
            else if (loggingStyle == _cf_logging_style_os_log) {
                // ======= 注意這里 =======
                __CFLogCString(lev, buf, len, 1);
            }
            else if (loggingStyle == _cf_logging_style_legacy) {
                // ======= 注意這里 =======
                __CFLogCStringLegacy(lev, buf, len, 1);
            }
    }
    }
    if (buf) free(buf);
    if (str) CFRelease(str);
#if TARGET_OS_MAC
    _CFSetTSD(__CFTSDKeyIsInCFLog, (void *)val, NULL);
#endif
}

會(huì)調(diào)用到 __CFLogCString__CFLogCStringLegacy這兩個(gè)方法虫啥,那么現(xiàn)在調(diào)用的流程是這樣

                      NSLog 
                        ↓
                      NSLogv
                        ↓
                      CFLog1
                        ↓
                      CFLog
                        ↓             
                   _CFLogvEx3
                        ↓             
               _CFLogvEx2Predicate
                        |
                       / \
                     /     \
                   /         \
          __CFLogCString   __CFLogCStringLegacy

繼續(xù)閱讀源碼__CFLogCString__CFLogCStringLegacy這兩個(gè)方法最終都調(diào)用了_logToStderr方法

static void _logToStderr(char *banner, const char *message, size_t length) {
#if TARGET_OS_MAC
    struct iovec v[3];
    v[0].iov_base = banner;
    v[0].iov_len = banner ? strlen(banner) : 0;
    v[1].iov_base = (char *)message;
    v[1].iov_len = length;
    v[2].iov_base = "\n";
    v[2].iov_len = (message[length - 1] != '\n') ? 1 : 0;
    int nv = (v[0].iov_base ? 1 : 0) + 1 + (v[2].iov_len ? 1 : 0);
    static CFLock_t lock = CFLockInit;
    __CFLock(&lock);

     // ======= 注意這里 =======

    writev(STDERR_FILENO, v[0].iov_base ? v : v + 1, nv);
    __CFUnlock(&lock);
#elif TARGET_OS_WIN32
    size_t bannerLen = strlen(banner);
    size_t bufLen = bannerLen + length + 1;
    char *buf = (char *)malloc(sizeof(char) * bufLen);
    if (banner) {
        // Copy the banner into the debug string
        memmove_s(buf, bufLen, banner, bannerLen);
        
        // Copy the message into the debug string
        strcpy_s(buf + bannerLen, bufLen - bannerLen, message);
    } else {
        strcpy_s(buf, bufLen, message);
    }
    buf[bufLen - 1] = '\0';
    fprintf_s(stderr, "%s\n", buf);
    // This Win32 API call only prints when a debugger is active
    // OutputDebugStringA(buf);
    free(buf);
#else
    size_t bannerLen = strlen(banner);
    size_t bufLen = bannerLen + length + 1;
    char *buf = (char *)malloc(sizeof(char) * bufLen);
    if (banner) {
        // Copy the banner into the debug string
        memmove(buf, banner, bannerLen);
        
        // Copy the message into the debug string
        strncpy(buf + bannerLen, message, bufLen - bannerLen);
    } else {
        strncpy(buf, message, bufLen);
    }
    buf[bufLen - 1] = '\0';

    // ======= 注意這里 =======

    fprintf(stderr, "%s\n", buf);
    free(buf);
#endif
}

可見(jiàn)NSLog最終都調(diào)用了writevfprintf方法

                      NSLog 
                        ↓
                      NSLogv
                        ↓
                      CFLog1
                        ↓
                      CFLog
                        ↓             
                   _CFLogvEx3
                        ↓             
               _CFLogvEx2Predicate
                        |
                       / \
                     /     \
                   /         \
          __CFLogCString   __CFLogCStringLegacy
                   \         /
                     \     /
                       \ /
                 _logToStderr
                        ↓
                  writev / fprintf

結(jié)果與之前的注釋一致蔚约,那么我們只需要使用 fishhook 對(duì) writev / fprintf方法進(jìn)行hook就能達(dá)到我們的目的了,那么我們繼續(xù)看看 print函數(shù)的源碼

print函數(shù)源碼閱讀

print的源碼位置在類(lèi)似 你的編譯工程目錄/Swift-Build/build/Xcode-RelWithDebInfoAssert/swift-macosx-x86_64具體名字和編譯參數(shù)和機(jī)器有關(guān)

我們很容易就找到了源碼

// ============ print方法1
public func print(
  _ items: Any...,
  separator: String = " ",
  terminator: String = "\n"
) {
  if let hook = _playgroundPrintHook {
     // ======= 注意這里 =======
    var output = _TeeStream(left: "", right: _Stdout())
    _print(items, separator: separator, terminator: terminator, to: &output)
    hook(output.left)
  }
  else {
   // ======= 注意這里 =======
    var output = _Stdout()
    _print(items, separator: separator, terminator: terminator, to: &output)
  }
}

// ============ print方法2
public func print<Target: TextOutputStream>(
  _ items: Any...,
  separator: String = " ",
  terminator: String = "\n",
  to output: inout Target
) {
  // ======= 注意這里 =======
  _print(items, separator: separator, terminator: terminator, to: &output)
}

可見(jiàn)print 方法會(huì)調(diào)用_print方法

internal func _print<Target: TextOutputStream>(
  _ items: [Any],
  separator: String = " ",
  terminator: String = "\n",
  to output: inout Target
) {
  var prefix = ""
  output._lock()
  defer { output._unlock() }
  for item in items {
    output.write(prefix)

    // ======= 注意這里 =======
    _print_unlocked(item, &output)

    prefix = separator
  }
  output.write(terminator)
}

// _print_unlocked 源碼
@usableFromInline
@_semantics("optimize.sil.specialize.generic.never")
internal func _print_unlocked<T, TargetStream: TextOutputStream>(
  _ value: T, _ target: inout TargetStream
) {
  // Optional has no representation suitable for display; therefore,
  // values of optional type should be printed as a debug
  // string. Check for Optional first, before checking protocol
  // conformance below, because an Optional value is convertible to a
  // protocol if its wrapped type conforms to that protocol.
  // Note: _isOptional doesn't work here when T == Any, hence we
  // use a more elaborate formulation:
  if _openExistential(type(of: value as Any), do: _isOptional) {
    let debugPrintable = value as! CustomDebugStringConvertible
    debugPrintable.debugDescription.write(to: &target)
    return
  }
  if case let streamableObject as TextOutputStreamable = value {
    streamableObject.write(to: &target)
    return
  }

  if case let printableObject as CustomStringConvertible = value {
    printableObject.description.write(to: &target)
    return
  }

  if case let debugPrintableObject as CustomDebugStringConvertible = value {
    debugPrintableObject.debugDescription.write(to: &target)
    return
  }

  let mirror = Mirror(reflecting: value)

  _adHocPrint_unlocked(value, mirror, &target, isDebugPrint: false)
}


可見(jiàn)調(diào)用流程如下

print ------> _print ------> _print_unlocked

這里的

  • TextOutputStreamable
  • CustomDebugStringConvertible
  • CustomStringConvertible
////////////////////   CustomStringConvertible
public protocol CustomStringConvertible {
  var description: String { get }
}

////////////////////   CustomDebugStringConvertible
public protocol CustomDebugStringConvertible {
  var debugDescription: String { get }
}

////////////////////   TextOutputStreamable
public protocol TextOutputStreamable {
  /// Writes a textual representation of this instance into the given output
  /// stream.
  func write<Target: TextOutputStream>(to target: inout Target)
}

等都是協(xié)議, 將 Target 傳入并調(diào)用 Targetwrite 方法

我們回過(guò)頭來(lái)看下函數(shù)名
internal func _print_unlocked<T, TargetStream: TextOutputStream>( _ value: T, _ target: inout TargetStream )

target 是遵循 TextOutputStream協(xié)議的對(duì)象涂籽,也就是我們之前看到的
_Stdout ()函數(shù)


////////////// TextOutputStream 協(xié)議
public protocol TextOutputStream {
  mutating func _lock()
  mutating func _unlock()

  /// Appends the given string to the stream.
  mutating func write(_ string: String)

  mutating func _writeASCII(_ buffer: UnsafeBufferPointer<UInt8>)
}

////////////// _Stdout 
internal struct _Stdout: TextOutputStream {
  internal init() {}

  internal mutating func _lock() {
    _swift_stdlib_flockfile_stdout()
  }

  internal mutating func _unlock() {
    _swift_stdlib_funlockfile_stdout()
  }

  internal mutating func write(_ string: String) {
    if string.isEmpty { return }

    var string = string
    _ = string.withUTF8 { utf8 in
      
       // ======= 注意這里 ======= 
      _swift_stdlib_fwrite_stdout(utf8.baseAddress!, 1, utf8.count)
    }
  }
}

// =========== _swift_stdlib_fwrite_stdout 源代碼
SWIFT_RUNTIME_STDLIB_INTERNAL
__swift_size_t swift::_swift_stdlib_fwrite_stdout(const void *ptr,
                                                  __swift_size_t size,
                                                  __swift_size_t nitems) {
    // ======= 注意這里 ======= 
    return fwrite(ptr, size, nitems, stdout);
}

我們可以看到

_Stdout -> _swift_stdlib_fwrite_stdout -> fwrite

結(jié)合之前的調(diào)用方法, 最終也調(diào)用了 fwrite方法

print -> _print -> _print_unlocked ->  (print items) -> (write/description.write/debugDescription.write)-> Stdout -> _swift_stdlib_fwrite_stdout -> fwrite

繞了這么大的一圈苹祟,我們得出的結(jié)論是,print 函數(shù)最終調(diào)用了fwrite

中場(chǎng)休息~~~~

最終评雌,如果我們要日志監(jiān)控的話(huà)树枫,只需要hook如下三個(gè)方法

NSLog 調(diào)用  writev / fprintf
print 調(diào)用  fwrite

Hook代碼

  • 首先我們hook writev, 函數(shù)原型
static ssize_t writev(int a, const struct iovec * v, int v_len);

/// struct iovec 類(lèi)型
struct iovec {
    void *   iov_base;      /* [XSI] Base address of I/O memory region */
    size_t   iov_len;       /* [XSI] Size of region iov_base points to */
};

具體hook代碼

//--------------------------------------------------------------------------
// MARK: hook NSLog
//--------------------------------------------------------------------------

// origin writev IMP
static ssize_t (*orig_writev)(int a, const struct iovec * v, int v_len);

// swizzle method
ssize_t asl_writev(int a, const struct iovec *v, int v_len) {
    
    NSMutableString *string = [NSMutableString string];
    for (int i = 0; i < v_len; i++) {
        char *c = (char *)v[i].iov_base;
        [string appendString:[NSString stringWithCString:c encoding:NSUTF8StringEncoding]];
    }
    
    ////////// do something  這里可以捕獲到日志 string
    
    // invoke origin mehtod
    ssize_t result = orig_writev(a, v, v_len);
    return result;
}
  • 然后是 fprintf 函數(shù),這里因?yàn)?fprintf 是可變參數(shù)景东,具體可變參數(shù)相關(guān)使用可見(jiàn)博主的另外一篇博客 va_list 可變參數(shù)概覽

這里先使用NSString 的 @selector(initWithFormat : arguments)方法生成要輸出的字符串砂轻,直接調(diào)用 origin_fprintf 將自行生成的字符串作為參數(shù)就行了,免去再次傳遞可變參數(shù)至 origin_fprintf

//--------------------------------------------------------------------------
// MARK: hook fprintf
//--------------------------------------------------------------------------

// origin fprintf IMP
static int     (*origin_fprintf)(FILE * __restrict, const char * __restrict, ...);

// swizzle method
int     asl_fprintf(FILE * __restrict file, const char * __restrict format, ...)
{
    /*
     typedef struct {
         
        unsigned int gp_offset;
        unsigned int fp_offset;
        void *overflow_arg_area;
        void *reg_save_area;
     } va_list[1];
     */
    va_list args;
    
    va_start(args, format);

    NSString *formatter = [NSString stringWithUTF8String:format];
    NSString *string = [[NSString alloc] initWithFormat:formatter arguments:args];
    
    ////////// do something  這里可以捕獲到日志

    // invoke orign fprintf
    int result = origin_fprintf(file, [string UTF8String]);
    
    va_end(args);

    return result;
}
  • 然后是 fprintf方法

調(diào)用例如 fprintf ("test");方法時(shí)候 asl_fwrite會(huì)調(diào)用兩次斤吐,參數(shù)一次是test,另一次是\n搔涝,所以先將字符串放入__messageBuffer,等收到\n時(shí),再將 __messageBuffer中轉(zhuǎn)成字符串一次性讀取

//--------------------------------------------------------------------------
// MARK: hook print for swift
//--------------------------------------------------------------------------

// origin fwrite IMP
static size_t (*orig_fwrite)(const void * __restrict, size_t, size_t, FILE * __restrict);

static char *__messageBuffer = {0};
static int __buffIdx = 0;
void reset_buffer()
{
    __messageBuffer = calloc(1, sizeof(char));
    __messageBuffer[0] = '\0';
    __buffIdx = 0;
}


// swizzle method
size_t asl_fwrite(const void * __restrict ptr, size_t size, size_t nitems, FILE * __restrict stream) {
        
    if (__messageBuffer == NULL) {
        // initial Buffer
        reset_buffer();
    }
    
    char *str = (char *)ptr;
    
    NSString *s = [NSString stringWithCString:str encoding:NSUTF8StringEncoding];
    
    if (__messageBuffer != NULL) {
        
        if (str[0] == '\n' && __messageBuffer[0] != '\0') {
            
            s = [[NSString stringWithCString:__messageBuffer encoding:NSUTF8StringEncoding] stringByAppendingString:s];
            
            // reset buffIdx
            reset_buffer();

            ////////// do something  這里可以捕獲到日志
        }
        else {
            
            // append buffer
            __messageBuffer = realloc(__messageBuffer, sizeof(char) * (__buffIdx + nitems + 1));
            for (size_t i = __buffIdx; i < nitems; i++) {
                __messageBuffer[i] = str[i];
                __buffIdx ++;
            }
            __messageBuffer[__buffIdx + 1] = '\0';
            __buffIdx ++;
        }
    }
    
    return orig_fwrite(ptr, size, nitems, stream);
}

最后就是 hook 的代碼曲初,沒(méi)啥好說(shuō)的

//--------------------------------------------------------------------------
// MARK: fishhook調(diào)用
//--------------------------------------------------------------------------
// hook writev
rebind_symbols((struct rebinding[1]){{
    "writev",
    asl_writev,
    (void*)&orig_writev
}}, 1);

// hook fwrite
rebind_symbols((struct rebinding[1]){{
    "fwrite",
    asl_fwrite,
    (void *)&orig_fwrite}}, 1);

// hook fprintf
rebind_symbols((struct rebinding[1]){{
    "fprintf",
    asl_fprintf,
    (void *)&origin_fprintf}}, 1);

接下來(lái)我們看下成果


image.png

具體的代碼体谒,請(qǐng)見(jiàn)https://github.com/madaoCN/Supervisor 功能還在完善中,將間斷更新

參考

用fishhook hook輸出方法(NSLog, print)
捕獲NSLog日志小記
GodEye日志監(jiān)控
iOS逆向工程 - fishhook原理

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末臼婆,一起剝皮案震驚了整個(gè)濱河市抒痒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌颁褂,老刑警劉巖故响,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件傀广,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡彩届,警方通過(guò)查閱死者的電腦和手機(jī)伪冰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)樟蠕,“玉大人贮聂,你說(shuō)我怎么就攤上這事≌纾” “怎么了吓懈?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)靡狞。 經(jīng)常有香客問(wèn)我耻警,道長(zhǎng),這世上最難降的妖魔是什么甸怕? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任甘穿,我火速辦了婚禮,結(jié)果婚禮上梢杭,老公的妹妹穿的比我還像新娘温兼。我一直安慰自己,他們只是感情好式曲,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布妨托。 她就那樣靜靜地躺著,像睡著了一般吝羞。 火紅的嫁衣襯著肌膚如雪兰伤。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天钧排,我揣著相機(jī)與錄音敦腔,去河邊找鬼。 笑死恨溜,一個(gè)胖子當(dāng)著我的面吹牛符衔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播糟袁,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼判族,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了项戴?” 一聲冷哼從身側(cè)響起形帮,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后辩撑,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體界斜,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年合冀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了各薇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡君躺,死狀恐怖峭判,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情晰洒,我是刑警寧澤朝抖,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站谍珊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏急侥。R本人自食惡果不足惜砌滞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望坏怪。 院中可真熱鬧贝润,春花似錦、人聲如沸铝宵。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鹏秋。三九已至尊蚁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間侣夷,已是汗流浹背横朋。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留百拓,地道東北人琴锭。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像衙传,于是被迫代替她去往敵國(guó)和親决帖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

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

  • 日志幾乎是我們每一個(gè)iOS開(kāi)發(fā)者每一天都要打交道的東西蓖捶,比如運(yùn)行時(shí)想看一下某個(gè)變量的值地回,那就NSLog()一下;當(dāng)...
    子循_陳奕龍閱讀 2,820評(píng)論 2 8
  • 更新2021/2/26(感謝@lgq_9b65的提醒, 由于我一直沒(méi)用真機(jī)測(cè)試, 才搞出這個(gè)烏龍.) 真機(jī)測(cè)試中發(fā)...
    kagenZhao閱讀 4,918評(píng)論 6 25
  • 因?yàn)橐Y(jié)局swift3.0中引用snapKit的問(wèn)題,看到一篇介紹Xcode8,swift3變化的文章,覺(jué)得很詳細(xì)...
    uniapp閱讀 4,414評(píng)論 0 12
  • 峰會(huì)結(jié)束,傻瓜歸來(lái)。大家好落君,我是耀華穿香。TMD88大區(qū)北京峰會(huì)中文演講比賽,我做了一個(gè)叫演講绎速,名字叫傻瓜皮获。講了我生活...
    朱耀華閱讀 408評(píng)論 0 2
  • 這周持續(xù)練習(xí)固柢,嘴角起了一個(gè)疙瘩纹冤,紅腫發(fā)炎洒宝,左邊的脖子的紅疹發(fā)的越拉越大了,之前也有過(guò)萌京,不知道是否聯(lián)系后的好轉(zhuǎn)反...
    c126f72d7d3d閱讀 217評(píng)論 0 1