quququq

iOS Audio hand by hand: 變聲,混響,語音合成 TTS丐重,Swift5,基于 AVAudioEngine 等

AVAudioEngine 比 AVAudioPlayer 更加強大杆查,當然使用上比起 AVAudioPlayer 繁瑣扮惦。
AVAudioEngine 對于 Core Audio 作了一些使用上的封裝簡化,簡便的做了一些音頻信號的處理亲桦。
使用 AVAudioPlayer 崖蜜,是音頻文件級別的處理浊仆。
使用 AVAudioEngine,是音頻數(shù)據(jù)流級別的處理豫领。
AVAudioEngine 可以做到低時延的抡柿、實時音頻處理。還可以做到音頻的多輸入氏堤,添加特殊的效果沙绝,例如三維空間音效

AVAudioEngine 可以做出強大的音樂處理與混音 app, 配合制作復雜的三維空間音效的游戲搏明,本文來一個簡單的變聲應用

通用架構圖鼠锈,場景是 K 歌

aaa

AVAudioEngine 使用指南

首先,簡單理解下

111

來一個 AVAudioEngine 實例星著,然后添加節(jié)點 Node, 有播放器的 Player Node, 音效的 Effect Node.
將節(jié)點連在音頻引擎上购笆,即 AVAudioEngine 實例。然后建立節(jié)點間的關聯(lián)虚循,組成一條音頻的數(shù)據(jù)處理鏈同欠。
處理后的音頻數(shù)據(jù),流過最后的一個節(jié)點横缔,就是音頻引擎的輸出了铺遂。

開始做一個變聲的功能,也就是音調變化

需要用到 AVAudioEngine 和 AVAudioPlayerNode

    // 音頻引擎是樞紐
    var audioAVEngine = AVAudioEngine()
    // 播放節(jié)點
    var enginePlayer = AVAudioPlayerNode()
    // 變聲單元:調節(jié)音高
    let pitchEffect = AVAudioUnitTimePitch()
    // 混響單元
    let reverbEffect = AVAudioUnitReverb()
    // 調節(jié)音頻播放速度單元
    let rateEffect = AVAudioUnitVarispeed()
    // 調節(jié)音量單元
    let volumeEffect = AVAudioUnitEQ()
    // 音頻輸入文件
    var engineAudioFile: AVAudioFile!

做一些設置
先取得輸入節(jié)點的 AVAudioFormat 引用茎刚,
這是音頻流數(shù)據(jù)的默認描述文件襟锐,包含通道數(shù)、采樣率等信息膛锭。
實際上粮坞,AVAudioFormat 就是對 Core Audio 的音頻緩沖數(shù)據(jù)格式文件 AudioStreamBasicDescription, 做了一些封裝初狰。
audioAVEngine 做子節(jié)點關聯(lián)的時候莫杈,要用到。

// 做一些配置奢入,功能初始化
    func setupAudioEngine() {
        // 這個例子筝闹,是單音
        let format = audioAVEngine.inputNode.inputFormat(forBus: 0)
        // 添加功能
        audioAVEngine.attach(enginePlayer)
        
        audioAVEngine.attach(pitchEffect)
        audioAVEngine.attach(reverbEffect)
        audioAVEngine.attach(rateEffect)
        audioAVEngine.attach(volumeEffect)
        // 連接功能
        audioAVEngine.connect(enginePlayer, to: pitchEffect, format: format)
        audioAVEngine.connect(pitchEffect, to: reverbEffect, format: format)
        audioAVEngine.connect(reverbEffect, to: rateEffect, format: format)
        audioAVEngine.connect(rateEffect, to: volumeEffect, format: format)
        audioAVEngine.connect(volumeEffect, to: audioAVEngine.mainMixerNode, format: format)
        
        // 選擇混響效果為大房間
        reverbEffect.loadFactoryPreset(AVAudioUnitReverbPreset.largeChamber)
        
        do {
            // 可以先開啟引擎
            try audioAVEngine.start()
        } catch {
            print("Error starting AVAudioEngine.")
        }
    }

播放

func  play(){
        let fileURL = getURLforMemo()
        var playFlag = true
        
        do {
           //   先拿 URL 初始化 AVAudioFile
           //   AVAudioFile 加載音頻數(shù)據(jù),形成數(shù)據(jù)緩沖區(qū)腥光,方便 AVAudioEngine 使用
            engineAudioFile = try AVAudioFile(forReading: fileURL)
             //  變聲效果丁存,先給一個音高的默認值
            //  看效果,來點尖利的
            pitchEffect.pitch = 2400
            reverbEffect.wetDryMix = UserSetting.shared.reverb
            rateEffect.rate = UserSetting.shared.rate
            volumeEffect.globalGain = UserSetting.shared.volume
        } catch {
            engineAudioFile = nil
            playFlag = false
            print("Error loading AVAudioFile.")
        }
        
         // AVAudioPlayer 主要是音量大小的檢測柴我,這里做了一些取巧
        //  就是為了制作上篇播客介紹的解寝,企鵝張嘴的動畫效果
        do {
            audioPlayer = try AVAudioPlayer(contentsOf: fileURL)
            audioPlayer.delegate = self
            if audioPlayer.duration > 0.0 {
                // 不靠他播放,要靜音
                //  audioPlayer 不是用于播放音頻的艘儒,所以他的音量設置為 0
                audioPlayer.volume = 0.0
                audioPlayer.isMeteringEnabled = true
                audioPlayer.prepareToPlay()
            } else {
                playFlag = false
            }
        } catch {
            audioPlayer = nil
            engineAudioFile = nil
            playFlag = false
            print("Error loading audioPlayer.")
        }
        // 兩個播放器聋伦,要一起播放夫偶,前面做了一個 audioPlayer 可用的標記 
        if playFlag == true {
            //  enginePlayer,有聲音
             //  真正用于播放的 enginePlayer
            enginePlayer.scheduleFile(engineAudioFile, at: nil, completionHandler: nil)
            enginePlayer.play()
            // audioPlayer觉增,沒聲音兵拢,用于檢測
            audioPlayer.play()
            setPlayButtonOn(flag: true)
            startUpdateLoop()
            audioStatus = .playing
        }
    }


