【騰訊Bugly干貨分享】JSPatch 成長之路


本文來自於騰訊bugly開發者社區,非經作者同意,請勿轉載,原文地址:http://dev.qq.com/topic/579efa7083355a9a57a1ac5b

Dev Club 是一個交流移動開發技術,結交朋友,擴展人脈的社群,成員都是經過審核的移動開發工程師。每周都會舉行嘉賓分享,話題討論等活動。

本期,我們邀請了騰訊WXG iOS開發工程師——bang 陳振焯,為大家分享《JSPatch成長之路》

如何加入 Dev Club?

移動端開發經驗 >= 2 年,微信掃描下方群管理微信二維碼,備注姓名-公司(或產品) 申請加入。

外部群二維碼


分享內容簡介:
JSPatch 是 iOS 上的動態更新框架,只需要引入小小的引擎文件,就可以用 JS 調用和替換任意 OC 方法。目前被普遍用於實時修復 bug,已有超過2500個 APP 接入,本次分享介紹 JSPatch 發展過程中遇到的問題和解決思路。
(此內容已在 GMTC 線下分享過,本次重新整理為線上分享)

內容大體框架:

  1. 起步:介紹 JSPatch 的誕生和當時碰到的難題
  2. 發展:介紹 JSPatch 如何補全周邊功能變得更好用
  3. 下一步:介紹 JSPatch 下一步的計划

分享人介紹:

bang 陳振焯 廣州研發部 目前負責開發微信讀書iOS端,博客 http://blog.cnbang.net


下面是本期分享內容整理


大家好,我是 bang,目前在廣州研發部做微信讀書 iOS 端,今天分享的主題是《JSPatch 成長之路》。

