iOS APP包瘦身不是你想的那么難,難的還是業務!!!

一、背景

隨著業務的快速發展與持續迭代,玩物得志APP的包體積也在不斷增加,在僅僅四個月的時間,由V3.0.2的127.4M 增大到V3.5.0的174.5M,上漲了約37%,可想而知,如果不及時管控,包體積很快會突破200M。

安裝包過大,將會影響下載轉化率。google開發者大會上公布的統計數據顯示:

包體大小每上升 6MB,應用下載轉化率就會下降 1%,

而每當包體大小減少 10MB 的時候,平均下載轉化率也會有 0.5-1.5% 的增長。

安裝包大小有下載大小安裝大小兩個概念。

app包瘦身

下載大小:通過網絡下載的壓縮 App 大小。為了節省流量,用戶下載的都是壓縮包,而解壓的過程也就是我們說的安裝。

安裝大小:為 App解壓后將在用戶設備上占用的磁盤空間大小。也就是在App Store上看到的大小,安裝大小較大,通常會影響用戶的下載意愿。

下載大小過大,蘋果會限制用戶使用蜂窩網絡下載App。

  • 2017 年 9 月,iOS 11 后,下載限制從 100 MB 提升至 150 MB

  • 2019 年 5 月,下載限制從 150 MB 提升至 200 MB

  • 2019 年 9 月,iOS 13 后,若下載大小超過 200 MB,用戶可選擇是否使用蜂窩網絡下載,但iOS 13以下的系統仍然無法通過蜂窩網絡下載

app包瘦身

雖然蘋果在逐漸放寬限制。但下載大小若超出 200 MB,可以肯定對APP下載成本,推廣效率都會產生比較大的影響。

安裝大小過大,是會影響用戶的留存率的,畢竟當用戶手機內存不夠用時,肯定是優先刪除占內存比較大的App。

所以降低下載大小安裝大小就是我們的目的。

二、包大小分析

通過解壓一個ipa文件,我們可以看到一個.app文件中主要包括三個部分:

  • 資源文件:主要是圖片、音頻、視頻、等資源。

  • 可執行文件:程序的主體,是將我們的代碼、靜態庫、動態庫通過編譯鏈接生成的文件。

  • bundle:工程中使用的三方或資源bundle。

app包瘦身

不過.app的大小并不完全就是包體積的大小,在APP上傳到 AppStore Connect 到之后,Apple 也會對安裝包做一些處理,測試安裝包的變化無法對應到真正的下載大小變化的變化。處理主要包括:

  • App Slicing 對于不同架構的裁剪,可執行文件只剩下單架構;

  • Asset.car 中圖片只留下設備需要的特定尺寸和壓縮算法的變體;

  • __TEXT 段加密;

這也是在不同設備上看到的包大小不同的部分原因。

通過分析可知,瘦身的途徑主要還是針對可執行文件和資源的優化。

三、可執行文件優化

1、刪除無用類

一般的無用代碼篩查方式可以分為動態和靜態兩種方式。靜態的方式主要是通過代碼掃描、參與編譯構建過程或者分析最終產物來確認哪些代碼沒有被用到。而動態的方式主要是靠插樁或者運行時信息來獲取哪些代碼沒有執行。

1.1 動態查找

基于插樁的行級別代碼覆蓋率:

基于 GCOV 或者 LLVM Profile 二進制的插樁方案可以實現在運行時收集插樁數據來指導無用代碼的刪除。但插樁方案局限性也顯而易見,插樁會劣化二進制本身的大小和性能,同時原生的插樁方案是無法過審上線。數據收集只能局限于線下。

基于 Runtime 的輕量級運行時「類覆蓋率」方案:

Objc 的類首次調用類初始化時,+initialize 被執行,系統會自動標記已被調用,在 metaClass 中 data 的 flags 字段第 29 位就存著這個這個狀態。可以使用 flags & RW_INITIALIZED 獲取。

1.2 靜態查找

Mach-O文件中,__DATA`` __objc_classrefs 中記錄了引用類的地址,__DATA``__objc_classlist中記錄了所有類的地址,我們通過otool打印對應的信息,然后兩者取差值,再進行符號化,就得到沒有被引用的類信息。

  1. 通過otool -v -s __DATA __objc_classrefs獲取到引用類(明確用到的)的地址。

  2. 通過otool -v -s __DATA __objc_classlist獲取所有類的地址。

  3. 用所有類信息減去引用類的信息,此時我們可以拿到未使用類的地址信息。

  4. 通過nm -nm命令可以得到地址和對應的類名字。