上面的小技巧: AVAudioPlayerNode + AVAudioPlayer

同時播放 AVAudioPlayerNode (有聲音), AVAudioPlayer (啞巴的逾礁,就為了取下數(shù)據(jù)與狀態(tài))说铃, 通過 AVAudioPlayerNode 添加變聲等音效,通過做音量大小檢測嘹履。

看起來有些累贅腻扇,蘋果自然是不會推薦這樣做的。

111

如果是錄音砾嫉,通過 NodeTapBlock 對音頻輸入流的信息幼苛,做實時分析。
播放也類似焕刮,處理音頻信號舶沿,取出平均音量,就可以刷新 UI 了配并。

通過 AVAudioPlayer 括荡,可以方便拿到當前播放時間,文件播放時長等信息溉旋,

通過 AVAudioPlayerDelegate畸冲,可以方便播放結束了,去刷新 UI

當然低滩,使用 AVAudioPlayerNode 召夹,這些都是可以做到的


結束播放

func stopPlayback() {
        setPlayButtonOn(flag: false)
        audioStatus = .stopped
        // 兩個播放器,一起結束恕沫,一起結束
        audioPlayer.stop()
        enginePlayer.stop()
        stopUpdateLoop()
    } 

音效: 音高监憎,混響纤虽,播放速度柠辞,音量大小

調節(jié)音高,用來變聲艺智, AVAudioUnitTimePitch

音效的 pitch 屬性迄委,取值范圍從 -2400 音分到 2400 音分褐筛,包含 4 個八度音階。
默認值為 0
一個八度音程可以分為12個半音叙身。
每一個半音的音程相當于相鄰鋼琴鍵間的音程渔扎,等于100音分

    func setPitch(value: Float) {
        pitchEffect.pitch = value
    }
調節(jié)混響, AVAudioUnitReverb

wetDryMix 的取值范圍是 0 ~ 100信轿,
0 是全干晃痴,干聲即無音樂的純人聲
100 是全濕潤残吩,空間感很強。
干聲是原版倘核,濕聲是經過后期處理的泣侮。

   func toSetReverb(value: Float) {
        reverbEffect.wetDryMix = value
    }
調節(jié)音頻播放速度, AVAudioUnitVarispeed

音頻播放速度 rate 的取值范圍是 0.25 ~ 4.0紧唱,
默認是 1.0活尊,正常播放。

func toSetRate(value: Float) {
        rateEffect.rate = value
    }
調節(jié)音量大小漏益, AVAudioUnitEQ

globalGain 的取值范圍是 -96 ~ 24蛹锰, 單位是分貝

func toSetVolumn(value: Float){
        volumeEffect.globalGain = value
    }

語音合成 TTS,輸入文字遭庶,播放對應的語音

TTS宁仔,一般會用到 AVSpeechSynthesizer 和他的代理 AVSpeechSynthesizerDelegate
AVSpeechSynthesizer 是 AVFoundation 框架下的一個類稠屠,它的功能就是輸入文字峦睡,讓你的應用,選擇 iOS 平臺支持的語言和方言权埠,然后合成語音榨了,播放出來。

iOS 平臺攘蔽,支持三種中文龙屉,就是三種口音,有中文簡體 zh-CN满俗,Ting-Ting 朗讀转捕;有 zh-HK,Sin-Ji 朗讀唆垃;有 zh-TW五芝,Mei-Jia 朗讀。
可參考 How to get a list of ALL voices on iOS

AVSpeechSynthesizer 合成器相關知識

AVSpeechSynthesizer 需要拿材料 AVSpeechUtterance 去朗讀辕万。

語音文本單元 AVSpeechUtterance 封裝了文字枢步,還有對應的朗讀效果參數(shù)。

朗讀效果中渐尿,可以設置口音醉途,本文 Demo 采用 zh-CN。還可以設置變聲和語速 (發(fā)音速度)砖茸。

拿到 AVSpeechUtterance 隘擎,合成器 AVSpeechSynthesizer 就可以朗讀了。如果 AVSpeechSynthesizer 正在朗讀凉夯,AVSpeechUtterance 就會放在 AVSpeechSynthesizer 的朗讀隊列里面货葬,按照先進先出的順序等待朗讀推正。

蘋果框架的粒度都很細,語音合成器 AVSpeechSynthesizer宝惰,也有暫定植榕、繼續(xù)播放與結束播放功能。

停止了語音合成器 AVSpeechSynthesizer尼夺,如果他的朗讀隊列里面還有語音文本AVSpeechUtterance尊残,剩下的都會直接移除。

AVSpeechSynthesizerDelegate 合成器代理相關

使用合成器代理淤堵,可以監(jiān)聽朗讀時候的事件寝衫。例如:開始朗讀,朗讀結束

TTS: Text To Speech 三步走

先設置
// 來一個合成器
let synthesizer = AVSpeechSynthesizer()

// ...

// 設置合成器的代理拐邪,監(jiān)聽事件
synthesizer.delegate = self


朗讀慰毅、暫停、繼續(xù)朗讀與停止朗讀
// 朗讀
func  play() {
    let words = UserSetting.shared.message
    // 拿文本扎阶,去實例化語音文本單元
    let utterance = AVSpeechUtterance(string: words)
    // 設置發(fā)音為簡體中文 ( 中國大陸 )
    utterance.voice = AVSpeechSynthesisVoice(language: "zh-CN")
    // 設置朗讀的語速
    utterance.rate = AVSpeechUtteranceMaximumSpeechRate * UserSetting.shared.rate
    // 設置音高
    utterance.pitchMultiplier = UserSetting.shared.pitch
    synthesizer.speak(utterance)
  }

// 暫停朗讀汹胃,沒有設置立即暫停东臀,是按字暫停
func pausePlayback() {
        synthesizer.pauseSpeaking(at: AVSpeechBoundary.word)
    }

// 繼續(xù)朗讀
 func continuePlayback() {
        synthesizer.continueSpeaking()
    }

// 停止播放
func stopPlayback() {
    // 讓合成器馬上停止朗讀
    synthesizer.stopSpeaking(at: AVSpeechBoundary.immediate)
    // 停止計時器更新狀態(tài)着饥,具體見文尾的 github repo
    stopUpdateLoop()
    setPlayButtonOn(false)
    audioStatus = .stopped
  }