我在去年5月發布了 JSPatch (https://github.com/bang590/JSPatch) 這個開源庫,現在廣泛應用於 iOS 的熱修復,今天分享一下 JSPatch 過去一年以來的成長。

分享共分為三個部分:

  1. 起步 —— 介紹JSPatch的誕生和當時碰到的難題
  2. 發展 —— 介紹JSPatch如何補全周邊功能變得越來越好用
  3. 下一步 —— 介紹JSPatch下一步的計划

一、起步

先說下起步階段。當時碰到的一個問題是:APP 線上 bug 修復周期長,成本高,版本發布出去后,發現一個 bug,要修復這個 bug 就必須得另外發一個版本,也就是要經歷:測試——打包——發布——審核——用戶下載,這一系列過程,成本非常高,最后還很難讓所有用戶都升級上來。

當時業界已有一個解決方案,叫 waxPatch,它是在APP里嵌入 lua 引擎,然后通過 OC 的 runtime 接口在 lua 里調用和替換 OC 方法,這樣就可以下發 lua 腳本替換原生代碼,動態修復 bug。

waxPatch: https://github.com/mmin18/WaxPatch

這是個不錯的方案,但當時的 waxPatch 存在很多缺陷:

首先是 wax 已經多年不維護了,導致不支持一些 block/64 位等新特性,此外當時 wax 還有文檔不足,測試不足,線程不安全,難以調試等坑。

於是開始探求更好的解決方案。很容易想到如果用 JavaScript 做這個事情的話,相對 lua 原生就有一些優勢:

  1. iOS 里已內置 JavascriptCore 引擎,無需再另外嵌入。
  2. JS 在終端應用廣泛,很多混合開發內嵌 H5 頁面就是用 JS。
  3. 符合蘋果審核規則,蘋果在文檔里說明不可以下載可執行的代碼,由 JavascriptCore 執行的除外

那么有沒有人試過這樣做呢?用 Javascript 調用和替換 OC 方法,當然是有的。

  • 在當時有一個開源庫 JavascriptBridge (https://github.com/kishikawakatsumi/JavaScriptBridge), 它可以用 JS 調用 OC 接口。
不過它用的是 JavascriptCore 原生的接口做的,需要事先在 OC 里定義好要調用的接口,沒有事先定義的不能調,這導致它的實現很臃腫,因為要在 OC 定義大量的方法。此外它也不能替換 OC 方法,實用性很低。

  • 當時國外還有一個熱補丁服務叫 rollout (http://rollout.io)。 它是一個服務平台,底層也是用 JS 調用和替換 OC 方法去實時修復 bug,不過它不是開源的,只能在這個平台上用,另外它的 JS 寫法是比較復雜的,看看這個例子就知道,這導致它不得不在平台上做一些便捷的功能,把一些常用的操作封裝起來,減少使用者寫代碼。

總的來說,當時並沒有一個更好的方案,於是想自己造個。

當時期望做到的效果是這樣的:

我在 JS 寫 UIView.allOC(), 然后傳給 JavascriptCore 執行,JavascriptCore 把我要調用的信息,這里類名是 UIView,類方法名是 alloc 傳遞給 OC,OC runtime 就可以找到這個類和方法進行調用。這是最基本的一個語句調用。

實際上當時實現這個最基本的調用就遇到一個檻,在 JS 里這條語句根本無法執行:

要讓這條語句在 JS 環境中可以執行,在 JS 的語言規則下,UIView 必須是一個對象,alloc 必須這個對象的一個方法。

也就是說要像這樣定義后才可以執行:

UIView 必須是一個對象這點沒問題,在調用前定義就可以,但 UIView 的方法必須在調用前定義就很糟糕,這意味着如果你想調用任意 UIView 的方法,你就需要提前把所有 UIView 的方法都找出來,每一個方法都要預先定義好。

也就是說在使用UIView之前,需要先去 OC 把UIView所有方法找出來,然后構建UIView對象,每個方法都在這個對象里生成對應的函數,然后你才可以調用UIView的任意方法。

JSPatch 在開發時就嘗試過這種方案,后來發現這些對象的方法太多了,僅 NSObject 基類的實例方法就有830個,類方法有118個,這導致在JS生成的對應的對象占用內存極高,NSObject就占了1.3M,UIView占2M。這根本不可用。

對此我還進行了一些優化嘗試,例如去除掉里面的下划線開頭的私有方法,在 JS 構造繼承鏈共用基類方法。但這些優化都沒多少效果,占用內存依舊很高。當時就覺得不太可能實現。

實際上當時我陷入了一個思維定勢,做終端久了,思維停留在 iOS 的 OC 世界,寫代碼必須遵守語言的規則,上述的困難也是在遵守 JS 語言規則這個前提下碰到的。

如果有方法不遵守語言規則呢?實際上在 JS 界,有個很常用的伎倆,就是預編譯:

也就是我們寫的腳本不直接拿給 JS 引擎執行,而是進行一些轉換后才執行,在現代框架這個用法很常見, react/vue 都用了,甚至還有像 coffieScript 這樣把 JS 完全換成另一種語言的做法。

想到這一點,剛才的問題就很好解決了。

只需要把所有函數調用在執行前都替換一遍,變成去調用一個固定的 __c 函數, 這個 __c 函數模擬了 ruby/lua 等語言的元方法機制,對方法調用進行轉發。

還是以調用 UIViewalloc 方法為例,這個語句在執行之前會被替換為調 UIView 的 __c 方法,把 “alloc” 作為字符串參數穿進去,在 __c 方法里判斷調用者 UIView 是不是 OC 對象。如果是,就把類名和傳進來的方法名傳到 OC 層進行調用,如果不是,就調用回 JS 這個對象的方法。

這樣做簡潔高效地解決了前面的問題,不需要去 OC 遍歷每個類的方法,不需要存儲這些方法,就可以調用任意 OC 方法,只需要給 JS 基類定義一個 __c 方法就可以了。正則替換后無論調用 OC 的什么方法,都不會有語法錯誤,因為都變成調用這個 __c 方法,在這個 __c 方法里做處理去 OC 層調用相應的方法就行了。

使用這種方案后內存的占用下降了 99%,甚至更多,也使 JSPatch 的使用成為可能,這是 JSPatch 最核心的一點。

解決這個核心問題后,后面就是細化功能了,JSPatch 發布以后一直在完善,包括最基本的調用和替換OC方法,還有支持64位,支持 block,支持包括 c指針/Class/結構體等類型,支持 c函數的調用等,這里面的細節原理我覺得看文章會比較清晰,相關文章都可以在 github wiki (https://github.com/bang590/JSPatch/wiki) 上找到,這里就不多說了。

二、發展

接下來說說 JSPatch 是怎樣進行進一步發展的。

在完善 JSPatch 的同時,我也在想,如何把 hotfix 做得更好。

主要有兩個思路:

  1. 降低使用門檻
  2. 提高使用效率

我們一個點一個點來看對這兩個問題是怎么做的。

首先 JSPatch 在易用性上一直堅持着一個理念,就是 keep it simple and tiny,用中文說就是保持精巧,保持好用。JSPatch 從開源到現在一年多,增加和完善了很多功能,但它的使用方式和接口都沒有變過,一直以來都是只有三個文件,拖入項目直接可以使用,也會很謹慎地新增接口,不會影響到舊接口的使用,不會出現同一份代碼在舊版本能用,在新版本不能用的情況,在易用性上降低使用門檻。

另一個問題是安全問題

JSPatch 可以調用和替換任意 OC 方法,權限很大,如果在傳輸過程中被第三方攻擊,替換了下發的代碼去執行,會對用戶和 APP 本身造成很大傷害。如果每個接入 JSPatch 的人都要考慮這個安全問題,接入門檻就會很高,也可能會因為考慮不周全導致 APP 處於危險狀態。

對此當時詳細考慮了安全策略。對這種情況:

  • 最簡單的方案是直接對腳本加密,后台使用固定密鑰加密腳本后下發,客戶端使用同樣的密鑰解密。這種方案的優點是簡單,缺點是這個密鑰必須存在客戶端,黑客很容易破解拿到這個密鑰,然后通過傳輸過程第三方攻擊,下發同樣用這個密鑰加密的惡意代碼,就沒有安全可言了。

  • 第二個方案是讓腳本通過 https 傳輸,這個方案的優點是安全性高,只要客戶端對證書進行過足夠的驗證,就能很好地保證安全性。缺點是這個方案門檻高,部署繁瑣,需要購買證書,對一些中小 APP 來說可能難以接受,並且如果用戶手機信任了一些惡意證書,也還是存在被攻擊的危險。

  • 第三個方案是使用RSA簽名驗證。
    整個流程是這樣:

第一步服務端計算腳本文件的MD5值,用存在服務端的私鑰對這個MD5值進行加密,然后把這個MD5值和腳本一起打包下發給客戶端。客戶端拿到腳本和加密后的MD5值,用存在客戶端的公鑰進行解密,拿到服務端計算的MD5值,本地再計算一遍腳本文件的MD5值,對比這兩個值是否一致,若一致則表示傳輸過程中沒有被篡改。

如果第三方要截獲請求下發惡意腳本,第三方必須用私鑰加密這個惡意腳本的MD5值一起下發,才能通過驗證執行,只要服務端不被攻破,第三方就沒有私鑰,也就無法進行篡改。

可以看到這第三個方案門檻低,通用性高,部署簡單,安全性也高,對服務端和客戶端都沒有什么特殊要求。

我把這一套安全方案做成一個組件,叫 JPLoader,也開源在 JSPatch 項目上(https://github.com/bang590/JSPatch/tree/master/Loader), 需要部署 JSPatch 的同學可以直接使用這套組件,解決安全性問題,客戶端的工作就完成了,只剩下后端的工作。

前面把安全性問題解決了,只剩下后端的工作,但搭建后台對使用者來說也是挺麻煩的事,特別是作為 iOS 開發者,在中小公司自己搭后台麻煩,在大公司要后台幫你搭也不容易,這又會導致使用 JSPatch 的門檻還是很高。

於是在想這部分工作能不能也幫使用者省了呢?

對此我搭建了 JSPatch 平台 (http://JSPatch.com), 讓使用 JSPatch 的人不需要搭建后台,直接通過平台下發補丁代碼。

這個平台幾個月前已經開放注冊,現在所有人都可以使用。

在搭建這個平台時,碰到一個問題值得分享一下,就是如何支持高並發?

由於 JSPatch 的補丁特性,補丁需要及時推送給用戶,也就是說至少需要在每次啟動時向服務端請求詢問 APP 是否有新的補丁,有的話下發執行。這里詢問的請求量是很高的,單個 APP 可以控制,但平台要面對多個 APP,累計起來的請求數量會非常多,並發會很高,怎樣支撐這樣的高並發?

正常來說這樣一個系統整體設計大致是這樣的:

平台用戶把腳本放到平台服務端,服務端的數據庫保存着腳本的各種信息和內容,APP 客戶端向平台發起請求詢問是否有新腳本,平台服務端接收到請求后通過 CGI 處理請求參數,根據 APPkey 等參數從數據庫拿出這個 APP的信息,然后組裝數據告訴APP客戶端有沒有新腳本。

這里的詢問請求至少時 APP 每次啟動都要發一次請求,才能保證腳本的更新能盡快下發。請求量大時,這里從數據庫取出數據很容易成為整個系統的瓶頸,CGI 處理請求參數和組裝數據也要耗不少資源。

對此我改用了另一種方式:

平台用戶上傳腳本到平台服務器時,服務端除了把 APP 信息存在 DB 外,同時會另外上傳一份 JSON 靜態文件到靜態雲服務器,JSON 里保存了當前補丁的版本,而這個靜態資源的文件名是由 APPkey/APP 版本號組成的。

例如這里腳本補丁版本號是10,這個JSON靜態文件的內容就是 {v:10}。可以想象靜態文件的訪問路徑就是:

 http://JSPatch.com/{APPkey}/{APP_version}.json

然后 APP 客戶端不再向平台服務端發請求,而是向這個靜態資源服務器發請求,根據 APPKey 和 APP 版本直接請求到這個 JSON 文件,里面帶的版本號信息就可以告訴 APP 腳本是否有更新。

整個流程就變成了:

APP 向靜態服務器詢問是否有新補丁,靜態服務器直接返回預先設置好的 JSON,就結束了。

這樣 APP 永遠不會跟平台服務器打交道,只需跟靜態資源服務器打交道,靜態資源的高並發處理起來就簡單得多,成本也低很多,現在有很多靜態資源雲存儲,直接接入就可以了,以這些雲存儲的能力,支持多高的並發都沒有問題,用戶量多大的 APP 接入都可以支撐到。就是這樣 JSPatch 平台解決了高並發問題,可以投入使用。

接下來在開發效率上,有一個問題是轉換代碼效率低。

我們用 JSPatch 修復 bug 時時以方法為單位進行替換的,若原方法有上百行,你的需求只是修改其中一兩行代碼,你也要把這上百行代碼人工翻譯成 JS 才行。對此我開發了JSPatch Convertor 這個工具,可以自動把 OC 代碼轉為 JSPatch 代碼,提升開發效率。

這個工具也開源在 github 上(https://github.com/bang590/JSPatchConvertor), 支持了大部分語法特性,但目前還做不到支持所有特性,像私有變量/靜態變量/宏這些還不支持,所以轉換后需要人工修改,但還是很大地提高了使用 JSPatch 的效率。

總結下來,在降低使用門檻上,JSPatch 保證了易用性,封裝了安全方案,提供了 JSPatch 平台讓使用者可以直接接入,另外還有完善的文檔和解析文章保證使用無障礙。提高使用效率上,做了 JSPatch Convertor 自動轉換代碼,也內置了一些擴展方便直接調用一些常用的 C函數。

經過不斷發展,JSPatch 可以說是 iOS hotfix 的最佳解決方案。

目前大部分應用都已經接入使用,據不完全統計至少有 2500 個 APP 接入,經過了的大用戶量的考驗。

三、下一步

接下來說說下一步的計划,JSPatch 在 hotfix 上已經做得不錯,目前下一步打算推動使用 JSPatch 開發功能模塊。

JSPatch 做這個事情跟 React Nativeweex 這類方案比起來,會有一些優勢:

  • 首先 React Native 和 weex 都是從前端出發擴展到終端,是前端方案的延伸,他們的體系對於前端來說更熟悉,對於終端來說,意味着要重新學習前端的一套知識,學習成本較高,而 JSPatch 是從終端出發,編碼體系也差不多是直譯 OC,學習成本較低。

  • 第二點是 ReactNative 和 Weex 是比較大型的框架,環境配置都很復雜,也會增大不少安裝包的大小,如果說只想擴展實現一兩個小功能,接入這么大型的框架不合適。而 JSPatch 前面也說了,屬於微型框架,只有三個文件,也無需環境配置。

  • 第三點是 ReactNative 和 Weex 的組件都是要一個個封裝好,難以復用現有的 OC 組件,並且他們都是大型框架,在未成熟階段框架本身實現上的坑會很多,而 JSPatch 可以直接復用所有 OC 現有組件,並且只是薄薄的轉接層,坑會較少。

但 JSPatch 要用於開發功能,有兩個問題:

  1. 開發效率較低
  2. 運行效率較低。

在開發效率上,我做了兩件事去提高,第一個是 JSPatchX 代碼補全插件 (https://github.com/bang590/JSPatchX)。

寫 JSPatch 代碼時並不像 OC 那樣有代碼補全,在調用 OC 長長的方法時效率會很低,而且用 JSPatch 寫功能時,不像 hotfix 那樣有對應的 OC 代碼,也無法使用前面說的 JSPatchConvertor 進行轉換。於是做了 JSPatchX 去彌補這個缺陷,可以在 XCode 自動提示補全 JSPatch 代碼。

另一個是 Playground 即時刷新范例 (https://github.com/bang590/JSPatch/tree/master/Demo/iOSPlayground)

可以實時預覽 JSPatch 腳本執行的結果,無需像原生代碼那樣每一次修改都要 build 重啟才能看到效果,這也是腳本語言的優勢。使用者可以仿照這個 playground 的實現,在開發功能時在自己的頁面實現這樣的即時刷新,這樣一定程度上提高了開發效率。

接下來看看運行效率。

JSPatch 寫功能時運行效率低,於是着手進行優化,第一步是確定瓶頸,發現運行速度最慢的在於在 JS 調用 JS 上定義的新方法。

例如這里新定義了一個dribbbleView類,里面有個新方法renderItem,在 JS 里調用這個新方法時,速度很慢。

分析下這個調用過程:

主要問題在於這個新定義的方法與 OC 掛鈎,這一次普通的調用,需要在 JS 和 OC 之間不斷來回通信,不斷進行參數轉換,經過這9個步驟后才能成功調用。

對此我通過一些手段做了優化,把這樣的方法直接放在 JS 環境上,在 JS 調用這個方法時無需再與 OC 通信,整個調用流程就變成了只有兩步:

經過這個優化后,這樣的方法調用性能最高提高 700 倍,這才使 JSPatch 寫功能變成一件靠譜的事。

除此之外還做了一些其他優化,包括提升新增 property 性能,提供跟定義 OC 類一樣的純 JS 類定義接口,自動轉換參數類型等,具體優化細節可以在這篇文章(http://blog.cnbang.net/tech/3123/) 上看到。

我用 JSPatch 寫了個 Dribbble 客戶端 demo (https://github.com/bang590/JSPatch/tree/master/Demo/DribbbleDemo) 在 iPhone5C 上測試過,滑動性能沒有問題。

最后,可以從這個腦圖看出 JSPatch 的現狀,周邊設施仍在繼續建設中。

我今天的分享就到這里,謝謝。

問答環節:

Q1: JSPatch 的底層原理跟 ReactNative 是不是差不多呢?有受到其啟發么?

JSPatch 的原理跟 ReactNative 是完全不一樣的,JSPatch 是 OC 方法調用和替換的一層轉接,ReactNative 並不會去調用和替換 OC 方法,它有自己的一套通信規則。

Q2: 本身基於OC runtime 對 Swift 的項目如何支持?

Swift 相關問題在 wiki 里有提到:


  1. 只支持調用繼承自 NSObject 的 Swift 類
  2. 繼承自 NSObject 的 Swift 類,其繼承自父類的方法和屬性可以在 JS 調用,其他自定義方法和屬性同樣需要加 dynamic 關鍵字才行。
  3. 若方法的參數/屬性類型為 Swift 特有(如 Character / Tuple),則此方法和屬性無法通過 JS 調用。
  4. Swift 項目在 JSPatch 新增類與 OC 無異,可以正常使用。

Swift 的原生類目前沒找到替換的方法,動態調用倒是可以實現。

Q3: JSPatch 運行一次就會把JS轉換為 OC 緩存起來?那我們可以利用它去做一些重復調用的事情?甚至用來開發?它的效率和原生相近吧?

會緩存一些 methodSignature,但還是得通過反射 (className->class->imp) 去找到要調用的方法,效率會比原生低。但一般程序的瓶頸不會在語言這里。

Q4: 對於 JSPatch 資源更新服務平台還是表示一些擔憂,如果被別人攻破了,豈不是很多 APP 都受牽連了?

JSPatch 平台就算平台被人黑了,也無法對平台上的 APP 下發惡意代碼。只要使用者用了自定義的 RSA 密鑰就可以了,只有使用者有私鑰,每次發布腳本都要使用這個私鑰,平台不會保存它,詳情可見:http://JSPatch.com/DOCs/rsa

Q5: 現在 iOS 加快了審核速度,好像現在是24小時內審核上線。那現在 JSPatch 前景還會好么?

審核只是一個環節,測試/打包/發布/用戶下載,這些其他環節還是不可少,並且最大的問題還是是用戶下載更新不可控。

Q6: Swift 屬於靜態編譯類型,是不是可以利用類似 c函數替換的方法呢?像 fishhook 這樣的工具

fishhook 需要編譯時確定要替換的函數指針,並不能在運行時替換任意 c函數

Q7: 我看網上的一些介紹說 JSPatch 對小的 bug 修復好點,大的還是提交新的版本,但是我看您介紹使用靜態資源服務器管理.應該不存在數據量大,並發的問題.這個您怎么看?

他指的大的 bug 應該是要寫很多代碼才能修復的 bug 吧?這點應該跟 JSPatch 開發效率問題有關,對於大量的代碼他不想原生 OC 寫一套修復,再用 JSPatch 寫一套,跟數據量和並發應該沒什么關系。

Q8: 為何 JSPatch 上面,QQmail 沒有接入?有什么顧慮嗎?

因為QQ郵箱在 JSPatch 出現之前已接入 lua,剛出現時 JSPatch 還不是很成熟,團隊當時想同時使用兩種方案作對比,時間久了也沒有再切換過來了。

Q9: 有沒有可能進一步提升 JSPatch convertor 的能力。最終發到直接打開 Xcode 項目,尋找依賴,通過語法語義分析等,將 OC 轉換為 JS

可以做到的,不過這事要投入很大精力,之前有搞過一個 demo,直接用 OC 寫 Patch,然后在執行前轉換成 JSPatch 代碼,有一個開源庫 JSTalk (https://github.com/ccgus/JStalk) 有基本的 OC->JS 的轉換,但要做到好用還有很多工作。

Q10: 請問如果我的 APP 引入了 JSPatch, 但是產生 crash 的代碼並不是通過 JS 寫的, 而是原生的 OC 代碼, 那么 JSPatch 可以通過下發 JS 腳本修復這種 crash 嗎, 如果可以的話, 原理是怎樣的?

可以,原理就是把導致 crash 出現的方法替換掉,OC 調用那個方法時轉成調用 JSPatch 里寫的替換的方法,就不會 crash 了

Q11: 有沒有意識到 JSPatch 的性能瓶頸最終都取決於 JavascriptCore 的性能?所以低端機永遠是低性能,有沒有想過借鑒 JSX 做點事情呢?

JavascriptCore 的性能並沒有問題,性能瓶頸不是 JavascriptCore,目前來看瓶頸會是 OC 與 JS 通信時大對象的參數轉換,但這是可以避免的

Q12: JSPatch 效率怎么樣啊

效率可以試試上文說的 dribbbleDemo

Q13: 調試 JSPatch 時能不能打斷點,如何定位到 JS 的 crash 堆棧

可以斷點,文檔有寫:https://github.com/bang590/JSPatch/wiki/JS-%E6%96%AD%E7%82%B9%E8%B0%83%E8%AF%95

如果大家對本次分享還有問題,請按以下格式在DEV社區(dev.qq.com)發問答帖

發帖格式:【bang@DEV Club 你問我答】 “問題”

最后,歡迎大家關注 JSPatch 公眾號:JSPatchDev,會即時推送 JSPatch 最新信息以及相關技術文章:

更多精彩內容歡迎關注bugly的微信公眾賬號:

騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合並功能幫助開發同學把每天上報的數千條 Crash 根據根因合並分類,每日日報會列出影響用戶數最多的崩潰,精准定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在發布后快速的了解應用的質量情況,適配最新的 iOS, Android 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM