iOS端主流視頻直播技術(shù)

流程圖

流程圖.jpg

1菲饼、視頻編碼

11.png

1劲藐、1>初始化視頻編碼類

初始化調(diào)用:
VTCompressionSessionCreate(
kCFAllocatorDefault,
width,
height,
kCMVideoCodecType_H264,
nil,
attributes as CFDictionary?,
nil,
callback,
Unmanaged.passUnretained(self).toOpaque(),
&_session)
需要設(shè)置下熙掺,幅面流妻、碼率吼鳞、幀率赏表、回調(diào)函數(shù)等常規(guī)信息氢橙。
width,height分別是編碼的幅面大小酝枢。
kCMVideoCodecType_H264 采用的編碼技術(shù)。
attributes 流設(shè)置悍手,這里面涉及到的參數(shù):
[kVTCompressionPropertyKey_RealTime: kCFBooleanTrue, // 實(shí)時(shí)編碼
kVTCompressionPropertyKey_ProfileLevel: kVTProfileLevel_H264_Baseline_3_1 as NSObject, //編碼畫質(zhì) 低清Baseline Level 1.3隧枫,標(biāo)清Baseline Level 3,半高清Baseline Level 3.1谓苟,全高清Baseline Level 4.1(BaseLine表示直播官脓,Main存儲(chǔ)媒體,Hight高清存儲(chǔ)【只有:3.1 & 4.1】)
kVTCompressionPropertyKey_AverageBitRate: Int(bitrate) as NSObject, // 設(shè)置碼率
kVTCompressionPropertyKey_ExpectedFrameRate: NSNumber(value: expectedFPS), // 設(shè)置幀率
kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration: NSNumber(value: 2.0) // 關(guān)鍵幀間隔涝焙,單位秒, kVTCompressionPropertyKey_AllowFrameReordering: !isBaseline as NSObject, //是否產(chǎn)生B幀卑笨,直播設(shè)置為false【B幀是雙向差別幀,也就是B幀記錄的是本幀與前后幀的差別仑撞,B幀可以大大減少空間赤兴,但運(yùn)算量較大】
kVTCompressionPropertyKey_PixelTransferProperties: [
"ScalingMode": "Trim"
] as NSObject] 像素轉(zhuǎn)換規(guī)則
kVTCompressionPropertyKey_H264EntropyMode:kVTH264EntropyMode_CABAC // 如果是264編碼指定算法

2、2設(shè)置回調(diào)函數(shù)隧哮。

private var callback: VTCompressionOutputCallback = {(
outputCallbackRef: UnsafeMutableRawPointer?,
sourceFrameRef: UnsafeMutableRawPointer?,
status: OSStatus,
infoFlags: VTEncodeInfoFlags,
sampleBuffer: CMSampleBuffer?) in
guard let ref: UnsafeMutableRawPointer = outputCallbackRef,
let sampleBuffer: CMSampleBuffer = sampleBuffer, status == noErr else {
return
}
let encoder: H264Encoder = Unmanaged<H264Encoder>.fromOpaque(ref).takeUnretainedValue() //因?yàn)槌跏蓟臅r(shí)候傳了進(jìn)去桶良,現(xiàn)在取回來(lái)。
encoder.formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) // 得到視頻流沮翔,用于編碼
encoder.delegate?.sampleOutput(video: sampleBuffer) //交給外部處理,通過(guò)解析 CMSampleBufferRef 分別處理SPS陨帆,PPS,I-Frame和非I-Frame,然后通過(guò)RTMP推出去疲牵。
}

2.3 編碼

編碼后會(huì)自動(dòng)調(diào)用2.2的回調(diào)函數(shù)承二。
BTW:這是在視頻采集的時(shí)候調(diào)用這個(gè)
func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
VTCompressionSessionEncodeFrame(
session,
sampleBuffer,
CMSampleBufferGetPresentationTimeStamp(sampleBuffer),
CMSampleBufferGetDuration(sampleBuffer),
nil,
nil,
&flags
)
}

22.png

這個(gè)就是CMSampleBuffer的內(nèi)部結(jié)構(gòu)圖,編碼和解碼前后的內(nèi)部結(jié)構(gòu)
編碼就是CVPixelBuffer—>CMSampleBufferRef纲爸,解碼反之亥鸠。


2、音頻編碼

2识啦、1創(chuàng)建編碼器