設置合成器代理,監(jiān)聽狀態(tài)改變的時機
// 開始朗讀惰赋。朗讀每一個語音文本單元的時候宰掉,都會來一下
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
    setPlayButtonOn(true)
    startUpdateLoop()
    audioStatus = .playing
  }
  
// 結束朗讀。每一個語音文本單元結束朗讀的時候赁濒,都會來一下
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
    stopUpdateLoop()
    setPlayButtonOn(false)
    audioStatus = .stopped
  }
  
// 語音文本單元里面轨奄,每一個字要朗讀的時候,都會來一下
// 讀書應用拒炎,朗讀前挪拟,可以用這個高光正在讀的詞語
  func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) {
    let speakingString = utterance.speechString as NSString
    let word = speakingString.substring(with: characterRange)
    print(word)
  }
  
    // 暫定朗讀
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) {
        stopUpdateLoop()
        setPlayButtonOn(false)
        audioStatus = .paused
    }
    
    // 繼續(xù)朗讀
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) {
        setPlayButtonOn(true)
        startUpdateLoop()
        audioStatus = .playing
    }

11 個例子,由淺到深枝冀,學習 iOS 動畫

iOS 的動畫框架很成熟舞丛,提供必要的信息,譬如動畫的起始位置與終止位置果漾,動畫效果就出來了

動畫的實現(xiàn)方式挺多的球切,
有系統(tǒng)提供的簡單 API ,直接提供動畫般的交互效果绒障。
有手動設置交互效果吨凑,看起來像是動畫,一般要用到插值。
至于動畫框架鸵钝,有 UIView 級別的糙臼,有功能強勁的 CALayer 級別的動畫。
CALayer 級別的動畫通過靈活設置的 CoreAnimation恩商,CoreAnimation 的常規(guī)操作变逃,就是自定義路徑
當然有蘋果推了幾年的 UIViewPropertyAnimator, 動畫可交互性做得比較好


例子一怠堪,導航欄動畫

NavigationBarAnimation.gif
navigationController?.hidesBarsOnSwipe = true

簡單設置 hidesBarsOnSwipe 屬性揽乱,就可以了。
該屬性粟矿,除了可以調節(jié)頭部導航欄凰棉,還可以調節(jié)底部標簽工具欄 toolbar


例子二,屏幕開鎖效果

image

一眼看起來有點炫陌粹,實際設置很簡單

    func openLock() {
        UIView.animate(withDuration: 0.4, delay: 1.0, options: [], animations: {
            
            // Rotate keyhole.
            self.lockKeyhole.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
            
            }, completion: { _ in
                
                UIView.animate(withDuration: 0.5, delay: 0.2, options: [], animations: {
                    
                    // Open lock.
                    let yDelta = self.lockBorder.frame.maxY
                    
                    self.topLock.center.y -= yDelta
                    self.lockKeyhole.center.y -= yDelta
                    self.lockBorder.center.y -= yDelta
                    self.bottomLock.center.y += yDelta
                    
                    
                    }, completion: { _ in
                        self.topLock.removeFromSuperview()
                        self.lockKeyhole.removeFromSuperview()
                        self.lockBorder.removeFromSuperview()
                        self.bottomLock.removeFromSuperview()
                })
        })
    }

總共有四個控件撒犀,先讓中間的鎖控件旋轉一下,然后對四個控件掏秩,做移位操作

用簡單的關鍵幀動畫或舞,處理要優(yōu)雅一點


例子三,地圖定位波動

MapLocationAnimation.gif

看上去有些眼花的動畫哗讥,可以分解為三個動畫

111

一波未平嚷那,一波又起胞枕,做一個動畫效果的疊加杆煞,就成了動畫的第一幅動畫

一個動畫波動效果,效果用到了透明度的變化腐泻,范圍的變化
范圍的變化决乎,用的就是 CoreAnimation 的路徑 path

CoreAnimation 簡單設置,就是指明 from 派桩、to构诚,動畫的起始狀態(tài),和動畫終止狀態(tài)铆惑,然后選擇使用哪一種動畫效果范嘱。
動畫的起始狀態(tài),一般是起始位置员魏。簡單的動畫丑蛤,就是讓他動起來

func sonar(_ beginTime: CFTimeInterval) {
        let circlePath1 = UIBezierPath(arcCenter: self.center, radius: CGFloat(3), startAngle: CGFloat(0), endAngle:CGFloat.pi * 2, clockwise: true)

        let circlePath2 = UIBezierPath(arcCenter: self.center, radius: CGFloat(80), startAngle: CGFloat(0), endAngle:CGFloat.pi * 2, clockwise: true)
        
        let shapeLayer = CAShapeLayer()
        shapeLayer.strokeColor = ColorPalette.green.cgColor
        shapeLayer.fillColor = ColorPalette.green.cgColor
        shapeLayer.path = circlePath1.cgPath
        self.layer.addSublayer(shapeLayer)
        
        
        // 兩個動畫
        
        let pathAnimation = CABasicAnimation(keyPath: "path")
        pathAnimation.fromValue = circlePath1.cgPath
        pathAnimation.toValue = circlePath2.cgPath
        
        let alphaAnimation = CABasicAnimation(keyPath: "opacity")
        alphaAnimation.fromValue = 0.8
        alphaAnimation.toValue = 0
        
        // 組動畫
        let animationGroup = CAAnimationGroup()
        animationGroup.beginTime = beginTime
        animationGroup.animations = [pathAnimation, alphaAnimation]
        
        // 時間有講究
        animationGroup.duration = 2.76
        
        // 不斷重復
        animationGroup.repeatCount = Float.greatestFiniteMagnitude
        animationGroup.isRemovedOnCompletion = false
        animationGroup.fillMode = CAMediaTimingFillMode.forwards
        
        // Add the animation to the layer.
        // key 用來 debug
        shapeLayer.add(animationGroup, forKey: "sonar")
    }


波動效果調用了三次

    
    func startAnimation() {
        // 三次動畫,效果合成撕阎,
        sonar(CACurrentMediaTime())
        sonar(CACurrentMediaTime() + 0.92)
        sonar(CACurrentMediaTime() + 1.84)
    }


