<acronym id="cr5pu"></acronym>
  • <kbd id="cr5pu"><font id="cr5pu"></font></kbd>
  • <li id="cr5pu"><output id="cr5pu"></output></li>
    <del id="cr5pu"><li id="cr5pu"></li></del><center id="cr5pu"></center>
    <output id="cr5pu"><kbd id="cr5pu"></kbd></output>
  • <rp id="cr5pu"></rp>
    <var id="cr5pu"></var>
  • <nav id="cr5pu"></nav>
  • Jack Jiang

    我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
    posts - 409, comments - 13, trackbacks - 0, articles - 0

    本文由微信客戶端團隊rhythm分享,原題“視頻號直播:如何進一步降低功耗占用?”,本文有修訂和改動。

    1、引言

    功耗優化一直是 app 性能優化中讓人頭疼的問題,尤其是在直播這種用戶觀看時長特別久的場景。怎樣能在不影響主體驗的前提下,進一步優化微信iOS端視頻號直播的功耗占用,本文給出了一個不太一樣的答案。

     
     
    技術交流:

    (本文已同步發布于:http://www.52im.net/thread-4507-1-1.html

    2、問題背景

    問題的起因是我們測試統計發現帶有點贊的直播會比無點贊動畫的直播 GPU 占用要高將近一倍,同時 FPS 差異也很大。

    高刷屏下,PerfDog 測試顯示,有點贊情況下的大部分視頻號直播居然是以60fps在跑,這導致了極高的GPU占用。

    但我們根本沒有60fps 這么高的直播流,且絕大部分直播流都只有30fps 而已,少部分也就最高60fps,怎么到了設備上就達到了60fps?

    而且這還是我們開啟了強制低幀率UIViewAnimationOptionPreferredFramesPerSecond30后的效果,沒開之前直接奔120fps 去了。

    如下圖所示 PerfDog 數據顯示在 13 pro max上直播點贊期間 FPS 直奔120:

    正常情況下,視頻號直播里大部分主播開播流基本都是30fps 以內,也就是正常情況下我們只需要維持30fps 渲染,就能保持好流程的用戶體驗。

    那為什么這里降幀后依舊會出現60fps 呢?

    經過一系列排查我們發現這是由于直播的點贊動畫導致的高幀率,如果去掉動畫后 FPS 就會回到正常情況下了,且 GPU 占用也有了明顯下降。

    這到底是怎么回事?我們是否可以降動畫的幀率降低到某個值來去優化我們整體的 GPU 占用呢?

    3、知識儲備1:iOS中的動畫分類

    在iOS中,大部分動畫的本質就是根據輸入的時間戳,返回對應屬性的動畫參數,從而移動圖像,達到運動的效果。根據動畫 api 實現方式的特點我們可以把動畫 api 劃分為如下幾類。

    3.1UIView block animation

    基于 「+[UIView animateWithDuration:delay : options:animations:completion:]」 動畫api驅動的動畫,特點是所有動畫都在 animations block 里同步觸發,可以方便的設置任何屬性動畫。

    [UIView animateWithDuration:duration

                                 delay:0

                               options:option

                            animations:^{

                                view.top -= offsetY;

                                view.left -= offsetX;

                            }

                            completion:completion];

    3.2CAAnimation

    基于 「CAAnimation api」 直接觸發提交的動畫,例如:

    CABasicAnimation *ani_position = [CABasicAnimation animationWithKeyPath:@"position"];

    ani_position.fromValue = @(val.position.from);

    ani_position.toValue = @(val.position.to);

    [view.layer addAnimation:group forKey:key];

    3.3Timer

    基于 「NSTimer/GCD」 觸發的動畫,例如定時去修改某個 imageView.image,使得它能定期變換的效果?;?「CADisplayLink」 觸發的動畫,和基于 NSTimer 觸發類似,只不過這個 timer 源是和渲染保持一致的,能夠做到更流暢更貼合。

    比如我們要實現自定義的 UIScrollView 動畫,就可以基于 CADisplayLink 來做。

    3.4UIViewPropertyAnimator

    「UIViewPropertyAnimator」是iOS10開始蘋果推動的新的動畫api,相比 UIView block animation 可以更靈活的控制動畫的過程。

    [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:duration

                                         delay:0

                                       options:option

                                    animations:^{

                                        view.top -= offsetY;

                                        view.left -= offsetX;

                                    }

                                    completion:completion];

    4、知識儲備2:iOS中的動畫渲染

    iOS中的動畫或者 UIView 的修改到底是怎么被渲染到屏幕上去的?

    4.1Core Animation Pipeline

    iOS 的 UI 更新和動畫操作都離不開 Core Animation 和 UIKit,他們的底層都是 QuartzCore,所有的 UI 刷新和動畫提交都會打包成對應的 CA::Transaction 和 CAAnimation 對象并提交給 Render Server 去處理。

    App 本身并不負責渲染,渲染是由獨立的進程 Render Server 來負責的,Render Server 最終調用 -[AGXG14FamilyRenderContext drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:] 等 GPU 接口來完成 GPU 任務的提交,最終觸發屏幕更新操作。

    整體過程大概如下:

    • 1)App 處理事件,例如 touch 事件或者 displaylink timer 事件;
    • 2)App 完成視圖的 layout、圖像 decode 等操作,并觸發 CA::Transaction 提交;
    • 3)Render Server 接收 App 提交的 Transction 和圖片數據,Render Server 可直接跨進程訪問 App 進程的位圖內存資源,并最終觸發 GPU 調用;
    • 4)GPU 最終完成了圖像的渲染并顯示到屏幕 Display。

    4.2Render Server

    如下圖所示,最終的上屏任務提交操作是由 Render Server 也即 backboardd 進程來最終觸發的。

    在 iOS 中 Render Server 通常指的是 backboardd 進程,backboardd 進程是一個與 SpringBoard 守護進程一起運行的守護進程。

    它在 iOS 6 中引入,旨在減輕 Springboard 的一些職責,主要是事件處理的職責。它主要負責把 touch 事件分發到 app 進程以及處理 app 進程觸發的動畫和UI更新操作。

    如上圖所示,time profiler 里我們能清晰看到 backboardd 進程在處理來自 app 進程的圖像提交操作。

    微信直播之前就遇到過好幾次 Render Server 命中了 gpu io fence 導致系統全局卡死的問題。

    如下圖 ips 文件日志所示:

    4.3Render Loop

    Render Loop 是包括了從 app 到 Render Server 再最終到屏幕的一系列任務觸發,刷新,更新與提交,直到上屏的一系列過程,是對渲染管道的進一步封裝,類似于一套 runloop 循環機制,能隨時的處理輸入和輸出。

    4.4動畫渲染

    當我們調用-[UIView animateWithDuration:animations:] api觸發動畫后,整體動畫渲染過程如下圖3步所示。

    4.5幀率

    幀率即 FPS(frames per second),每秒渲染了多少幀,正常情況下只要我們定期提交一次 opengl 上屏 [curContext presentRenderbuffer:GL_RENDERBUFFER] 就會觸發一幀的上屏操作,這就回導致 FPS 發生變化,也最終影響了 app 的性能占用。

    FPS 越高對于游戲等高清視頻效果就更細膩更好,但是并不是所有情況都需要高 FPS,部分情況下高 FPS 反而導致了無用的功耗但并沒有帶來更好的體驗。

    4.6屏幕刷新率

    對于 iOS15/iPhone 13以前的設備,屏幕是固定的刷新率,在這之后 iPhone 13和 iPad Pro 后引入了高刷屏,并且支持了動態刷新率。

    對于直播場景 FPS 有3個:

    • 1)視頻流 FPS;
    • 2)Render Server FPS;
    • 3)屏幕 FPS。

    對于非可變刷新率的屏幕,我們可以盡可能減少 GPU 的幀率(即 Render Server 提交的 FPS)來達到降低 GPU 功耗的目的,對于可變刷新率的屏幕,那只要減少了 GPU 幀率就自然而然也減少了屏幕的刷新率,使得屏幕和 GPU 功耗都下降了。

    在我們遇到的問題中,我們的視頻流 FPS 是25,那么我們預期的最終 GPU FPS 和屏幕 FPS 理應同理也是接近25才是,而這里卻達到了60fps,說明了有重復的內容幀一直被 Render Server 重復的復制并提交給 GPU,導致了畫質細節沒有增加,但頻繁的拷貝渲染造成了更高的 GPU 占用。這就是我們的問題所在。

    5、知識儲備3:iOS中的動畫降幀

    5.1概述

    結合上文,我們要解決直播幀率異常升高的問題,就需要解決點贊動畫的高幀率問題。

    很幸運,蘋果在 iOS15提供了一個 CAAnimation 的 api,即-[CAAnimation preferredFrameRateRange],它接受3個參數分別指定minimum 幀率,maximum 幀率,以及 preferred 幀率,基于這個api我們可以對于 CAAnimation 動畫設置幀率。

    蘋果的建議是把動畫分為了如下幾檔:

    5.2CAAnimation 降幀原理

    iOS15開始蘋果引入了 CAFrameRateRange 相關 api 來供 app 去設置 CADisplayLink 和 CAAnimation 的 preferredFrameRateRange,以方便調節幀率,達到在高刷機上能進一步降低功耗的目的。

    那它又是如何工作的呢?

    首先需要明確 iOS15后 CAAnimation 和 CADisplayLink 的幀率控制底層都是一致的,也就是都是 CA:: Display: : DisplayLinkItem 來驅動觸發的。而動畫的本質就是根據時間的輸入來得到對應的動畫 fraction 并觸發對應進度的動畫修改,再提交上屏完成修改。

    具體而言,我們以 UIScrollView的 setContentOffset:animated 動畫為例。

    5.3setContentOffset:animated 動畫機制

    當我們觸發[scrollView setContentOffset:CGPointMake(120,0) animated:YES]后,會觸發創建一個 UIScrollViewAnimation 的實例對象(UIAnimation的子類),接下來會調用 UIUpdateSequenceInsertItem 將這個動畫實例注冊到當前的 UIUpdateCycle 循環中。

    UIUpdateCycle 負責根據設備的 CADisplay 屏幕刷新率和設置動態效果里設置的是否限制幀速率來抉擇出到底是以120hz還是60hz來驅動 UIUpdateCycle 循環的觸發,當以120hz觸發動畫循環時,接著會在每8ms間觸發一次_UIUpdateSequenceRun,來執行 UIScrollViewAnimation 的動畫 progress 計算操作。

    如圖:

    每次觸發觸發 _UIUpdateSequenceRun 時,會向 UIScrollViewAnimation 請求[UIAnimation fractionForTime:]來返回對應時間戳的 contentOffset 和 progress,然后觸發修改 contentOffset,最終接近目標 contentOffset 后就完成了完整的動畫。

    5.4CAFrameRateRange

    當我們設置 CAAnimation 的 preferredFrameRateRange 后,QuartzCore 會將 CAFrameRateRange 轉為CAFrameIntervalRange 結構,并最終嘗試觸發 Render Server 在指定幀間隔內渲染每一幀動畫。

    如下圖:

    5.5幀率變化探索

    所有的幀提交操作最終都是在 Render Server 觸發的,也就是只有從 Render Server 統計FPS才是最終的實際 FPS,那我們要怎么統計呢?

    QuartzCore 提供了一個系統級的面板工具,它可以很方便的顯示當前的 QuartzCore 渲染信息,包括fps,frame duration等一應俱全。

    我們可以在越獄后給 app 自簽名 com.apple.QuartzCore.debug 這個 entitlement 后,再調用如下代碼所示的私有 api 即可全局打開這個面板,可以方便的在手機端查看 Render Server 上的實際 FPS。

    extern"C"{

    intCARenderServerGetDebugOption(mach_port_t port, intkey);

    intCARenderServerGetDebugValue(mach_port_t port, intkey);

    voidCARenderServerSetDebugOption(mach_port_t port, intkey, intvalue);

    voidCARenderServerSetDebugValue(mach_port_t port, intkey, intvalue);

    }

    由于以上能力無法在非越獄設備上開啟,所以實際上我們無法檢測 app 在任意時刻的 FPS 變化情況。

    不過經過分析,我們發現只要觸發了以下行為就可能代表要幀率要變化了。如下。

    1)在設置->動態效果里開啟或關閉“限制幀速率”:修改限制幀速率會觸發系統拋出 com.apple.CoreAnimation.CAWindowServer.DisplayChanged 的通知,QuartCore 會在啟動時注冊這個通知,并收到通知后通過 mach port 通信獲取當前注冊的幀速率值,以動態修改 displaylink 的回調頻次。

    2)直接通過 opengl/metal api 提交一幀畫面給 Render Server。

    3)觸發 CA::Transaction 對象的提交:除了觸發動畫提交,觸發 view property 提交變更外,甚至創建 view 也會導致 source0觸發一次,如下圖所示。

    通過調試分析,我們大概清楚了 iOS15引入的 CAAnimation 的 preferredFrameRateRange 工作機制,如下圖所示。

    我們只要修改 UIUpdateSequenceRun 的回調頻率,也就是 -[UIAnimator _advanceAnimationsOnScreenWithIdentifier:withTimestamp:] 的回調頻次我們就能控制部分系統動畫的幀率,強制調節他的執行頻次,或者我們通過模擬系統設置->動態效果->限制幀速率的實現方式,主動調用 -[CADisplay overrideMinimumFrameDuration:] 傳入4便可將 UIAnimator 的刷新率調節為 240/4=60hz,或者傳入8即可將系統動畫刷新率調節為 240/8=30hz。

    基于以上研究,理論上我們可以嘗試調用私有 api 來全局控制 CADisplay 的刷新率,來進一步降低性能占用,但是由于 Render Server 是在其他進程,我們還是無法控制 Render Server 的刷新率,并且私有 api 會導致 app 被拒審,所以我們最終依舊只能改造部分系統動畫實現以繼續基于 CAAnimation api 去優化幀率。

    6、我們的優化方案

    6.1概述

    從 iOS15開始蘋果新增加了 preferredFrameRateRange api 可用于設置相應動畫或timer的刷新頻率,我們就可以基于該方案去改造相應動畫即可。

    @propertyCAFrameRateRange preferredFrameRateRange

        API_AVAILABLE(macos(12.0), ios(15.0), watchos(8.0), tvos(15.0));

    但新的問題又來了,系統僅給 CAAnimation 和 CADisplayLink 的 api 提供了動態修改幀率的操作。

    但是在我們直播場景中,一共有如下幾種場景的動畫提交:

    • 1)UIView block 動畫;
    • 2)UIScrollView scroll 動畫;
    • 3)NSTimer 動畫;
    • 4)CAAnimation。

    除了4我們可以直接修改為 iOS15支持的 preferredFrameRateRange api 外,其它幾個我們要怎么解決呢?

    針對以上1~3點我們分別做如下處理。

    6.2UIView block 動畫

    通過分析 +[UIView animateWithDuration:delay : options:animations:completion:] 調用,我們發現 animations block 里的 property animation 會被同步的創建為 CAAnimation 對象。

    如圖所示:

    那我們是否可以 hook CAAnimation 然后尋找時機設置它的 preferredFrameRateRange 以達到降幀的目的?

    很遺憾,不行,因為這個 api 觸發的動畫不會去觸發對應的 setter 與 getter 去讀取新修改的值,而是被覆蓋為一個默認值,導致無法降幀。

    再進一步調試發現與UIViewPropertyAnimator 里是有主動 setPreferredFrameRateRange:的操作,那是否可以從這里入手?

    經過驗證,果然可行,于是我們可以將所有的 UIView block animation 動畫都無縫替換為新方案后,即可實現自動降幀隨意靈活控制的目的了。

    部分代碼如下:

        if(@available(iOS 15.0, *)) {

            setFrameRateLevel(level);

            [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:duration

                                                                  delay:delay

                                                                options:options

                                                             animations:animations

    completion:^(UIViewAnimatingPosition finalPosition) {

                                                                 if(completion) {

                                                                     completion(YES);

                                                                 }

                                                             }];

            clearFrameRateLevel();

        } else{

            if(level != MMAnimationFrameRateLevelNone) {

                if(level <= MMAnimationFrameRateLevelMedium)

                    options |= UIViewAnimationOptionPreferredFramesPerSecond30;

            }

            [selfanimateWithDuration:duration delay:delay options:options animations:animations completion:completion];

        }

    新的接口可以無縫替換原有的+[UIView animateWithDuration:delay : options:animations:completion:] 調用,可對所有系統實現降幀調節優化,極大的方便了業務開發同學在不同場景中選擇合適的動畫幀率,以達到效果和耗電的平衡。

    6.3UIScrollView 動畫

    經過上文的分析我們發現 UIScrollView setContentOffset 的動畫是基于系統_UIUpdateTarget 機制來驅動的,由于對應的回調是私有 api 觸發的,所以我們無法直接調節它的幀率,于是我們干脆自己實現一個基于 CADisplayLink 驅動的 setContentOffset 滑動動畫即可解決問題。

    即:創建一個CADisplayLink對象,指定我們需要的 preferredFrameRateRange 幀率,然后在每一幀回調時,根據當前的時間戳計算出當前需要設置的 contentOffset 值,直到最終達到了指定的動畫 duration 時間后,我們再把 contentOffset 調整為目標值,即可。

    主體代碼大致如下:

    - (void)tt_contentOffset:(CGPoint)contentOffset duration:(CFTimeInterval)duration {

        self.duration = duration;

        self.deltaContentOffset = CGPointMinus(contentOffset, self.scrollView.contentOffset);

        if(!self.displayLink) {

            self.displayLink = [CADisplayLink displayLinkWithTarget:selfselector:@selector(updateContentOffset:)];

            if(@available(iOS 15.0, *)) {

                self.displayLink.preferredFrameRateRange = CAFrameRateRangeMake(15, 24, 0);

            } else{

                self.displayLink.preferredFramesPerSecond = 30;

            }

            [self.displayLink addToRunLoop:[NSRunLoopcurrentRunLoop] forMode:NSDefaultRunLoopMode];

        } else{

            self.displayLink.paused = NO;

        }

    }

    - (void)tt_onDisplayLink:(CADisplayLink *)displayLink {

        if(self.beginTime == 0.0) {

            self.beginTime = self.displayLink.timestamp;

            self.beginContentOffset = self.scrollView.contentOffset;

        } else{

            CFTimeInterval duration = displayLink.timestamp - self.beginTime;

            CGFloat percent = (CGFloat)(duration / self.duration);

            if(percent < 1.0) {

                CGFloat progress = (CGFloat)timingFunctionValue(self.timingFunction, percent);

                if(1 - progress < 0.001) {

                    [selftt_stopAnimation];

                } else{

                    [selftt_updateProgress:progress];

                }

            } else{

                [selftt_stopAnimation];

            }

        }

    }

    6.4NSTimer 動畫

    根據蘋果Explore UI animation hitches and the render loop 的說法,為了避免 vsync 信號和 timer  可能不同步的情形,我們直接用 CADisplayLink 來替換原先的 NSTimer,并在 CADisplayLink 回調里再觸發對應的 UI 提交操作和動畫即可。

    這是因為:如下圖所示,對于13 pro max 高刷屏設備而言,其 UI 動畫的系統回調頻次是240hz,渲染幀率是可變的可為0~120fps 之間,而常規的 NSTimer 是基于 RunLoop 觸發回調的,RunLoop 的回調間隔可能只有幾十 us,那么 Timer 的靈敏度遠高于 DisplayLink,所以完全是有可能在2幀渲染之間,回調了一次 Timer,而最終導致可能會多觸發了一幀的提交或一次渲染事件。所以我們采取 CADisplayLink 來替換 NSTimer,盡可能避免和 Display 不同步的渲染觸發操作。

    7、優化后的效果

    按照蘋果的建議 ,app 內容在沒有頻繁更新時,應該盡量降低 FPS 以平衡功耗占用,因為高刷必然帶來更頻繁的 GPU 任務提交,使得 GPU 占用提升。

    并且 app 的刷新率是由所有內容的最高刷新率決定的,也就是高 FPS 的界面元素會導致整個屏幕全局 FPS 提升,只有適當平衡全局界面元素的 FPS 后,才能進一步降低不必要的性能消耗。

    基于上述指導思想和優化方案,我們最終在視頻號直播上驗證測試如下:

    先基于 「UIViewAnimationOptionPreferredFramesPerSecond30」 將直播點贊場景下的fps從高刷屏的120fps 降低到60fps,再基于 「UIViewPropertyAnimator」 將任意UIView block animation的幀率降低到30~48fps(最終全局穩定在40~50fps),幀率同比下降16%而 GPU 同比下降了26%~38%(在主場景和其他場景)。并且由于我們的視頻畫面依舊是25fps的低幀率,所以此處降幀只是降低了 QuartzCore 的重復幀,而沒有減少任何畫面細節,最終本質上是無損的畫面降幀。

    另外,「在實驗過程中調試」,進一步發現了一些很有用的環境變量,可以幫助我們更好的調試UI問題。

    如下:

    8、問題擴展

    我們通過一些奇怪的繞過方式間接的實現了對所有基于 UIView block animation api 調用的動畫以及 CAAnimation api 調用的動畫都實現了動態降幀,這極大的改善了低幀率直播間的業務動畫導致的 GPU 功耗占用問題。

    那對于高幀率直播間我們還能怎么解決呢?

    基于蘋果的文檔幀率檔位設置建議和我們的綜合實踐效果,我們對高幀率直播間采取了部分用戶無明顯感知的有損降級策略。即當檢測到設備過熱后,我們會將60fps 的直播流,以渲染端均勻丟幀的方式降幀到48fps。

    方案如下:

    最終也一樣取得了GPU 同比下降28%甚至更高的效果,有效減輕了過熱時的系統負載和功耗,并且從肉眼上基本無法分辨出差異。

    9、本文小結

    本文在不影響現有用戶體驗和業務邏輯的情況下,通過擴展系統接口的能力與實驗調試分析,最終實現了一套 UI 動畫的幀率調節方案。

    該方案得到的效果是:

    • 1)快速改造既有業務的所有動畫,動態的控制各自的幀率;
    • 2)最終達到不影響效果的前提下,盡可能的降低了功耗;
    • 3)同時極大的減輕了業務開發同學適配多系統和改造動畫的工作量。

    該方案最終在微信視頻號直播上得到廣泛應用,取得了較大的性能提升。

    至此,關于iOS視頻號直播的功耗優化方案講解完畢。

    10、相關文章

    [1] 淘寶直播技術干貨:高清、低延時的實時視頻直播技術解密

    [2] 技術干貨:實時視頻直播首屏耗時400ms內的優化實踐

    [3] 七牛云技術分享:使用QUIC協議實現實時視頻直播0卡頓!

    [4] 首次披露:快手是如何做到百萬觀眾同場看直播仍能秒開且不卡頓的?

    [5] 淺談實時音視頻直播中直接影響用戶體驗的幾項關鍵技術指標

    [6] 移動端實時視頻直播技術實踐:如何做到實時秒開、流暢不卡

    [7] 實現延遲低于500毫秒的1080P實時音視頻直播的實踐分享

    [8] 直播系統聊天技術(五):微信小游戲直播在Android端的跨進程渲染推流實踐

    附錄:微信團隊分享的其它文章

    微信團隊分享:極致優化,iOS版微信編譯速度3倍提升的實踐總結

    IM“掃一掃”功能很好做?看看微信“掃一掃識物”的完整技術實現

    微信團隊分享:微信支付代碼重構帶來的移動端軟件架構上的思考

    IM開發寶典:史上最全,微信各種功能參數和邏輯規則資料匯總

    微信團隊分享:微信直播聊天室單房間1500萬在線的消息架構演進之路

    企業微信的IM架構設計揭秘:消息模型、萬人群、已讀回執、消息撤回等

    IM全文檢索技術專題(四):微信iOS端的最新全文檢索技術優化實踐

    微信團隊分享:微信后臺在海量并發請求下是如何做到不崩潰的

    微信Windows端IM消息數據庫的優化實踐:查詢慢、體積大、文件損壞等

    微信技術分享:揭秘微信后臺安全特征數據倉庫的架構設計

    企業微信針對百萬級組織架構的客戶端性能優化實踐

    揭秘企業微信是如何支持超大規模IM組織架構的——技術解讀四維關系鏈

    微信團隊分享:詳解iOS版微信視頻號直播中因幀率異常導致的功耗問題


    (本文已同步發布于:http://www.52im.net/thread-4507-1-1.html



    作者:Jack Jiang (點擊作者姓名進入Github)
    出處:http://www.52im.net/space-uid-1.html
    交流:歡迎加入即時通訊開發交流群 215891622
    討論:http://www.52im.net/
    Jack Jiang同時是【原創Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
    本博文 歡迎轉載,轉載請注明出處(也可前往 我的52im.net 找到我)。


    只有注冊用戶登錄后才能發表評論。


    網站導航:
     
    Jack Jiang的 Mail: jb2011@163.com, 聯系QQ: 413980957, 微信: hellojackjiang
    久久一级片
    <acronym id="cr5pu"></acronym>
  • <kbd id="cr5pu"><font id="cr5pu"></font></kbd>
  • <li id="cr5pu"><output id="cr5pu"></output></li>
    <del id="cr5pu"><li id="cr5pu"></li></del><center id="cr5pu"></center>
    <output id="cr5pu"><kbd id="cr5pu"></kbd></output>
  • <rp id="cr5pu"></rp>
    <var id="cr5pu"></var>
  • <nav id="cr5pu"></nav>