AudioConverterNewSpecific(
&inSourceFormat!, //輸入?yún)?shù)
&inDestinationFormat, //輸出參數(shù)
UInt32(inClassDescriptions.count), //音頻描述符數(shù)量
&inClassDescriptions, //音頻描述符數(shù)組
&converter //編碼器
)
創(chuàng)建好編碼器后负蚊,還要修改一下編碼器的碼率
UInt32 outputBitrate = 64000 * channelscount // 還要* 通道數(shù)。需要注意颓哮,AAC并不是隨便的碼率都可以支持盖桥。比如,如果PCM采樣率是44100KHz题翻,那么碼率可以設(shè)置64000bps揩徊,如果是16K,可以設(shè)置為32000bps嵌赠。
UInt32 propSize = sizeof(outputBitrate);
AudioConverterSetProperty(audioConverter,
kAudioConverterEncodeBitRate,
propSize,
&outputBitrate);

2塑荒、2音頻描述文件

inDestinationFormat = AudioStreamBasicDescription()
inDestinationFormat!.mSampleRate = sampleRate == 0 ? inSourceFormat!.mSampleRate : sampleRate //設(shè)置采樣率,有 32K, 44.1K姜挺,48K
inDestinationFormat!.mFormatID = kAudioFormatMPEG4AAC // 采用AAC編碼方式
inDestinationFormat!.mFormatFlags = profile //指明格式的細(xì)節(jié). 設(shè)置為 0 說(shuō)明沒(méi)有子格式齿税。
inDestinationFormat!.mBytesPerPacket = 0 //每個(gè)音頻包的字節(jié)數(shù),該字段設(shè)置為 0, 表明包里的字節(jié)數(shù)是變化的。
inDestinationFormat!.mFramesPerPacket = 1024 每個(gè)音頻包幀的數(shù)量. 對(duì)于未壓縮的數(shù)據(jù)設(shè)置為 1. 動(dòng)態(tài)碼率格式炊豪,這個(gè)值是一個(gè)較大的固定數(shù)字凌箕,比如說(shuō)AAC的1024。如果是動(dòng)態(tài)幀數(shù)(比如Ogg格式)設(shè)置為0词渤。
inDestinationFormat!.mBytesPerFrame = 0 // 每個(gè)幀的字節(jié)數(shù)牵舱。對(duì)于壓縮數(shù)據(jù),設(shè)置為 0.
inDestinationFormat!.mChannelsPerFrame = 1 //音頻聲道數(shù)
inDestinationFormat!.mBitsPerChannel = 0 // 壓縮數(shù)據(jù)缺虐,該值設(shè)置為0.
inDestinationFormat!.mReserved = 0 // 用于字節(jié)對(duì)齊芜壁,必須是0.
CMAudioFormatDescriptionCreate(
kCFAllocatorDefault, &inDestinationFormat!, 0, nil, 0, nil, nil, &formatDescription
)

2、3轉(zhuǎn)碼