例子四受裹,加載動畫

LoadingDotsAnimation.gif

這是 UIView 框架自帶的動畫,看起來不錯,就是做了一個簡單的縮放棉饶,通過 transform 屬性做仿射變換

func startAnimation() {
    
        dotOne.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
        dotTwo.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
        dotThree.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
        
        
        // 三個不同的 delay, 漸進時間
        UIView.animate(withDuration: 0.6, delay: 0.0, options: [.repeat, .autoreverse], animations: {
            self.dotOne.transform = CGAffineTransform.identity
            }, completion: nil)
        
        UIView.animate(withDuration: 0.6, delay: 0.2, options: [.repeat, .autoreverse], animations: {
            self.dotTwo.transform = CGAffineTransform.identity
            }, completion: nil)
        
        UIView.animate(withDuration: 0.6, delay: 0.4, options: [.repeat, .autoreverse], animations: {
            self.dotThree.transform = CGAffineTransform.identity
            }, completion: nil)
    }


例子五厦章,下劃線點擊轉移動畫

這個也是 UIView 的動畫

UnderlineAnimation.gif

動畫的實現(xiàn)效果,是通過更改約束照藻。
約束動畫要注意的是袜啃,確保動畫的起始位置準確,起始的時候幸缕,一般要調用其父視圖的 layoutIfNeeded 方法囊骤,確保視圖的實際位置與約束設置的一致。
這里的約束動畫冀值,是通過 NSLayoutAnchor 做得也物。
一般我們用的是 SnapKit 設置約束,調用也差不多列疗。

 func animateContraintsForUnderlineView(_ underlineView: UIView, toSide: Side) {
        
        switch toSide {
        case .left:
            
            for constraint in underlineView.superview!.constraints {
                if constraint.identifier == ConstraintIdentifiers.centerRightConstraintIdentifier {
                    
                    constraint.isActive = false
                    
                    let leftButton = optionsBar.arrangedSubviews[0]
                    let centerLeftConstraint = underlineView.centerXAnchor.constraint(equalTo: leftButton.centerXAnchor)
                    centerLeftConstraint.identifier = ConstraintIdentifiers.centerLeftConstraintIdentifier
                    
                    NSLayoutConstraint.activate([centerLeftConstraint])
                }
            }
            
        case .right:
            
            for constraint in underlineView.superview!.constraints {
                if constraint.identifier == ConstraintIdentifiers.centerLeftConstraintIdentifier {
                    // 先失效滑蚯,舊的約束
                    constraint.isActive = false
                    // 再新建約束,并激活
                    let rightButton = optionsBar.arrangedSubviews[1]
                    let centerRightConstraint = underlineView.centerXAnchor.constraint(equalTo: rightButton.centerXAnchor)
                    centerRightConstraint.identifier = ConstraintIdentifiers.centerRightConstraintIdentifier
                    
                    NSLayoutConstraint.activate([centerRightConstraint])
                    
                }
            }
        }
        
        UIView.animate(withDuration: 0.6, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, options: [], animations: {
            self.view.layoutIfNeeded()
            }, completion: nil)
        
    }


例子六抵栈,列表視圖的頭部拉伸效果

這個沒有用到動畫框架告材,就是做了一個交互插值

就是補插連續(xù)的函數(shù) scrollViewDidScroll, 及時更新列表視圖頭部的位置、尺寸

override func scrollViewDidScroll(_ scrollView: UIScrollView) {
        updateHeaderView()
    }
    
    
    func updateHeaderView() {
        var headerRect = CGRect(x: 0, y: -tableHeaderHeight, width: tableView.bounds.width, height: tableHeaderHeight)
        // 決定拉動的方向
        if tableView.contentOffset.y < -tableHeaderHeight {
            // 就是改 frame
            headerRect.origin.y = tableView.contentOffset.y
            headerRect.size.height = -tableView.contentOffset.y
        }
        
        headerView.frame = headerRect
    }


例子七古劲,進度繪制動畫

ProgressAnimation.gif

用到了 CoreAnimation斥赋,也用到了插值。
每一段插值都是一個 CoreAnimation 動畫产艾,進度的完成分為多次插值疤剑。
這里動畫效果的主要用到 strokeEnd 屬性, 筆畫結束

插值的時候,要注意闷堡,下一段動畫的開始隘膘,正是上一段動畫的結束

    // 這個用來,主要的效果
    let progressLayer = CAShapeLayer()
   // 這個用來杠览,附加的顏色
    let gradientLayer = CAGradientLayer()
    
    // 給個默認值弯菊,外部設置
    var range: CGFloat = 128

    var curValue: CGFloat = 0 {
        didSet {
            animateStroke()
        }
    }
    

    func setupLayers() {
        
        progressLayer.position = CGPoint.zero
        progressLayer.lineWidth = 3.0
        progressLayer.strokeEnd = 0.0
        progressLayer.fillColor = nil
        progressLayer.strokeColor = UIColor.black.cgColor

        let radius = CGFloat(bounds.height/2) - progressLayer.lineWidth
        let startAngle = CGFloat.pi * (-0.5)
        let endAngle = CGFloat.pi * 1.5
        
        let width = bounds.width
        let height = bounds.height
        let modelCenter = CGPoint(x: width / 2, y: height / 2)
        let path = UIBezierPath(arcCenter: modelCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        //  指定路徑
        progressLayer.path = path.cgPath

        layer.addSublayer(progressLayer)
        // 有一個漸變
        gradientLayer.frame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
        
        //  teal, 藍綠色
        
        gradientLayer.colors = [ColorPalette.teal.cgColor, ColorPalette.orange.cgColor, ColorPalette.pink.cgColor]
        gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
        gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
        
        gradientLayer.mask = progressLayer // Use progress layer as mask for gradient layer.
        layer.addSublayer(gradientLayer)
    }
    
    func animateStroke() {
        // 前一段的終點
        let fromValue = progressLayer.strokeEnd
        let toValue = curValue / range
        
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = fromValue
        animation.toValue = toValue
        progressLayer.add(animation, forKey: "stroke")
        progressLayer.strokeEnd = toValue
    }

}