通過otool -v -s __DATA __objc_classrefs獲取到引用類的地址。

def classref_pointers(path, binary_file_arch):
    ref_pointers = set()
    lines = os.popen('/usr/bin/otool -v -s __DATA __objc_classrefs %s' % path).readlines()
    for line in lines:
        pointers = pointers_from_binary(line, binary_file_arch)
        ref_pointers = ref_pointers.union(pointers)
    return ref_pointers

通過otool -v -s __DATA __objc_classlist獲取所有類的地址。

def classlist_pointers(path, binary_file_arch):
    list_pointers = set()
    lines = os.popen('/usr/bin/otool -v -s __DATA __objc_classlist %s' % path).readlines()
    for line in lines:
        pointers = pointers_from_binary(line, binary_file_arch)
        list_pointers = list_pointers.union(pointers)
    return list_pointers

用所有類信息減去引用類的信息,此時我們可以拿到未使用類的地址信息。

unref_pointers = classlist_pointers(path, binary_file_arch) - classref_pointers(path, binary_file_arch)

通過nm -nm命令可以得到地址和對應的類名字。

def class_symbols(path):
    symbols = {}
    re_class_name = re.compile('(\w{16}) .* _OBJC_CLASS_\$_(.+)')
    lines = os.popen('nm -nm %s' % path).readlines()
    for line in lines:
        result = re_class_name.findall(line)
        if result:
            (address, symbol) = result[0]
            symbols[address] = symbol
    return symbols

得到結果輸出到txt中

app包瘦身

由于是靜態查找,對于動態生成的類,比如通過反射生成的類,會被認為沒有被引用,所以查找出列表后,還需要人工檢查一遍。

優化結果:刪除無用類110個,收益0.5M。

2、編譯選項優化

2.1 開啟LTO

編譯選項Link-Time Optimization優化

蘋果官方介紹,開啟LTO后會使在release下的運行速度提升10%,而且包體積會減小。

Apple uses LTO extensively internally

  • Typically 10% faster than executables from regular Release builds Multiplies

  • with Profile Guided Optimization (PGO)

  • Reduces code size when optimizing for size

但是有個缺點,debug時的編譯速度慢了很多,而且二次編譯時會全部編譯,所以我們只是在release模式下開啟了LTO。

app包瘦身

2.2 Optimization Level

Optimization Level是指clang采用什么樣的編譯優化等級,在Clang的文檔里 clang - Code Generation Options 可以查閱到主要有以下等級:

-O0 Means “no optimization”: this level compiles the fastest and generates the most debuggable code.

-O1 Somewhere between -O0 and -O2.

-O2Moderate level of optimization which enables most optimizations.

-O3 Like -O2, except that it enables optimizations that take longer to perform or that may generate larger code (in an attempt to make the program run faster).

-Ofast Enables all the optimizations from -O3 along with other aggressive optimizations that may violate strict compliance with language standards.

-Os Like -O2 with extra optimizations to reduce code size.

-Oz Like -Os (and thus -O2), but reduces code size further.

Xcode默認debug時為-O0不優化,release時為-Os。經過測試這里如果使用-Oz會大約減小3M左右的包體積,但是在一些頁面會出現crash, 經過排查是一些延遲釋放導致的內存問題。出于安全考慮,目前采用的是-Os這種優化等級。

app包瘦身

2.3 符號相關

symbols是指程序中的所有的變量、類、函數、枚舉、變量和地址映射關系,以及一些在調試的時候使用到的用于定位代碼在源碼中的位置的調試符號,符號和斷點定位以及堆棧符號化有很重要的關系。

2.3.1 Strip Linked Product (STRIP_INSTALLED_PRODUCT)

If enabled, the linked product of the build will be stripped of symbols when performing deployment postprocessing.

如果設置為yes,打包的時候會將symbols裁剪。

并不是所有的符號都是必須的,比如 Debug Map,所以 Xcode 提供給我們 Strip Linked Product 來去除不需要的符號信息(Strip Style 中選擇的選項相應的符號),去除了符號信息之后我們就只能使用 dSYM 來進行符號化了,所以需要將 Debug Information Format 修改為 DWARF with dSYM file。