通過(guò)音頻捕獲獲取音頻流
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
// 編碼流程:
首先高氮,創(chuàng)建一個(gè) AudioBufferList慧妄,并將輸入數(shù)據(jù)存到 AudioBufferList里。
其次剪芍,設(shè)置輸出塞淹。
然后,調(diào)用 AudioConverterFillComplexBuffer 方法罪裹,該方法又會(huì)調(diào)用 inInputDataProc 回調(diào)函數(shù)饱普,將輸入數(shù)據(jù)拷貝到編碼器中运挫。
最后,轉(zhuǎn)碼费彼。將轉(zhuǎn)碼后的數(shù)據(jù)輸出到指定的輸出變量中。
//設(shè)置輸入
var blockBuffer: CMBlockBuffer?
currentBufferList = AudioBufferList.allocate(maximumBuffers: 1)
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
sampleBuffer,
nil,
currentBufferList!.unsafeMutablePointer,
AudioBufferList.sizeInBytes(maximumBuffers: 1),
kCFAllocatorDefault,
kCFAllocatorDefault,
0,
&blockBuffer
)
// 設(shè)置輸出
var finished: Bool = false
while !finished {
var ioOutputDataPacketSize: UInt32 = 1
let dataLength: Int = blockBuffer!.dataLength
let outOutputData: UnsafeMutableAudioBufferListPointer = AudioBufferList.allocate(maximumBuffers: 1)
outOutputData[0].mNumberChannels = inDestinationFormat.mChannelsPerFrame
outOutputData[0].mDataByteSize = UInt32(dataLength)
outOutputData[0].mData = UnsafeMutableRawPointer.allocate(byteCount: dataLength, alignment: 0)
let status: OSStatus = AudioConverterFillComplexBuffer(
converter,
inputDataProc,
Unmanaged.passUnretained(self).toOpaque(),
&ioOutputDataPacketSize,
outOutputData.unsafeMutablePointer,
nil
)
if 0 <= status && ioOutputDataPacketSize == 1 {
var result: CMSampleBuffer?
var timing: CMSampleTimingInfo = CMSampleTimingInfo(sampleBuffer: sampleBuffer)
let numSamples: CMItemCount = sampleBuffer.numSamples
CMSampleBufferCreate(kCFAllocatorDefault, nil, false, nil, nil, formatDescription, numSamples, 1, &timing, 0, nil, &result)
CMSampleBufferSetDataBufferFromAudioBufferList(result!, kCFAllocatorDefault, kCFAllocatorDefault, 0, outOutputData.unsafePointer) // 這里通過(guò)fillComplexBuffer指向outOutputData口芍,然后通過(guò)inputDataProc回調(diào)箍铲,最后再次回調(diào)給自己的onInputDataForAudioConverter函數(shù),再通過(guò)memcpy拷貝到這個(gè)outOutputData里鬓椭。下面的這行代碼才最終把buffer數(shù)據(jù)拿走
delegate?.sampleOutput(audio: result!)
} else {
finished = true
}
for i in 0..<outOutputData.count {
free(outOutputData[i].mData)
}
free(outOutputData.unsafeMutablePointer)
}
}
// 編碼解釋
AudioConverterFillComplexBuffer(
inAudioConverter: AudioConverterRef,
inInputDataProc: AudioConverterComplexInputDataProc,
inInputDataProcUserData: UnsafeMutablePointer,
ioOutputDataPacketSize: UnsafeMutablePointer<UInt32>,
outOutputData: UnsafeMutablePointer<AudioBufferList>,
outPacketDescription: AudioStreamPacketDescription
) -> OSStatus
inAudioConverter : 轉(zhuǎn)碼器
inInputDataProc : 回調(diào)函數(shù)颠猴。用于將PCM數(shù)據(jù)喂給編碼器。
inInputDataProcUserData : 用戶自定義數(shù)據(jù)指針小染。
ioOutputDataPacketSize : 輸出數(shù)據(jù)包大小翘瓮。
outOutputData : 輸出數(shù)據(jù) AudioBufferList 指針。
outPacketDescription : 輸出包描述符裤翩。
回調(diào)處理
private var inputDataProc: AudioConverterComplexInputDataProc = {(
converter: AudioConverterRef,
ioNumberDataPackets: UnsafeMutablePointer<UInt32>,
ioData: UnsafeMutablePointer<AudioBufferList>,
outDataPacketDescription: UnsafeMutablePointer<UnsafeMutablePointer<AudioStreamPacketDescription>?>?,
inUserData: UnsafeMutableRawPointer?) in
return Unmanaged<AACEncoder>.fromOpaque(inUserData!).takeUnretainedValue().onInputDataForAudioConverter(
ioNumberDataPackets,
ioData: ioData,
outDataPacketDescription: outDataPacketDescription
)
}
再回調(diào)處理
func onInputDataForAudioConverter(
_ ioNumberDataPackets: UnsafeMutablePointer<UInt32>,
ioData: UnsafeMutablePointer<AudioBufferList>,
outDataPacketDescription: UnsafeMutablePointer<UnsafeMutablePointer<AudioStreamPacketDescription>?>?) -> OSStatus {
guard let bufferList: UnsafeMutableAudioBufferListPointer = currentBufferList else {
ioNumberDataPackets.pointee = 0
return -1
}
memcpy(ioData, bufferList.unsafePointer, bufferListSize) // 通過(guò)上面的回調(diào)傳值處理资盅,然后再這里在通過(guò)memcpy把數(shù)據(jù)拷貝到iodata里實(shí)現(xiàn)數(shù)據(jù)的保存到outOutputData
ioNumberDataPackets.pointee = 1
free(bufferList.unsafeMutablePointer)
currentBufferList = nil
return noErr
}


3. 流合成。