// 動畫路徑,結合插值


例子八踱阿,漸變動畫

GradientAnimation.gif

這個漸變動畫管钳,主要用到了漸變圖層 CAGradientLayerlocations 位置屬性,用來調整漸變區(qū)域的分布

另一個關鍵點是用了圖層 CALayer 的遮罩 mask, 簡單理解软舌,把漸變圖層全部蒙起來才漆,只露出文本的形狀,就是那幾個字母的痕跡

class LoadingLabel: UIView {

    let gradientLayer: CAGradientLayer = {
        let gradientLayer = CAGradientLayer()
        
        gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
        gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
        
        // 灰葫隙, 白栽烂, 灰
        let colors = [UIColor.gray.cgColor, UIColor.white.cgColor, UIColor.gray.cgColor]
        gradientLayer.colors = colors
        
        let locations = [0.25, 0.5, 0.75]
        gradientLayer.locations = locations as [NSNumber]?
        
        return gradientLayer
    }()
    
    // 文字轉圖片,然后繪制到視圖上
    // 通過設置漸變圖層的遮罩 `mask` , 為指定文字,來設置漸變閃爍的效果

    @IBInspectable var text: String! {
          didSet {
               setNeedsDisplay()
            
                UIGraphicsBeginImageContextWithOptions(frame.size, false, 0)
                text.draw(in: bounds, withAttributes: textAttributes)
                let image = UIGraphicsGetImageFromCurrentImageContext()
                UIGraphicsEndImageContext()
               // 從文字中腺办,抽取圖片
            
                 let maskLayer = CALayer()
                 maskLayer.backgroundColor = UIColor.clear.cgColor
                 maskLayer.frame = bounds.offsetBy(dx: bounds.size.width, dy: 0)
                 maskLayer.contents = image?.cgImage
            
                 gradientLayer.mask = maskLayer
            }
      }

    // 設置位置與尺寸
    override func layoutSubviews() {
        gradientLayer.frame = CGRect(x: -bounds.size.width, y: bounds.origin.y, width: 2 * bounds.size.width, height: bounds.size.height)
    }
    
    override func didMoveToWindow() {
        super.didMoveToWindow()
        
        layer.addSublayer(gradientLayer)
        
        let gradientAnimation = CABasicAnimation(keyPath: "locations")
        gradientAnimation.fromValue = [0.0, 0.0, 0.25]
        gradientAnimation.toValue = [0.75, 1.0, 1.0]
        gradientAnimation.duration = 1.7
        
        // 一直循環(huán)
        gradientAnimation.repeatCount = Float.infinity
        gradientAnimation.isRemovedOnCompletion = false
        gradientAnimation.fillMode = CAMediaTimingFillMode.forwards
        
        gradientLayer.add(gradientAnimation, forKey: nil)
    }

}


例子九焰手,下拉刷新動畫

PullToRefreshAnimation.gif

首先通過方法 scrollViewDidScrollscrollViewWillEndDragging 做插值

extension PullRefreshView: UIScrollViewDelegate{
    
    // MARK: - UIScrollViewDelegate
       
       func scrollViewDidScroll(_ scrollView: UIScrollView) {
           let offsetY = CGFloat(max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0))
           self.progress = min(max(offsetY / frame.size.height, 0.0), 1.0)
           
           // 做互斥的狀態(tài)管理
           if !isRefreshing {
               redrawFromProgress(self.progress)
           }
       }
       
       func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
           if !isRefreshing && self.progress >= 1.0 {
               delegate?.PullRefreshViewDidRefresh(self)
               beginRefreshing()
           }
       }

}

畫面中飛碟動來動去,是通過 CAKeyframeAnimation(keyPath: "position") 怀喉,關鍵幀動畫的位置屬性书妻,設置的

   func redrawFromProgress(_ progress: CGFloat) {
        
        /* PART 1 ENTER ANIMATION */
        
        let enterPath = paths.start

       
        // 動畫指定路徑走
        let pathAnimation = CAKeyframeAnimation(keyPath: "position")
        pathAnimation.path = enterPath.cgPath
        pathAnimation.calculationMode = CAAnimationCalculationMode.paced
        pathAnimation.timingFunctions = [CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)]
        pathAnimation.beginTime = 1e-100
        
        pathAnimation.duration = 1.0
        pathAnimation.timeOffset = CFTimeInterval() + Double(progress)
        pathAnimation.isRemovedOnCompletion = false
        pathAnimation.fillMode = CAMediaTimingFillMode.forwards
        
        flyingSaucerLayer.add(pathAnimation, forKey: nil)
        flyingSaucerLayer.position = enterPath.currentPoint
        
        
        let sizeAlongEnterPathAnimation = CABasicAnimation(keyPath: "transform.scale")
        sizeAlongEnterPathAnimation.fromValue = 0
        sizeAlongEnterPathAnimation.toValue = progress
        sizeAlongEnterPathAnimation.beginTime = 1e-100
        
        sizeAlongEnterPathAnimation.duration = 1.0
        sizeAlongEnterPathAnimation.isRemovedOnCompletion = false
        sizeAlongEnterPathAnimation.fillMode = CAMediaTimingFillMode.forwards
        
        flyingSaucerLayer.add(sizeAlongEnterPathAnimation, forKey: nil)

    }

//  設置路徑
   func customPaths(frame: CGRect = CGRect(x: 4, y: 3, width: 166, height: 74)) -> ( UIBezierPath, UIBezierPath) {
       
        // 兩條路徑
        
        let startY = 0.09459 * frame.height
        let enterPath = UIBezierPath()
        // ...
        enterPath.addCurve(to: CGPoint(x: frame.minX + 0.21694 * frame.width, y: frame.minY + 0.85855 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.04828 * frame.width, y: frame.minY + 0.68225 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.21694 * frame.width, y: frame.minY + 0.85855 * frame.height))
        
        
        enterPath.addCurve(to: CGPoint(x: frame.minX + 0.36994 * frame.width, y: frame.minY + 0.92990 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.21694 * frame.width, y: frame.minY + 0.85855 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.33123 * frame.width, y: frame.minY + 0.93830 * frame.height))
        // ...
        enterPath.usesEvenOddFillRule = true
        
        let exitPath = UIBezierPath()
        exitPath.move(to: CGPoint(x: frame.minX + 0.98193 * frame.width, y: frame.minY + 0.15336 * frame.height))
        exitPath.addLine(to: CGPoint(x: frame.minX + 0.51372 * frame.width, y: frame.minY + 0.28558 * frame.height))
        // ... 
        exitPath.miterLimit = 4
        exitPath.usesEvenOddFillRule = true
        
        return (enterPath, exitPath)
    }

}