2.3.2 **Strip Debug Symbols During Copy **(COPY_PHASE_STRIP)

Specifies whether binary files that are copied during the build, such as in a Copy Bundle Resources or Copy Files build phase, should be stripped of debugging symbols. It does not cause the linked product of a target to be stripped—use Strip Linked Product (STRIP_INSTALLED_PRODUCT) for that.

與 Strip Linked Product 類似,但是這個是將那些拷貝進項目包的三方庫、資源或者 Extension 的 Debug Symbol 去除掉,同樣也是使用的 strip 命令。這個選項沒有前置條件,所以我們只需要在 Release 模式下開啟,不然就不能對三方庫進行斷點調試和符號化了。

2.3.3 Symbols Hidden by Default (GCC_SYMBOLS_PRIVATE_EXTERN)

When enabled, all symbols are declared private extern unless explicitly marked to be exported using attribute((visibility("default"))) in code. If not enabled, all symbols are exported unless explicitly marked as private extern.

意思就是設置為yes后,所有的symbols都會被申明為private extern,經過測試,確實可以減小包體積。

工程中的設置如下:

target.build_configurations.each do |config|
    config.build_settings['COPY_PHASE_STRIP'] = 'YES'
    config.build_settings['GCC_SYMBOLS_PRIVATE_EXTERN'] = 'YES'
    config.build_settings['STRIP_INSTALLED_PRODUCT'] = 'YES'

編譯選項優化結果:收益4.2M

3、__TEXT段遷移

iOS的可執行文件就是一個MachO文件,MachO結構主要分為 Header、Load Commands、Data三部分。

  • Header 包含該二進制文件的一般信息,字節順序、架構類型、加載指令的數量等。使得可以快速確認一些信息,比如當前文件用于32位還是64位,對應的處理器是什么、文件類型是什么。

  • Load Commands 是一張包含很多內容的表。 內容包括區域的位置、符號表、動態符號表等。它們描述了 Data 在二進制文件和虛擬內存中的布局信息,有了這個布局信息就能夠知道 Data 在二進制文件中和虛擬內存中是怎樣排布的。

  • Data 存儲了實際的內容,通常是對象文件中最大的部分,包含Segement的具體數據,如靜態C字符串,帶參數/不帶參數的OC方法,帶參數/不帶參數的C函數。

以下是在MachOView中查看的結構:

app包瘦身

Data的結構又可以分為多個Segment,主要有__PAGEZERO__TEXT__DATA__LINKEDIT :

  • __PAGEZERO 是在可執行文件有的,動態庫里沒有。這個段開始地址為0(NULL指針指向的位置),是一個不可讀、不可寫、不可執行的空間,能夠在空指針訪問時拋出異常。

  • __TEXT 是代碼段,里面主要是存放代碼的,該段是可讀可執行,但是不可寫。

  • __DATA 是數據段,里面主要是存放數據,該段是可讀可寫,但不可執行。

  • __LINKEDIT 段用于存放簽名信息,該段是只可讀,不可寫不可執行。

其中的每一個Segment又可以分為一個或多個Section,而__TEXT是Data中的一個Segment。

__TEXT段遷移的方式:

一個Mach-O文件構建的構成主要包括 預處理 -> 編譯 -> 匯編 -> 鏈接 等 4 個階段。

我們通過在 Other Linker Flags 中添加參數可以在鏈接期移動Section。

-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring

-Wl,-segport,__RODATA,rx,rx

其中 -Wl 的作用就是告訴 Xcode 它后面的參數是添加給 Ld 鏈接器的,這些參數將在鏈接階段生效。

第一行參數會新創建一個 __RODATA 段,并把 __TEXT,__cstring 移動到 __RODATA,__cstring

第二行參數是給 __RODATA 賦予可讀和可執行權限。

我們先來看移動__TEXT,__cstring前的 Mach-O 文件:

app包瘦身

構建完成后再來看一下移動__TEXT,__cstring后的 Mach-O 文件:

app包瘦身

這樣就成功的移動了__TEXT段中的一些Section。

facebook早期解決__TEXT段大小限制問題就是使用的這種方式,具體參考: Analysis of the Facebook.app for iOS

