Object-C & Swift 自定义音乐播放器详解(基于 Avplayer)

记得刚写音乐播放器那会儿,和大多数人一样。都会想自己写的也许不够完善,还可能会出各种问题,而且现在有很多开源的比较完善的播放器,找个好用的就好了呀,之前也确实是这样做的。但是随着自己的App 在音频播放上的业务以及要求变更,你会发现第三方的完全不够用,那么就会想着去修改第三方的东西,这时候你会发现,第三方的东西虽好是很多大牛写的(基于底层)比较好用,但改起来却苦不堪言。所以最终还是回归本源,自己定义音频播放。最近在整理以前的东西,顺便在此分享一下,希望可以给刚写播放器的兄弟一些帮助.

自定义播放用选择的是ios 新的播放Api AVPlayer

优点:AVPlayer属于AVFoundation框架既可以播放本地音频也可以网络音频,更接近底层也会更加灵活,定制性比较高

用AVPlayer 播放音视频你会发现,它在设计上各个部分相对独立,这样更有利拆分使用,更加灵活。(比如 用AVPlayer 播放视频你会发现 和生活中看视频套路差不多 它也需要 播放器 显示屏 磁盘)

1 AVPlayer:播放器
2 AVPlayerLayer: 显示屏(如果要播放视频则要加上画布,音频则不用)
3 AVPlayerItem:一个媒体资源管理对象,管理者视频的一些基本信息和状态,一个AVPlayerItem对应着一个视频资源**
相关对象的意义:

AVAsset:AVAsset类专门用于获取多媒体的相关信息,包括获取多媒体的画面、声音等信息,属于一个抽象类,不能直接使用。
AVURLAsset:AVAsset的子类,可以根据一个URL路径创建一个包含媒体信息的AVURLAsset对象
CMTime:是一个结构体,里面存储着当前的播放进度,总的播放时长

你会发现 AVPlayer 虽然是一个整体的音频播放器但是,它内部把各个功能分成了单独的对象,定义时就相对独立,又可以组合完成功能。这样做耦合性会降低,这也能给我们启发,我们在写一些比较大的功能的时候,要把它们细化成小的功能(查错方便,其他地方也可以用)。接下来的自定义播放器也会用到这种思想。

接下来,就详细梳理一下自定义的AVPlayer 以swift 代码为例(为了紧跟时代步伐 - _ -)

oc 和 swift 版本
带缓冲进度的自定义进度条
友好的图片高斯模糊处理
全部在这里了。
WPYPlayer

不知道为什么图片模糊处理后录出来成这样了,项目比这个好看多了。


Untitled2.gif

Swift 自定义音乐播放器 主要分为

1 自定义Avplayer 的基本内容
2 一些附加功能
3 需要注意的问题

一 自定义Avplayer 的基本内容

1 单例初始化

    static let playManager = WPY_AVPlayer()
    var player : AVPlayer = {
        let _player = AVPlayer()
        _player.volume = 2.0 //默认最大音量
        
        return _player
    }()

播放器初始化