這個動畫比較復雜,需要做大量的數(shù)學計算躬拢,還要調試躲履,具體看文尾的 git repo.
一般這種動畫,我們用 Lottie


例子十聊闯,文本變換動畫

SecretTextAnimation.gif

這個動畫有些復雜工猜,重點使用了 CoreAnimation 的組動畫,疊加了五種效果菱蔬,縮放篷帅、尺寸、布局拴泌、位置與透明度魏身。

具體看文尾的 git repo.

    class func animation(_ layer: CALayer, duration: TimeInterval, delay: TimeInterval, animations: (() -> ())?, completion: ((_ finished: Bool)-> ())?) {
        
        
        let animation = CLMLayerAnimation()
        
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) {
            
            
            var animationGroup: CAAnimationGroup?
            let oldLayer = self.animatableLayerCopy(layer)
            animation.completionClosure = completion
            
            if let layerAnimations = animations {
                CATransaction.begin()
                CATransaction.setDisableActions(true)
                layerAnimations()
                CATransaction.commit()
            }
            
            animationGroup = groupAnimationsForDifferences(oldLayer, newLayer: layer)
            
            if let differenceAnimation = animationGroup {
                differenceAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
                differenceAnimation.duration = duration
                differenceAnimation.beginTime = CACurrentMediaTime()
                layer.add(differenceAnimation, forKey: nil)
            }
            else {
                if let completion = animation.completionClosure {
                    completion(true)
                }
            }
            
            
        }        
    }
    
    
    
    
    class func groupAnimationsForDifferences(_ oldLayer: CALayer, newLayer: CALayer) -> CAAnimationGroup? {
        var animationGroup: CAAnimationGroup?
        var animations = [CABasicAnimation]()
        
        // 疊加了五種效果
        
        if !CATransform3DEqualToTransform(oldLayer.transform, newLayer.transform) {
            let animation = CABasicAnimation(keyPath: "transform")
            animation.fromValue = NSValue(caTransform3D: oldLayer.transform)
            animation.toValue = NSValue(caTransform3D: newLayer.transform)
            animations.append(animation)
        }
        
        if !oldLayer.bounds.equalTo(newLayer.bounds) {
            let animation = CABasicAnimation(keyPath: "bounds")
            animation.fromValue = NSValue(cgRect: oldLayer.bounds)
            animation.toValue = NSValue(cgRect: newLayer.bounds)
            animations.append(animation)
        }
        
        if !oldLayer.frame.equalTo(newLayer.frame) {
            let animation = CABasicAnimation(keyPath: "frame")
            animation.fromValue = NSValue(cgRect: oldLayer.frame)
            animation.toValue = NSValue(cgRect: newLayer.frame)
            animations.append(animation)
        }
        
        if !oldLayer.position.equalTo(newLayer.position) {
            let animation = CABasicAnimation(keyPath: "position")
            animation.fromValue = NSValue(cgPoint: oldLayer.position)
            animation.toValue = NSValue(cgPoint: newLayer.position)
            animations.append(animation)
        }
        
        if oldLayer.opacity != newLayer.opacity {
            let animation = CABasicAnimation(keyPath: "opacity")
            animation.fromValue = oldLayer.opacity
            animation.toValue = newLayer.opacity
            animations.append(animation)
        }
        
        if animations.count > 0 {
            animationGroup = CAAnimationGroup()
            animationGroup!.animations = animations
        }
        
        return animationGroup
    }
    


例子十一,動態(tài)圖動畫

GifAnimation.gif

從 gif 文件里面取出每楨圖片蚪腐,算出持續(xù)時間箭昵,設置動畫圖片


    internal class func animatedImageWithSource(_ source: CGImageSource) -> UIImage? {
        
        // 需要喂圖片,
        // 喂動畫持續(xù)時間
        
        let count = CGImageSourceGetCount(source)
        
        var data: (images: [CGImage], delays: [Int]) = ([CGImage](), [Int]())

        // Fill arrays
        for i in 0..<count {
            // Add image
            if let image = CGImageSourceCreateImageAtIndex(source, i, nil) {
                data.images.append(image)
            }

            let delaySeconds = UIImage.delayForImageAtIndex(Int(i),
                source: source)
            data.delays.append(Int(delaySeconds * 1000.0)) // Seconds to ms
        }

        // Calculate full duration
        let duration: Int = {
            var sum = 0
            for val: Int in data.delays {
                sum += val
            }
            return sum
        }()
        
        let gcd = gcdForArray(data.delays)
        var frames = [UIImage]()

        var frame: UIImage
        var frameCount: Int
        for i in 0..<count {
            frame = UIImage(cgImage: data.images[Int(i)])
            frameCount = Int(data.delays[Int(i)] / gcd)

            for _ in 0..<frameCount {
                frames.append(frame)
            }
        }

        let animation = UIImage.animatedImage(with: frames,
            duration: Double(duration) / 1000.0)

        return animation
    }


github repo




音頻回季,參考了這個庫 syedhali/AudioStreamer

形象地理解 LRU, 拿起算法的鋼筆

LRU 還是挺有用的家制,緩存管理的時候,有時用到茧跋。

因為內存是有限的慰丛,要聚焦在重點的資源上,

學習 LRU, 首先要建立直觀的認識

LRU 的描述很簡潔瘾杭,容量有限哪亿,最近使用到的資源粥烁,排前面。

運用你所掌握的數(shù)據(jù)結構蝇棉,設計和實現(xiàn)一個 LRU (最近最少使用) 緩存機制讨阻。它應該支持以下操作: 獲取數(shù)據(jù) get 和 寫入數(shù)據(jù) put 。