通過(guò)1踊赠、2的音視頻的編碼操作呵扛,下面我們就可以合成流以便給Socket準(zhǔn)備發(fā)送的數(shù)據(jù)

3.1 視頻合成流
func sampleOutput(video sampleBuffer: CMSampleBuffer) {
let keyframe: Bool = !sampleBuffer.dependsOnOthers
var compositionTime: Int32 = 0
let presentationTimeStamp: CMTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
var decodeTimeStamp: CMTime = CMSampleBufferGetDecodeTimeStamp(sampleBuffer)
if decodeTimeStamp == kCMTimeInvalid {
decodeTimeStamp = presentationTimeStamp
} else {
compositionTime = Int32((decodeTimeStamp.seconds - decodeTimeStamp.seconds) * 1000)
}
let delta: Double = (videoTimestamp == kCMTimeZero ? 0 : decodeTimeStamp.seconds - videoTimestamp.seconds) * 1000
guard let data: Data = sampleBuffer.dataBuffer?.data, 0 <= delta else {
return
}
var buffer: Data = Data([((keyframe ? FLVFrameType.key.rawValue : FLVFrameType.inter.rawValue) << 4) | FLVVideoCodec.avc.rawValue, FLVAVCPacketType.nal.rawValue]) // 設(shè)置頭
buffer.append(contentsOf: compositionTime.bigEndian.data[1..<4]) // 大小端處理
buffer.append(data) //添加流數(shù)據(jù)
delegate?.sampleOutput(video: buffer, withTimestamp: delta, muxer: self) //回調(diào)出去
videoTimestamp = decodeTimeStamp
}
public enum FLVFrameType: UInt8 {
case key = 1
3.1 視頻合成流
func sampleOutput(video sampleBuffer: CMSampleBuffer) {
let keyframe: Bool = !sampleBuffer.dependsOnOthers
var compositionTime: Int32 = 0
let presentationTimeStamp: CMTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
var decodeTimeStamp: CMTime = CMSampleBufferGetDecodeTimeStamp(sampleBuffer)
if decodeTimeStamp == kCMTimeInvalid {
decodeTimeStamp = presentationTimeStamp
} else {
compositionTime = Int32((decodeTimeStamp.seconds - decodeTimeStamp.seconds) * 1000)
}
let delta: Double = (videoTimestamp == kCMTimeZero ? 0 : decodeTimeStamp.seconds - videoTimestamp.seconds) * 1000
guard let data: Data = sampleBuffer.dataBuffer?.data, 0 <= delta else {
return
}
var buffer: Data = Data([((keyframe ? FLVFrameType.key.rawValue : FLVFrameType.inter.rawValue) << 4) | FLVVideoCodec.avc.rawValue, FLVAVCPacketType.nal.rawValue]) // 設(shè)置頭
buffer.append(contentsOf: compositionTime.bigEndian.data[1..<4]) // 大小端處理
buffer.append(data) //添加流數(shù)據(jù)
delegate?.sampleOutput(video: buffer, withTimestamp: delta, muxer: self) //回調(diào)出去
videoTimestamp = decodeTimeStamp
}
public enum FLVFrameType: UInt8 {
case key = 1
case inter = 2
case disposable = 3
case generated = 4
case command = 5
}

3、2音頻合成流

func sampleOutput(audio sampleBuffer: CMSampleBuffer) {
let presentationTimeStamp: CMTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let delta: Double = (audioTimestamp == kCMTimeZero ? 0 : presentationTimeStamp.seconds - audioTimestamp.seconds) * 1000
guard let data: Data = sampleBuffer.dataBuffer?.data, 0 <= delta else {
return
}
var buffer: Data = Data([RTMPMuxer.aac, FLVAACPacketType.raw.rawValue]) // 設(shè)置頭
buffer.append(data) // 添加流數(shù)據(jù)
delegate?.sampleOutput(audio: buffer, withTimestamp: delta, muxer: self) // 回調(diào)出去
audioTimestamp = presentationTimeStamp
}
public enum FLVAACPacketType: UInt8 {
case seq = 0
case raw = 1
}

3筐带、3組RTMP協(xié)議數(shù)據(jù)今穿,僅供參考
func sampleOutput(audio buffer: Data, withTimestamp: Double, muxer: RTMPMuxer) {
guard readyState == .publishing else {
return
}
let type: FLVTagType = .audio
let length: Int = rtmpConnection.socket.doOutput(chunk: // 發(fā)送數(shù)據(jù)給socket,寫入inputstream
RTMPChunk( //拼接流數(shù)據(jù)
type: audioWasSent ? .one : .zero, // 是否是第一次發(fā)送用于處理大小端數(shù)據(jù)
streamId: type.streamId,
message: RTMPAudioMessage(streamId: id, timestamp: UInt32(audioTimestamp), payload: buffer)), locked: nil)
audioWasSent = true
OSAtomicAdd64(Int64(length), &info.byteCount) 原子鎖定伦籍,避免重復(fù)添加蓝晒。發(fā)送數(shù)據(jù)大小統(tǒng)計(jì)
audioTimestamp = withTimestamp + (audioTimestamp - floor(audioTimestamp))
}
和上面很接近只是增加了鎖
func sampleOutput(video buffer: Data, withTimestamp: Double, muxer: RTMPMuxer) {
guard readyState == .publishing else {
return
}
let type: FLVTagType = .video
OSAtomicOr32Barrier(1, &mixer.videoIO.encoder.locked)
let length: Int = rtmpConnection.socket.doOutput(chunk: RTMPChunk(
type: videoWasSent ? .one : .zero,
streamId: type.streamId,
message: RTMPVideoMessage(streamId: id, timestamp: UInt32(videoTimestamp), payload: buffer)
), locked: &mixer.videoIO.encoder.locked)
videoWasSent = true
OSAtomicAdd64(Int64(length), &info.byteCount)
videoTimestamp = withTimestamp + (videoTimestamp - floor(videoTimestamp))
frameCount += 1
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市帖鸦,隨后出現(xiàn)的幾起案子芝薇,更是在濱河造成了極大的恐慌,老刑警劉巖作儿,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剩燥,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡立倍,警方通過(guò)查閱死者的電腦和手機(jī)灭红,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)口注,“玉大人变擒,你說(shuō)我怎么就攤上這事∏拗荆” “怎么了娇斑?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵策添,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我毫缆,道長(zhǎng)唯竹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任苦丁,我火速辦了婚禮浸颓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘旺拉。我一直安慰自己产上,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布蛾狗。 她就那樣靜靜地躺著晋涣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪沉桌。 梳的紋絲不亂的頭發(fā)上谢鹊,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音留凭,去河邊找鬼撇贺。 笑死,一個(gè)胖子當(dāng)著我的面吹牛冰抢,可吹牛的內(nèi)容都是我干的松嘶。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼挎扰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼翠订!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起遵倦,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤尽超,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后梧躺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體似谁,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年掠哥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了巩踏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡续搀,死狀恐怖塞琼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情禁舷,我是刑警寧澤彪杉,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布毅往,位于F島的核電站,受9級(jí)特大地震影響派近,放射性物質(zhì)發(fā)生泄漏攀唯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一渴丸、第九天 我趴在偏房一處隱蔽的房頂上張望侯嘀。 院中可真熱鬧,春花似錦曙强、人聲如沸残拐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至囊卜,卻和暖如春娜扇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背栅组。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留玉掸,地道東北人刃麸。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像司浪,于是被迫代替她去往敵國(guó)和親泊业。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 導(dǎo)語(yǔ) 在上一篇中簡(jiǎn)單分析了 Weak 屬性是如何被存儲(chǔ)啊易,獲取和銷毀的吁伺,其中的 SideTable 結(jié)構(gòu)體當(dāng)做黑盒進(jìn)...
    iOSugarCom閱讀 1,138評(píng)論 0 5
  • 從前的我 烈日當(dāng)空下 盡情揮灑汗水 并沒(méi)求會(huì)超越誰(shuí) 只是單純的喜歡令你執(zhí)著 現(xiàn)在的我 烈日當(dāng)空下奔波 為追名逐利 ...
    Chring閱讀 78評(píng)論 0 0
  • 蒼二醫(yī)精細(xì)化管理項(xiàng)目的反饋會(huì)
    松林幽靜閱讀 203評(píng)論 1 0
  • 自然界每件事物 哪怕塵埃或是羽毛 也是按法則而不是靠運(yùn)氣運(yùn)動(dòng)的 種瓜得瓜 種豆得豆 依靠勤奮發(fā)力 掌握自己吃的面包...
    蠻小子閱讀 237評(píng)論 2 1