Facebook avoids this limitation by moving some if the __TEXT sections into the read only __RODATA segment. Implementing this trick is really simple: you just need to add a linker flag to rename the chosen sections. And it appears you need absolutely nothing at runtime: the renamed sections will be found automatically. This linker flag is described in the ld man page:

-rename_section orgSegment orgSection newSegment newSection

Renames section orgSegment/orgSection to newSegment/newSection.

You could use it to rename the (__TEXT, __cstring) section to (__RODATA, __cstring) by simply adding this line into the Other Linker Flags (OTHER_LDFLAGS):

<pre data-language="plain" id="67Kfj" class="ne-codeblock language-plain" style="border: 1px solid rgb(232, 232, 232); border-top-left-radius: 2px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; background-color: rgb(249, 249, 249); padding: 16px; font-size: 13px; color: rgb(89, 89, 89);">-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring</pre>

今日頭條在減小下載大小時也采用了這種方式,

通過在Other Linker Flags中添加下面的參數,就可以達到這樣的目的

app包瘦身
app包瘦身

這里的作用就是對__TEXT段中的section移動到其他section,然后賦予讀權限和可執行權限。

那么__TEXT段遷移為什么會減小下載大小呢?

原因就是App在上傳到App Store Connect后,蘋果會對其進行加密,然后壓縮成ipa。加密對可執行文件本身的大小幾乎沒有影響,但是卻大大影響了壓縮效率。而__TEXT段又是加密段中最主要的一部分,通過減小__TEXT段就可以減小加密范圍,所以就可以將__TEXT段中的一些Section遷移到其它Segment中。

優化結果:安裝大小減小0.2M,下載大小減小25M。

4、三方庫相關

1、推動直播SDK使用精簡版,由于直播場景不需要實時音視頻、超級播放器SDK以及 AI 特效組件的能力,所以修改了直播SDK。為了防止更換SDK出現問題,進行了兩個版本的灰度觀察,經過充分測試無誤,才確定更換。

2、推動一些SDK的刪除。刪除了一些可以被取代的SDK。

優化結果:收益7M。

四、資源優化

資源優化主要是對圖片資源和其他一些json、音頻、視頻等資源的優化。

1、PNG圖片壓縮

png壓縮主要對比了兩種方案:

TinyPNG

有損壓縮,主要是使用Quantization的技術,通過合并圖片中相似的顏色,通過將 24 位的 PNG 圖片壓縮成小得多的 8 位色值的圖片,并且去掉了圖片中不必要的 metadata,這種方式幾乎能完美支持原圖片的透明度。

網站: https://tinypng.com/

ImageOptim

無損壓縮,圖片文件中往往包含一些注釋、顏色 Profile 等多余信息,移除后圖像質量不變,體積更小載入更快。ImageOptim 以此方式壓縮圖片,先分析圖片,找到最優壓縮參數,去除無關信息減小體積。

網站:https://imageoptim.com/mac

經過壓縮測試,發現TinyPNG壓縮效果遠好于ImageOptim,TinyPNG壓縮比約為65%,ImageOptim壓縮比約為30%,并且肉眼看起來無差異。

使用過程中發現,一些png圖片雖然壓縮后變小了,但是打包后變化并不明顯,有些甚至變大了。通過分析ipa中的png圖片以及查閱資料了解到,由于蘋果本身也會對png圖片進行壓縮,這個壓縮過程是為了加快對圖片的處理速度,將其轉換為更方便處理的格式--CgBI格式。并且修改了存放實際的圖像數據的IDAT數據塊,改變了決定IDAT數據塊大小的Filter方式、zlib的壓縮方式。因為CgBI的IDAT是BGRA格式的,所以不管之前的IDAT是否有Alpha通道,在處理的時候,都會增加alpha通道,其次就是因為每一行數據的filter不同,蘋果處理的時候,默認每一行都使用相同的filter,而原始文件則可以通過更好的算法,對不同的數據行使用不同的filter,為后面的數據壓縮提供更容易壓縮的數據。因此蘋果對于png的優化可能會導致部分png圖片變大的情況。

所以對于部分壓縮后的png圖片,我們也會采用轉為WebpP的方式進行進一步處理。

優化結果:收益5M

2、PNG圖片轉為WebP圖片

相較于PNG格式, WebP具有更加優秀的圖像數據壓縮算法,能帶來更小的圖片體積。所以會對一些較大的圖片轉換為WebP圖片。