獲取數(shù)據(jù) get(key) - 如果密鑰 (key) 存在于緩存中篡殷,則獲取密鑰的值(總是正數(shù))钝吮,否則返回 -1。
寫入數(shù)據(jù) put(key, value) - 如果密鑰不存在,則寫入其數(shù)據(jù)值奇瘦。當緩存容量達到上限時棘催,它應該在寫入新數(shù)據(jù)之前刪除最近最少使用的數(shù)據(jù)值,從而為新的數(shù)據(jù)值留出空間耳标。

模擬一下

下面有兩個綠色的格子醇坝,代表這個 LRU 的容量,是兩個

先放節(jié)點 1 次坡,容量 2呼猪, 當前是空的,直接放

0.png

再放節(jié)點 2 砸琅,容量 2宋距, 當前個數(shù) 1,可以直接放

1.png
2.png

然后讀取節(jié)點 1症脂,當前哈希表中有乡革,返回正常,該元素為最新使用元素

3.png
4.png

放入節(jié)點 3摊腋, 當前個數(shù)達到容量沸版,需要刪除一個最久使用的,才能插入新的兴蒸。

怎么刪除视粮,從當前節(jié)點出發(fā),順著箭頭數(shù)橙凳。數(shù)到容量個數(shù)的蕾殴,不重復節(jié)點,就都要的岛啸。(記得跳過钓觉,讀取不到值的節(jié)點 )
數(shù)到容量個數(shù)的,不重復節(jié)點的坚踩,前面的那一個不重復節(jié)點荡灾,就是要被刪除的。(記得跳過瞬铸,讀取不到值的節(jié)點 )
因為當前節(jié)點批幌,是最新使用的,越是箭頭方向嗓节,越是以前有用到荧缘,( 同一元素,第一次數(shù)到拦宣,為有效 )

5.png

刪除節(jié)點 2

6.png

讀取節(jié)點 2截粗,發(fā)現(xiàn)取不到信姓,返回 -1

7.png
8.png

最終:

9.png

數(shù)據(jù)結構部分:

采用了哈希表 ( Swift 中的字典 )和雙鏈表。

Key - Value 存取绸罗,當然要用哈希表意推。
要保證新插入和新使用的元素在前,很久沒使用的元素在后从诲,可以來一個鏈表左痢。

頭部元素,最近使用系洛。尾部方向元素俊性,最近越來越少用到

元素個數(shù)超過了容量,就要刪除尾部元素描扯,需要有一個尾指針記錄定页,有了尾指針,要刪除最后的元素绽诚,就要找到他的上一個指針來操作典徊,就要有前驅。(或者上一個指針的上一個來操作)

有了前驅恩够。鏈表元素自然要找到他的下一個卒落,也就是后繼。(鏈表的固有屬性)

鏈表存在前驅與后繼蜂桶,就是雙鏈表了儡毕。

另一角度,雙鏈表里面的節(jié)點扑媚,可以輕松實現(xiàn)自刪除腰湾,不需要其他指針的協(xié)助

簡單粗暴,實現(xiàn)一個 LRU, O ( 1 ) 復雜度

LRU 可以簡單分為兩部分疆股,數(shù)據(jù)的寫入與讀取

先實現(xiàn)寫入费坊,寫入了,進程里面才有數(shù)據(jù)旬痹,方便調試讀取

設計存入的部分

分情況處理附井,

如果要插入的元素,已經在鏈表里面了唱凯,根據(jù) key.
要維持鏈表的 LRU 有序羡忘,就要把他放在最前面,就要改他的前后指針磕昼,已經相關節(jié)點的。
先刪除他节猿,再把他插入頭節(jié)點票从,
還要更新他的 value, 也許這個 key 的值變了漫雕。

如果要插入的元素,不在鏈表里面了峰鄙,根據(jù) key.
又要考慮三種情況浸间,
如果是插入第一個元素,要先建立結構吟榴,假的頭部節(jié)點魁蒜,后面是假的尾節(jié)點
如果已經存在的元素滿了,要刪除最后面的元素吩翻,也就是最近少用到的
最后一種情況兜看,一切正常。把新的節(jié)點狭瞎,插入頭部第一個细移。



class DLinkedNode {
    // 這個是,刪除尾節(jié)點熊锭,要同步哈希表弧轧。哈希表也要對應刪除的時候,用到
    let key: Int
    var val: Int
    var prior: DLinkedNode?
    var next: DLinkedNode?
    
    init(_ key: Int, value: Int) {
        self.key = key
        val = value
    }

}


class LRUCache {
    
    var dummyHead = DLinkedNode(0, value: 0)
    var dummyTail = DLinkedNode(0, value: 0)
    var capacity: Int
    var container = [Int: DLinkedNode]()
    var hasCount: Int = 0

    init(_ capacity: Int) {
        self.capacity = capacity
    }
    
    func put(_ key: Int, _ value: Int) {
       // 先設計存的部分
    }
    
    func insertHead(_ node: DLinkedNode){
        let former = dummyHead.next
        former?.prior = node
        dummyHead.next = node
        node.prior = dummyHead
        node.next = former
    }
    
    func deleteNode(_ node: DLinkedNode){
        node.prior?.next = node.next
        node.next?.prior = node.prior
        node.prior = nil
        node.next = nil
    }
    
    func deleteTail(){
        if let toDel = dummyTail.prior{
            toDel.prior?.next = dummyTail
            dummyTail.prior = toDel.prior
            container.removeValue(forKey: toDel.key)
        }
    }
}


設計讀取的部分

讀取部分碗殷,相對簡單

哈希表中沒有 key, 就返回 -1 精绎,沒有

哈希表中存在 key, 就找到對應的節(jié)點锌妻,返回值代乃。同時把該節(jié)點更新到頭部第一個節(jié)點。
也就是在鏈表中从祝,先刪除襟己,再插入到頭部。



class DLinkedNode {
    let key: Int
    var val: Int
    var prior: DLinkedNode?
    var next: DLinkedNode?
    
    init(_ key: Int, value: Int) {
       self.key = key
        val = value
    }

}


class LRUCache {
    
    var dummyHead = DLinkedNode(0, value: 0)
    var dummyTail = DLinkedNode(0, value: 0)
    // 這個記錄設定的容量
    var capacity: Int
    var container = [Int: DLinkedNode]()
   // 這個記錄實際的元素個數(shù)
    var hasCount: Int = 0