func  initPlayer() {  
        //APP进入后台通知
        NotificationCenter.default.addObserver(self, selector: #selector(configLockScreenPlay) , name:UIApplication.didEnterBackgroundNotification, object: nil)
        
        let session = AVAudioSession.sharedInstance()
        try? session.setActive(true)
        //后台播放
        Util_OC.setAVAudioSessionCategory(.playback)
    }

播放前需要配置一些监听事件
例如:
1 监听播放状态 (对于音频的不同状态,给与不懂操作)
2 缓冲加载情况(便于有加载播放进度条需求)
3 播放进度(就不用自己用定时器来表示播放时间,那样也不准确,直接用系统的就好)
4 播放结束通知(便于音频播放结束做相关操作)
5 监听打断处理(播放期间被 电话 短信 微信 等打断后的处理)

// 播放前增加配置 监测
    func currentItemAddObserver(){
        
        //监听是否靠近耳朵
        NotificationCenter.default.addObserver(self, selector: #selector(sensorStateChange), name:UIDevice.proximityStateDidChangeNotification, object: nil)
        
        //播放期间被 电话 短信 微信 等打断后的处理
        NotificationCenter.default.addObserver(self, selector: #selector(handleInterreption(sender:)), name:AVAudioSession.interruptionNotification, object:AVAudioSession.sharedInstance())
        
        // 监控播放结束通知
        NotificationCenter.default.addObserver(self, selector: #selector(playMusicFinished), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player.currentItem)
        //监听状态属性 ,注意AVPlayer也有一个status属性 通过监控它的status也可以获得播放状态
        
        self.player.currentItem?.addObserver(self, forKeyPath: "status", options:[.new,.old], context: nil)
        
        //监控缓冲加载情况属性
        self.player.currentItem?.addObserver(self, forKeyPath:"loadedTimeRanges", options: [.new,.old], context: nil)
        
        self.timeObserVer = self.player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main) { [weak self] (time) in
            
            guard let `self` = self else { return }
            
            let currentTime = CMTimeGetSeconds(time)
            self.progress = Float(currentTime)
            if self.isSeekingToTime {
                return
            }
        
            let total = self.durantion
            if total > 0 {
                self.delegate?.updateProgressWith(progress:Float(currentTime)  / Float(total))
            }
            
            
        }
    }

相应的当该 playItem 播放结束时 移除相关监测,观察

 // 播放后   删除配置 监测
    
    func currentItemRemoveObserver(){
        self.player.currentItem?.removeObserver(self, forKeyPath:"status")
        self.player.currentItem?.removeObserver(self, forKeyPath:"loadedTimeRanges")
        
        NotificationCenter.default.removeObserver(self, name:UIDevice.proximityStateDidChangeNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
        NotificationCenter.default.removeObserver(self, name:AVAudioSession.interruptionNotification, object: nil)
        
        if(self.timeObserVer != nil){
            self.player.removeTimeObserver(self.timeObserVer!)
        }
        
    }

一些监测的相关处理

1 app进入后台的 进行后台播放
注意:记得在工程中打开发后台播放功能 否则不会后台播放

1545300365959.jpg

//锁屏 或 退入后台 保持音频继续播放
    
    @objc func configLockScreenPlay() {
        //设置并激活音频会话类别
        let session = AVAudioSession.sharedInstance()
        
        Util_OC.setAVAudioSessionCategory(.playback)
        try? session.setActive(true)
        //允许应用接收远程控制
        
        //设置后台任务ID
        var  newTaskID = UIBackgroundTaskIdentifier.invalid
        newTaskID = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
        if (newTaskID != UIBackgroundTaskIdentifier.invalid) && (self.bgTaskId != UIBackgroundTaskIdentifier.invalid)  {
            UIApplication.shared.endBackgroundTask(self.bgTaskId)
        }
        
        self.bgTaskId = newTaskID
        
    }

监测和耳朵的距离 来判断是听筒 还是 外音 播放

 @objc func sensorStateChange() {
        
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
            
            if UIDevice.current.proximityState == true {
                
                //靠近耳朵
             /*  AVAudioSession *session = [AVAudioSession sharedInstance];
    
    [session setCategory:category withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker error:nil]; */
    
    //swift 4.2 后ios 10 以下不兼容 所以用了oc 的方式写的**
    
    Util_OC.setAVAudioSessionCategory(.playAndRecord)
            }else {
                //远离耳朵
                Util_OC.setAVAudioSessionCategory(.playback)
            }
        }
    }

处理播放音频是被来电 或者 其他 打断音频的处理

@objc func handleInterreption(sender:NSNotification) {
        
        let info = sender.userInfo
        guard let type : AVAudioSession.InterruptionType =  info?[AVAudioSessionInterruptionTypeKey] as? AVAudioSession.InterruptionType else { return }
        
        if type == AVAudioSession.InterruptionType.began {
            
            self.pause()
        }else {
            guard  let options = info![AVAudioSessionInterruptionOptionKey] as? AVAudioSession.InterruptionOptions else {return}
            
            if(options == AVAudioSession.InterruptionOptions.shouldResume){
                self.pause()
            }
        }
    }

单个音频播放结束后的逻辑处理

@objc func playMusicFinished(){
        
        UIDevice.current.isProximityMonitoringEnabled = true
        self.seekToZeroBeforePlay = true
        self.isPlay = false
        self.updateCurrentPlayState(state: AVPlayerPlayState.AVPlayerPlayStateEnd)
        
        //在这里进逻辑处理
        
        if (self.playType == WPY_AVPlayerType.PlayTypeSpecial) {
            
            self.next()
        }
    }

播放单个音频的方法 如播放 音效, 试听, 问题回答, 即无关联性只有url的

func playMusic(url : String,type:WPY_AVPlayerType){
        
        self.playType = type // 记录播放类型 以便做出不同处理
        self.setPlaySpeed(playSpeed: 1.0) //播放前初始化倍速 1.0
        
        self.currentItemRemoveObserver() //移除上一首的通知 观察
        
        let playUrl = self.loadAudioWithPlaypath(playpath: url)
        let playerItem = AVPlayerItem(url: playUrl)
        
        self.playerItem = playerItem
        self.currentUrl = url
        self.isImmediately = true
        
        self.player.replaceCurrentItem(with: playerItem)
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
        self .currentItemAddObserver()
    }

播放多个连续音频的方法 例如 音乐播放器,或者多个连续的音频

/// 用于播放多个音频的列表  播放方法
    ///
    /// - Parameters:
    ///   - index: 播放列表中的第几个音频
    ///   - isImmediately: 是否立即播放
    
    func playTheLine(index :Int,isImmediately :Bool){
        
        self.currentItemRemoveObserver()
        self.playType = .PlayTypeLine // 记录播放类型 以便做出不同处理
        
        let record = self.musicArray[index]
        
        guard let url = record.playpath else { return }
        let playUrl = self.loadAudioWithPlaypath(playpath:url )
        
        let playerItem = AVPlayerItem(url: playUrl)
        
        self.playerItem = playerItem
        self.currentUrl = url
        self.isImmediately = isImmediately
        self.currentScenicPoint = record
        self.currentIndex = index
        if !isImmediately {
            self.pause()
        }
        self.player.replaceCurrentItem(with: playerItem)
        
        self.currentItemAddObserver()
    }

实现观察者方法 根据 playitem 的播放状态做相应操作 以及 及时更新缓冲进度

/// 观察者   播放状态  和  缓冲进度
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        
        let item = object as! AVPlayerItem
        if keyPath == "status" {
            switch item.status {
            case AVPlayerItem.Status.readyToPlay:
                
                if isImmediately {
                    self.play()
                }else{
                    self.setNowPlayingInfo()
                }
            case AVPlayerItem.Status.failed,AVPlayerItem.Status.unknown:
                self.updateCurrentPlayState(state: AVPlayerPlayState.AVPlayerPlayStateNotPlay)
            }
        }else if keyPath == "loadedTimeRanges" {
            
            let array = item.loadedTimeRanges
            
            let timeRange = array.first?.timeRangeValue
            
            guard let start = timeRange?.start , let end = timeRange?.end else {
                return
            }
            
            let startSeconds = CMTimeGetSeconds(start)
            let durationSeconds = CMTimeGetSeconds(end)
            
            let totalBuffer = startSeconds + durationSeconds
            
            let total = self.durantion
            if totalBuffer != 0  && total != 0{
                
                delegate?.updateBufferProgress(progress: Float(totalBuffer) / Float(total))
                print("\(Float(totalBuffer) / Float(total))")
            }
        }
    }
}

这样 一个url 或者 数组 + 播放序列 就可以实现基本的播放音频了
接下可以写一下 播放器的四个基本操作

暂停

func pause(){....... player.pause() ..........}

播放

func play(){ .......   self.player.play() ......}

上一首

 func next(){ ...... changeTheMusicByIndex .......}

下一首

func previous(){ .......... changeTheMusicByIndex .........}

因为一般音频的切换会有很多相应的操作需要 比如界面的图片,文字的替换等等
所以我们统一下载了一个方法里

 func changeTheMusicByIndex(index : Int){
        self.playTheLine(index: index, isImmediately: true)
        
        delegate?.changeMusicToIndex(index: index)
        //
    }

那么作为一个成熟的自定义播放器我们应该给使用的地方提供哪些回调操作呢?

1 音频混缓冲进度

2 音频播放进度

3 音频切换的相应操作

这三个回调我们采用代理方式 因为这三个操作一般设计到了 播放界面的单独操作一般为 一对一的

protocol WPY_AVPlayerDelegate : class {
    
    func updateProgressWith(progress : Float)
    func changeMusicToIndex(index : Int)
    func updateBufferProgress(progress : Float)
}

4 音频播放状态
相对于音频播放状态而言,就不一定是一对一了,
例如: 有可能tableView 上的每个cell中都有试听 操作

而且个能有不同类型的各种播放形式,然后最基本的播放状态都是要的。所以 我们对播放状态的回调采用全局通知的形式

里面最好带参数
1 播放类型
2 播放链接 这样可以在一个界面有多个播放时来准确改变补个view 的状态
3 播放相应的状态类型 (统一管理播放状态)

如: 暂停, 播放, 结束, 缓冲准备, 播放出错
case AVPlayerPlayStatePreparing // 准备播放
case AVPlayerPlayStateBeigin // 开始播放
case AVPlayerPlayStatePlaying // 正在播放
case AVPlayerPlayStatePause // 播放暂停
case AVPlayerPlayStateEnd // 播放结束
case AVPlayerPlayStateBufferEmpty // 没有缓存的数据供播放了
case AVPlayerPlayStateBufferToKeepUp //有缓存的数据可以供播放
case AVPlayerPlayStateseekToZeroBeforePlay
case AVPlayerPlayStateNotPlay // 不能播放
case AVPlayerPlayStateNotKnow // 未知情况

/// 实时更新播放状态  全局通知(便于多个地方都用到音频播放,改变播放状态)
    ///
    /// - Parameter state: 播放状态
    
    func updateCurrentPlayState(state : AVPlayerPlayState){
        
        if self.currentUrl != nil {
            
            NotificationCenter.default.post(name: NSNotification.Name(rawValue: WPY_PlayerState), object: nil, userInfo: [WPY_PlayerState : state,CurrentPlayUrl : self.currentUrl!,PlayType : self.playType])
            
        }else {
            
            NotificationCenter.default.post(name: NSNotification.Name(rawValue: WPY_PlayerState), object: nil, userInfo: [WPY_PlayerState : state,CurrentPlayUrl : "",PlayType : self.playType])
        }
    }

至此,一个基本的自定义播放器就宣布完成了

二 附加功能

1 根据靠近耳朵距离 自由切换外音 和 听筒 模式

监听

//监听是否靠近耳朵
        NotificationCenter.default.addObserver(self, selector: #selector(sensorStateChange), name:UIDevice.proximityStateDidChangeNotification, object: nil)

相应操作

 //监测是否靠近耳朵  转换声音播放模式
    
    @objc func sensorStateChange() {
        
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
            
            if UIDevice.current.proximityState == true {
                
                //靠近耳朵
                Util_OC.setAVAudioSessionCategory(.playAndRecord)
            }else {
                //远离耳朵
                Util_OC.setAVAudioSessionCategory(.playback)
            }
        }
    }

因为 swift 4.2 对于ios 10.0 以下 不兼容,所以用了调oc的方法解决

有更好处理方式的欢迎交流

+ (void)setAVAudioSessionCategory:(AVAudioSessionCategory) category {
    
    AVAudioSession *session = [AVAudioSession sharedInstance];
    
    [session setCategory:category withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker error:nil];
}

注意:其实在音频停止播放后 就不应该有这种操作 (听筒转换 屏幕息屏)

所以 我们应该在 自己的暂停 函数中关掉红外感应

UIDevice.current.isProximityMonitoringEnabled = false

在播放函数中子打开

UIDevice.current.isProximityMonitoringEnabled = true

2 锁屏 显示播放信息

锁屏显示播放信息 包括到了状态
所以 我们先进行状态相关操作的时候 ,都应该调用信息设置操作

如 : 暂停 播放 改变进度等

/// 设置锁屏时 播放中心的播放信息、
    
    func setNowPlayingInfo(){
        
        if (self.playType == .PlayTypeLine || self.playType == .PlayTypeSpecial) && self.currentScenicPoint != nil {
            
           // 1  名字
            var info = Dictionary<String,Any>()
            info[MPMediaItemPropertyTitle] = self.currentScenicPoint?.name ?? ""   
            
            // 2 图片
            
            if let image = UIImage(named: "AppIcon"){
                info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image:image)//显示的图片
            }
            
//            if  let url = self.currentScenicPoint?.pictureArray?.first ,let image = UIImage(named: "AppIcon"){
//                imageView.kf.setImage(with: URL(string:url), placeholder: image, options: nil, progressBlock: nil) { (img, _, _, _) in
//
//                    if
//                    info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image:img)//显示的图片
//                }
//            }else{
//
//            }
            
            //3  总时长
            info[MPMediaItemPropertyPlaybackDuration] = self.durantion 
            
            if let duration = self.player.currentItem?.currentTime() {
               info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = CMTimeGetSeconds(duration)
            }
            
            //4 播放速率
            info[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
            
            //最后 设置
            MPNowPlayingInfoCenter.default().nowPlayingInfo = info
        }
    }
3 远程事件操作

记得进入后台后开启接收远程事件

UIApplication.shared.beginReceivingRemoteControlEvents()

在某些不需要远程事件是要关掉

UIApplication.shared.endReceivingRemoteControlEvents()

 //    //后台操作   在delegate 或者 某个VC 中初始化
    
    //    override func remoteControlReceived(with event: UIEvent?) {
    //        guard let event = event else {
    //            print("no event\n")
    //            return
    //        }
    //
    //        if event.type == UIEventType.remoteControl {
    //            switch event.subtype {
    //            case .remoteControlTogglePlayPause:
    //                print("暂停/播放")
    //
    //            case .remoteControlPreviousTrack:
    //                print("上一首")
    //                self.previous()
    //            case .remoteControlNextTrack:
    //                print("下一首")
    //                self.next()
    //            case .remoteControlPlay:
    //                print("播放")
    //               self.play()
    //            case .remoteControlPause:
    //                print("暂停")
    //                self.pause()
    //            default:
    //                break
    //            }
    //        }
    //    }
    //
4 改变播放速度

设置 playSpeed 属性用于记录 改变的播放速率
因为有可能是暂停状态下改的播放速率,不能及时生效。所以要记录一下

也真因为如此,所以播放时要及时更新下播放速率

self.enableAudioTracks(enable: true, playerItem: self.playerItem!)

注意: 暂停是调用此方法会直接播放,所以要放在播放时再调用

 //设置播放速率
    func setPlaySpeed(playSpeed:Float) {
        
        if self.isPlay{
            self.enableAudioTracks(enable: true, playerItem: self.playerItem!)
            self.player.rate = playSpeed;
        }
        self.playSpeed = playSpeed
    }

/// 改变播放速率 必实现的方法

    ///
    /// - Parameters:
    ///   - enable:
    ///   - playerItem: 当前播放
    func enableAudioTracks(enable:Bool,playerItem : AVPlayerItem){
        
        for track : AVPlayerItemTrack in playerItem.tracks {
            
            if track.assetTrack?.mediaType == AVMediaType.audio {
                
                track.isEnabled = enable
            }
        }
    }
5 判断网络状态 询问是否播放

这个一般的网络库中都有网络状态的判断,那么应该在哪里进行此操作呢

最合理的地方应该是 播放 方法里面。因为在此可以最大限度的控制流量,即使播到一半暂停 网络变化后也可以及时终止

三 需要注意的问题

1 进度条问题

进度条有两个改变

1 随着音频播放,逐渐改变。
2 手动调整位置,调整播放进度

但是这个连个问题会存在交叉问题,即在手动调整进度是如果音频播放不停,而且进度回调也一直在走,那么你会发现进度条在拉的时候是在跳动。

解决方案:
所以 在手动拉进度时,应该停掉音频播放的进度回调,在手动进度结束时,根据进度播放器把音频跳到指定位置播放,同时恢复音频进度回调

2 时间进度问题
- (NSString *)timeFormatted:(int)totalSeconds {
    int seconds = totalSeconds % 60;
    int minutes = (totalSeconds / 60);
    return [NSString stringWithFormat:@"%02d:%02d", minutes, seconds];
}

时间一般为两个 一个是当前时间 另一个是剩余时间或者总时间

这里就需要将avplayer 的时间 CTime 转换为字符串

object-C 的相对来说比较好处理

//视频的总长度
NSTimeInterval total = CMTimeGetSeconds(self.player.currentItem.duration);
直接取值转化字符串就好

问题是 swift

因为swift对类型要求比较严格,所以要进行类型转换。这时候你会发现在进行时间赋值是会崩溃

原因:因为 swift 是不会有默认值的。有时音频数据没取回,有可能就已经有赋值操作。

所以 我们在进行赋值操作前 要进行判断

 if self.isNaN || self.isInfinite {
            
            return "00:00"
        }
目前想到的差不多就这些,欢迎指正交流。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,066评论 4 62
  • 原文链接http://www.cnblogs.com/kenshincui/p/4186022.html 音频在i...
    Hyman0819阅读 21,682评论 4 74
  • 换窗纱,自己抠破自己换[呲牙] 貌似很难的事情 其实真做起来超级简单 觉得难其实是懒而已[憨笑] 修行亦复如是 感...
    欧阳晴413阅读 279评论 0 0
  • 生活有时候,像是一场流放 如果被流放的田野,那么就种一片向日葵,那么就给自己一个美丽的春天 如果被流放到城市,那么...
    一言尔阅读 245评论 0 0
  • 材料:玻璃杯子 纸板 水 步骤:把杯子装满水,拿一块薄一点的纸板,把它扣在杯子上,摁住倒过来,把手松开。 观察:杯...
    顾傲然阅读 138评论 0 0