壓縮采用的是: cwebp -- Compress an image file to a WebP file

安裝方式:brew install webp

使用方式:

cwebp [options] -q quality input.png -o output.webp

其中option可選:-loss(有損壓縮,默認),-lossless(無損壓縮)

-q:質量指數(壓縮率),有損壓縮有效,無損壓縮忽略

input.png:待轉換圖片

-o:輸入圖片名稱格式

#png 轉換為webp
toWebp() {
    filePath=echo $1 |sed 's/ /\ /g'
    fileName={fileName##*/}
    fileName=echo $fileName|sed 's/ /_/g'
    fileName=${fileName%.*}
    # 靜默模式 轉換時將不會打印轉換日志
    if [[ -e $LOCAL_CWEBP_PATH  ]]; then
       cwebp -quiet "$filePath" -o $newFilePath$fileName.webp
    else  $basedir/bin/cwebp -quiet "$filePath" -o $newFilePath$fileName.webp
    fi

    echo $filePath
    printResult $? "${filePath##*/} ? $newFilePath$fileName.webp"
}

在轉換時,本以為腳本將png圖片轉為webp圖片,然后hook圖片加載方式就可以讀取webp圖片了,但是發現png圖片是由imageset管理的,代碼中使用的圖片名稱和png圖片名稱可能不一致。

解決方法:腳本轉換成webp的時候,不能直接使用png圖片名稱,而是要使用管理png的imageset名稱。

解決完,run起來后又發現了另外一個問題,圖片展示都放大了,經過排查,發現png有1x、2x、3x三種,一個60x60像素的3x圖片生成的UIImage對象scale為3,size為20x20,但是轉為webp再生成UIImage之后,UIImage的scale為1,size為60,所以顯示時圖片變大了。還好在SDImageCoder中找到了一個修改scale參數的轉換方法。

/**
 Decode the image data to image.
 @note This protocol may supports decode animated image frames. You can use `+[SDImageCoderHelper animatedImageWithFrames:]` to produce an animated image with frames.

 @param data The image data to be decoded
 @param options A dictionary containing any decoding options. Pass @{SDImageCoderDecodeScaleFactor: @(1.0)} to specify scale factor for image. Pass @{SDImageCoderDecodeFirstFrameOnly: @(YES)} to decode the first frame only.
 @return The decoded image from data
 */
- (nullable UIImage *)decodedImageWithData:(nullable NSData *)data
                                   options:(nullable SDImageCoderOptions *)options;

但是另外一個問題又出現了,本想著統一由3x的png圖片轉為webp,然后scale參數傳3,但是由于以前圖片管理不規范,導致png圖片有些只有1x圖,有些只有2x或者3x圖,所以這里還需要根據webp是哪種png圖片轉換來的,傳對應的scale參數。

然后后面測試時又發現部分圖片加載不出來,排查發現這些圖片是在xib中讀取的,而xib讀取png的方式并不通過imageNamed方法,然后首先第一個想法自然是hook xib讀取png的方法,但是蘋果并沒有暴露給我們xib加載png的方法。也有一些資料說可以通過hook UINibDecoder的decodeObjectFotKey方法,但是覺得并不十分嚴謹,所以xib中的使用的png, 我采用了另外一種方式:在代碼中將控件使用imageNamed方法再讀取一遍圖片。

還有一點需要注意的是,一些可以支持區域拉伸的png圖轉為webp后拉升是會變形的。這部分圖是不適合轉為webp的。

優化結果:收益6M

3、修改組件庫中圖片管理方式

Asset Catalog,是Xcode提供的一項圖片資源管理方式。每個Asset表示一個圖片資源,但是可以對應一張或者多張PNG圖,比如可以提供@1x, @2x, @3x多張尺寸的圖進行適配;

Asset Catalog中的圖片,在編譯時會被壓縮,然后在App運行時,可以通過API動態根據設備scale factor來選擇對應的真實的圖片渲染,使用Asset Catalog管理的圖片會在ipa包中生成一個Assets.car文件。

App Thing,是蘋果平臺上的一個用于優化App包下載資源大小的方案。在App包提交上傳到App Store后,蘋果后臺服務器,會對不同的設備,根據設備的scale factor,重新把App包進行精簡,這樣不同設備從App Store下載需要的容量不同,3x設備不需要同時下載1x和2x的圖。

但是,這套機制直接基于Asset Catalog,也就是說,只有在Asset Catalog中引入的圖片,才能享受到App Thinning。直接拷貝到App Bundle中的散落圖片,所有設備還是都會全部下載。

因此盡量提升Asset Catalog利用率,是一個很大的包大小優化點。

app包瘦身

所以在使用cocoapods進行組件庫管理時,組件庫中的PNG圖片也都使用Asset Catalog來管理。

除此之外還有一個資源引入方式的不同:pod中的資源引入方式有兩種,resource_bundlesresources。

使用resources,會在主bundle中導入。這種方式讀取圖片不需要修改讀取方式。

s.resources = ['ResourcesTest/Assets/*.xcassets']

使用resource_bundles,會在主bundle中生成一個自定義的bundle,bundle中存放著資源。讀取資源時需要到對應bundle下讀取。這種方式可以避免命名沖突。

s.resource_bundles = {
     'ResourcesBubdlesTest' => ['ResourcesBubdlesTest/Assets/*.xcassets']
   }

在修改的過程中,也有一些意外收獲,發現項目中部分組件庫引入資源方式存在問題,同時指定了resource_bundlesresources兩種方式,這樣會導致圖片既存在main bundle中又存在resource_bundles生成的bundle中。

這里推薦使用resource_bundles + Asset Catalog的方式來管理組件庫中的PNG圖片。

優化結果:收益4.5M

4、刪除無用PNG圖片

通過工具篩查:LSUnusedResources

app包瘦身

優化結果:刪除圖片56張,收益1.2M

5、壓縮文本文件

玩物得志APP中有一些Lottie動畫的json文件存放在本地,通過對這些文件的打包壓縮也取得了一些優化效果。

  1. 將本地中較大的json文件放到一起壓縮成zip;

  2. 啟動時在異步線程解壓zip,存放到沙盒中;

  3. 運行時從沙盒中讀取json;

app包瘦身

后續可以對更多的資源類型采用這種方式,比如音頻、視頻。

優化結果:收益1.2M

五、包大小監控

為了控制增量,我們也對每個版本做了包大小的監控。

針對可執行文件的變化,我們采用的是LinkMap來分析每個組件的大小變化并進行記錄。

針對資源的變化,我們也會從每個版本的ipa包中分析出資源大小的變化并記錄。

app包瘦身

以后,我們計劃對增量進行卡口,結合盤古打包平臺進行包體積自動分析,在每次分支被合并到主分支之前,就能體現出增量大小,這樣可以使得開發對自己開發的代碼有更直觀的感受,加強開發在日常編碼中的瘦身意識,有意識的去對資源、代碼進行清理。爭取做到包體積的零增量。

六、效果

以上所有方案都在玩物得志APP中進行了實踐和落地,經過一系列優化,玩物得志APP包體積整體收益如下:

下載大小由136.2M降為78.6M,減小57.6M

安裝大小由174.5M降為140M,減小34.5M

app包瘦身

下載大小最直觀的體現就是下載時間的長短,以下對比了3.5.0和3.6.7兩個版本的下載安裝時長:

優化前:下載安裝時長為64s

優化后:下載安裝時長為43s

下載時長縮短32.8%

以下是記錄的優化以來各個版本下載大小與安裝大小的變化:

app包瘦身
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • 安裝包組成: 談到 App 瘦身,最直接的想法莫過于分析一個安裝包內部結構,了解其每一部分的來源。解壓一個 ipa...
    孔雨露閱讀 3,337評論 1 7
  • 目錄一:摘要二:安裝包組成三:系統優化四:資源優化五:可執行文件優化六:編譯器優化七:拓展 一,摘要 眾所周知蘋果...
    你好小老虎閱讀 426評論 0 1
  • 前言 隨著APP的業務迭代,需求累積的越來越多,APP內引入的庫或是各種業務功能代碼也累積的越來越多,APP的包體...
    topws1閱讀 2,599評論 1 2
  • 對 App 包大小做優化的目的,就是節省用戶流量,提高用戶下載速度。App Store 規定了安裝包大小超過 15...
    rhc2008閱讀 598評論 0 0
  • 應用距離上次瘦身已接近一年時間,恰好版本修改了一些功能實現,需要刪除一些三方庫使用,所以借機進行一次應用瘦身。在此...
    FieryDragon閱讀 2,573評論 0 11