    init(_ capacity: Int) {
        self.capacity = capacity
    }
    
    func get(_ key: Int) -> Int {
     // 再設計取的部分
    }

    func put(_ key: Int, _ value: Int) {
        if let node = container[key]{
            // 包含牍陌,就換順序
            // 還有一個更新操作
            node.val = value
            deleteNode(node)
            insertHead(node)
        }
        else{
            if hasCount == 0{
                // 建立結構
                dummyHead.next = dummyTail
                dummyTail.prior = dummyHead
            }
            if hasCount >= capacity{
                // 超過擎浴,就處理
                hasCount -= 1
                deleteTail()
            }
            hasCount += 1
            // 不包含,就插入頭節(jié)點
            let node = DLinkedNode(key, value: value)
            insertHead(node)
            container[key] = node
        }
    }
    
    func insertHead(_ node: DLinkedNode){
        let former = dummyHead.next
        former?.prior = node
        dummyHead.next = node
        node.prior = dummyHead
        node.next = former
    }
    
    func deleteNode(_ node: DLinkedNode){
        node.prior?.next = node.next
        node.next?.prior = node.prior
        node.prior = nil
        node.next = nil
    }
    
    func deleteTail(){
        if let toDel = dummyTail.prior{
            toDel.prior?.next = dummyTail
            dummyTail.prior = toDel.prior
            container.removeValue(forKey: toDel.key)
        }
    }
}


可以看出毒涧,LRU 的性能關鍵, 在于采用結構記錄與保持

每次存取贮预,都對鏈表做了更新 ( 除了取的時候,key 不存在 )

方便調試契讲,會更好仿吞。重寫了 NSObject 的 var description.

最后的完整版本:

優(yōu)化一點,
假的頭節(jié)點和尾節(jié)點的鏈表關系結構捡偏,可以一開始就建好唤冈,不用以后每次寫元素,都判斷


class DLinkedNode: NSObject {
    let key: Int
    var val: Int
    var prior: DLinkedNode?
    var next: DLinkedNode?
    
    init(_ key: Int, value: Int) {
       self.key = key
        val = value
    }
    // 輔助調試 debug, 打印出信息的银伟,方便看
    override var description: String{
        var result = String(val)
        var point = prior
        while let bee = point{
            result = "\(bee.val) -> " + result
            point = bee.prior
        }
        point = next
        while let bee = point{
            result = result + "-> \(bee.val)"
            point = bee.next
        }
        return result
    }
}




class LRUCache {
    // 怎樣化 O ( n ) 為 O ( 1 ). 關心的狀態(tài)你虹,都用一個專門的指針绘搞,記錄了
    var dummyHead = DLinkedNode(0, value: 0)
    var dummyTail = DLinkedNode(0, value: 0)
    var capacity: Int
    var container = [Int: DLinkedNode]()
    var hasCount: Int = 0

    init(_ capacity: Int) {
        self.capacity = capacity
          // 建立結構
          dummyHead.next = dummyTail
          dummyTail.prior = dummyHead
    }
    
    func get(_ key: Int) -> Int {
        // 有一個刷新機制
        if let node = container[key]{
            deleteNode(node)
            insertHead(node)
            return node.val
        }
        else{
            return -1
        }
    }
    
    func put(_ key: Int, _ value: Int) {
        if let node = container[key]{
            // 包含,就換順序
            // 還有一個更新操作
            node.val = value
            deleteNode(node)
            insertHead(node)
        }
        else{
            if hasCount >= capacity{
                // 超過傅物,就處理
                hasCount -= 1
                deleteTail()
            }
            hasCount += 1
            // 不包含夯辖,就插入頭節(jié)點
            let node = DLinkedNode(key, value: value)
            insertHead(node)
            container[key] = node
        }
    }
    
    func insertHead(_ node: DLinkedNode){
        let former = dummyHead.next
        former?.prior = node
        dummyHead.next = node
        node.prior = dummyHead
        node.next = former
    }
    
    // 指針操作,最好還是弄個變量董饰,接一下
    func deleteNode(_ node: DLinkedNode){
        node.prior?.next = node.next
        node.next?.prior = node.prior
        node.prior = nil
        node.next = nil
    }
    
    func deleteTail(){
        if let toDel = dummyTail.prior{
            toDel.prior?.next = dummyTail
            dummyTail.prior = toDel.prior
            container.removeValue(forKey: toDel.key)
        }
    }
}


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末蒿褂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子卒暂,更是在濱河造成了極大的恐慌啄栓,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件介却,死亡現(xiàn)場離奇詭異谴供,居然都是意外死亡,警方通過查閱死者的電腦和手機齿坷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門桂肌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人永淌,你說我怎么就攤上這事崎场。” “怎么了遂蛀?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵谭羔,是天一觀的道長伯顶。 經常有香客問我壤短,道長测摔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任所坯,我火速辦了婚禮谆扎,結果婚禮上,老公的妹妹穿的比我還像新娘芹助。我一直安慰自己堂湖,他們只是感情好,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布状土。 她就那樣靜靜地躺著无蜂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蒙谓。 梳的紋絲不亂的頭發(fā)上斥季,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音累驮,去河邊找鬼泻肯。 笑死渊迁,一個胖子當著我的面吹牛慰照,可吹牛的內容都是我干的灶挟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼毒租,長吁一口氣:“原來是場噩夢啊……” “哼稚铣!你這毒婦竟也來了?” 一聲冷哼從身側響起墅垮,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤惕医,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后算色,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抬伺,經...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年灾梦,在試婚紗的時候發(fā)現(xiàn)自己被綠了峡钓。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡若河,死狀恐怖能岩,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情萧福,我是刑警寧澤拉鹃,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站鲫忍,受9級特大地震影響膏燕,放射性物質發(fā)生泄漏。R本人自食惡果不足惜悟民,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一坝辫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧逾雄,春花似錦阀溶、人聲如沸鸦泳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽做鹰。三九已至击纬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钾麸,已是汗流浹背更振。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工炕桨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肯腕。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓献宫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親实撒。 傳聞我的和親對象是個殘疾皇子姊途,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內容