<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[慎独]]></title><description><![CDATA[世界在他背后，原来这么的辽阔]]></description><link>https://xferris.cn/</link><image><url>http://xferris.cn/favicon.png</url><title>慎独</title><link>https://xferris.cn/</link></image><generator>Ghost 1.20</generator><lastBuildDate>Fri, 17 May 2024 13:39:45 GMT</lastBuildDate><atom:link href="https://xferris.cn/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[AVPlayer+AudioUnit之播放视频音轨(AVAssetTrack)]]></title><description><![CDATA[<div class="kg-card-markdown"><h2 id="">背景</h2>
<p>VoIP应用中，需要在通话端进行视频播放，同时该视频又不进入到VoIP声音中，避免产生回音现象。</p>
<h2 id="">参考</h2>
<ul>
<li><a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioUnitHostingGuide_iOS/Introduction/Introduction.html#//apple_ref/doc/uid/TP40009492-CH1-SW1">AudioUnit官方文档</a></li>
<li><a href="https://developer.apple.com/documentation/avfoundation/avaudiomixinputparameters/1388578-audiotapprocessor?language=objc">AudioTapProcessor官方Demo</a></li>
</ul>
<h2 id="">解法</h2>
<blockquote>
<p>iOS provides three I/O (input/output) units. The vast majority of audio-unit applications use the Remote I/O unit, which connects to input and output audio hardware and provides low-latency access to individual incoming and outgoing audio sample values. For</p></blockquote></div>]]></description><link>https://xferris.cn/avplayer-audiounitzhi-bo-fang-shi-pin-yin-gui-avassettrack/</link><guid isPermaLink="false">5f9bac576400dc0001f6f44e</guid><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Fri, 30 Oct 2020 07:12:47 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><h2 id="">背景</h2>
<p>VoIP应用中，需要在通话端进行视频播放，同时该视频又不进入到VoIP声音中，避免产生回音现象。</p>
<h2 id="">参考</h2>
<ul>
<li><a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioUnitHostingGuide_iOS/Introduction/Introduction.html#//apple_ref/doc/uid/TP40009492-CH1-SW1">AudioUnit官方文档</a></li>
<li><a href="https://developer.apple.com/documentation/avfoundation/avaudiomixinputparameters/1388578-audiotapprocessor?language=objc">AudioTapProcessor官方Demo</a></li>
</ul>
<h2 id="">解法</h2>
<blockquote>
<p>iOS provides three I/O (input/output) units. The vast majority of audio-unit applications use the Remote I/O unit, which connects to input and output audio hardware and provides low-latency access to individual incoming and outgoing audio sample values. For VoIP apps, the Voice-Processing I/O unit extends the Remote I/O unit by adding acoustic echo cancelation and other features. To send audio back to your application rather than to output audio hardware, use the Generic Output unit.</p>
</blockquote>
<p><strong>通过<code>Subtype</code>为<code>kAudioUnitSubType_VoiceProcessingIO</code>和<code>kAudioUnitSubType_RemoteIO</code>的AudioUnit来输出音频，可以使用上苹果自带的回音消除能力</strong></p>
<h3 id="">基本思路</h3>
<ul>
<li>如果AVPlayer使用AudioUnit，直接Hook改变subType完成。</li>
<li>从AVPlayer解码过程中取到实时音频数据，直接转推到另一个AudioUnit播放出来，这种方案要是能通，Seek等可以默认实现对齐。</li>
<li>保底方案，从AVPlayer取出PCM文件，做内存或者文件缓存，单独再播一份，需要手动对齐媒体时间。</li>
<li>保底方案，使用AVPlayer播视频，同时直接再解码一份，光播音频的，需要手动对齐媒体时间。</li>
</ul>
<h3 id="">尝试一</h3>
<p>首先是看到官网中的架构图，第一反应肯定是AVPlayer的音频播放也是基于AudioUnit，那就好办了<br>
<img src="http://image.simapps.cn/2020/10/avplayerhe-audiounit.png" alt="avplayerhe-audiounit"></p>
<p>直接<code>Hook</code>一下<code>AudioUnit</code>的几个核心函数，然后替换一下Unit初始化的subType。都是C函数，这里要使用到<code>fishhook</code>。要是hook</p>
<pre><code class="language-objectivec">+(void)load{
    int success = rebind_symbols((struct rebinding[1]){{&quot;AudioOutputUnitStart&quot;, fg_AudioOutputUnitStart, (void *)&amp;origin_AudioOutputUnitStart}}, 1);
    DebugLog(@&quot;%@&quot;,@(success));
}
</code></pre>
<p>分别尝试了</p>
<ul>
<li>AUGraphAddNode</li>
<li>AudioOutputUnitStart</li>
<li>AudioComponentInstanceNew</li>
<li>AudioUnitSetProperty<br>
发现AVPlayer均没有实现，于是这个方案失败告终。</li>
</ul>
<h3 id="">尝试二</h3>
<p>参考了苹果的<a href="https://developer.apple.com/documentation/avfoundation/avaudiomixinputparameters/1388578-audiotapprocessor?language=objc">AudioTapProcessor</a>DEMO，发现可以使用AudioMix方案来取到实时的音频数据，那转推一份就好了。<br>
首先从AVPlayer的KVO中监听状态，获得音轨。</p>
<pre><code class="language-objectivec">- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (object == self.player.currentItem) {
        if ([keyPath isEqualToString:@&quot;status&quot;]) {
            if (self.player.currentItem.status == AVPlayerItemStatusReadyToPlay) {
                //分离音频
                for (AVPlayerItemTrack* track in self.player.currentItem.tracks) {
                    if([track.assetTrack.mediaType isEqualToString:AVMediaTypeAudio]){
                        [self beginRecordingAudioFromTrack:track.assetTrack];
                    }
                }
            }
        }
    }
}
</code></pre>
<p>根据音轨生成AudioMix，赋值给PlayerItem</p>
<pre><code class="language-objectivec">    AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix];
    if (audioMix)
    {
        AVMutableAudioMixInputParameters *audioMixInputParameters = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:self.audioAssetTrack];
        if (audioMixInputParameters)
        {
            MTAudioProcessingTapCallbacks callbacks;
            
            callbacks.version = kMTAudioProcessingTapCallbacksVersion_0;
            callbacks.clientInfo = (__bridge void *)self,
            callbacks.init = tap_InitCallback;
            callbacks.finalize = tap_FinalizeCallback;
            callbacks.prepare = tap_PrepareCallback;
            callbacks.unprepare = tap_UnprepareCallback;
            callbacks.process = tap_ProcessCallback;
            
            MTAudioProcessingTapRef audioProcessingTap;
            if (noErr == MTAudioProcessingTapCreate(kCFAllocatorDefault, &amp;callbacks, kMTAudioProcessingTapCreationFlag_PostEffects, &amp;audioProcessingTap))
            {
                audioMixInputParameters.audioTapProcessor = audioProcessingTap;
                
                CFRelease(audioProcessingTap);
                
                audioMix.inputParameters = @[audioMixInputParameters];
                //赋值
                self.player.currentItem.audioMix = audioMix;
            }
        }
    }
</code></pre>
<p>在Prepare回调中获取音频格式信息，同时新建我们的outPutUnit</p>
<pre><code class="language-objectivec">static void tap_PrepareCallback(MTAudioProcessingTapRef tap, CMItemCount maxFrames, const AudioStreamBasicDescription *processingFormat)
{
    AVAudioTapProcessorContext *context = (AVAudioTapProcessorContext *)MTAudioProcessingTapGetStorage(tap);
    OSStatus status = noErr;
    AudioComponentDescription outputUnitDescription;
    outputUnitDescription.componentType = kAudioUnitType_Output;
    outputUnitDescription.componentSubType = kAudioUnitSubType_VoiceProcessingIO;
    outputUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
    outputUnitDescription.componentFlags = 0;
    outputUnitDescription.componentFlagsMask = 0;
    AudioComponent audioComponent = AudioComponentFindNext(NULL, &amp;outputUnitDescription);
    if(audioComponent){
        if (noErr == AudioComponentInstanceNew(audioComponent, &amp;context-&gt;outputUnit)){
            UInt32 maxFPS = (UInt32)maxFrames;
        
            //设置input和output
            status = AudioUnitSetProperty(context-&gt;outputUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,  0, processingFormat, sizeof(AudioStreamBasicDescription));
            UInt32 flag = 1;
            status = AudioUnitSetProperty(context-&gt;outputUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output,  0, &amp;flag, sizeof(flag));
            AudioUnitSetProperty(context-&gt;outputUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, processingFormat, sizeof(AudioStreamBasicDescription));
            //设置maxFrame
            status = AudioUnitSetProperty(context-&gt;outputUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0,&amp;maxFPS, sizeof(maxFPS));
            //设置renderLoop
            AURenderCallbackStruct callbackStruct;
            callbackStruct.inputProcRefCon = (void *)tap;
            callbackStruct.inputProc = RenderCallback;
            status = AudioUnitSetProperty(context-&gt;outputUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &amp;callbackStruct, sizeof(AURenderCallbackStruct));
            
            AudioUnitInitialize(context-&gt;outputUnit);
            //启动Unit
            AudioOutputUnitStart(context-&gt;outputUnit);
        }
    }
}

</code></pre>
<p>在process回调中获取并转存音频数据，尝试在这里直接把数据转发给outputUnit，会发现process的InputFrame(4096)和outputUnit的InputFrame(1024)不一致。<br>
这说明process回调的以后，并没有直接开始播放音频，这部分音频数据会缓存在内存中，等到要播的时候再取出来。参考苹果的思路，我们也转存到内存中，然后把原始音频静音，直接抹除掉所有数据。</p>
<pre><code class="language-objectivec">static void tap_ProcessCallback(MTAudioProcessingTapRef tap, CMItemCount numberFrames, MTAudioProcessingTapFlags flags, AudioBufferList *bufferListInOut, CMItemCount *numberFramesOut, MTAudioProcessingTapFlags *flagsOut)
{
    //不播放 手动增加frameout 转存data
    AVAudioTapProcessorContext *context = (AVAudioTapProcessorContext *)MTAudioProcessingTapGetStorage(tap);
    MYAudioTapProcessor *self = ((__bridge MYAudioTapProcessor *)context-&gt;self);
    OSStatus error = MTAudioProcessingTapGetSourceAudio(tap, numberFrames, bufferListInOut, flagsOut, NULL, numberFramesOut);
    if(error == noErr &amp;&amp; bufferListInOut-&gt;mBuffers[0].mDataByteSize &gt; 0){
        @synchronized (self) {
            
            self.currentTotalFrame += numberFrames;
            NSData* bufferData = [[NSData alloc] initWithBytes:bufferListInOut-&gt;mBuffers[0].mData length:bufferListInOut-&gt;mBuffers[0].mDataByteSize];
            //使用NSData可以实现内存区域的copy，类似memcpy。
            [self.totalBufferData appendData:bufferData];
        }
    }
    //清除原始音频数据 使之静音
    for (uint32_t i = 0; i &lt; bufferListInOut-&gt;mNumberBuffers; ++i) {
         memset(bufferListInOut-&gt;mBuffers[i].mData, 0, bufferListInOut-&gt;mBuffers[i].mDataByteSize);
     }
     *numberFramesOut = 0;
}
</code></pre>
<p>最后在我们的回调中计算1024需要的bufferData，从总的buffer中取出</p>
<pre><code class="language-objectivec">static OSStatus RenderCallback(void *userData, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData)
{
    AVAudioTapProcessorContext *context = (AVAudioTapProcessorContext *)MTAudioProcessingTapGetStorage(userData);
    MYAudioTapProcessor *self = ((__bridge MYAudioTapProcessor *)context-&gt;self);
    @synchronized (self) {
        if(self.currentPlayedFrame &lt; self.currentTotalFrame){
            //是均匀的4
            uint64_t perFrameLength = self.totalBufferData.length / self.currentTotalFrame;
            NSData* playData = [self.totalBufferData subdataWithRange:NSMakeRange(perFrameLength*self.currentPlayedFrame, perFrameLength*inNumberFrames)];
            memcpy(ioData-&gt;mBuffers[0].mData, playData.bytes, playData.length);
            ioData-&gt;mBuffers[0].mDataByteSize = perFrameLength*inNumberFrames;
            ioData-&gt;mBuffers[0].mNumberChannels = 1;
            self.currentPlayedFrame += inNumberFrames;
            return noErr;
        }else{
            return -1;
        }
    }
}
</code></pre>
<p>其中的<code>userData</code>是我们在初始化的时候传入的对象。</p>
<h3 id="">注意点</h3>
<ul>
<li>AudioUnit相关的操作必须在Audio线程中操作，可以在Tap的回调中操作，否则会导致线程死锁。</li>
<li>AudioUnit开启必须释放。</li>
</ul>
<pre><code class="language-objectivec">AudioOutputUnitStop(self.outputUnit);
AudioUnitUninitialize(self.outputUnit);
AudioComponentInstanceDispose(self.outputUnit);
</code></pre>
</div>]]></content:encoded></item><item><title><![CDATA[LLDB实战之导出Mac微信备份聊天记录的SQLite密码(SQLCipher加密)]]></title><description><![CDATA[<div class="kg-card-markdown"><p><strong>涉及到的LLDB命令</strong></p>
<ul>
<li>br: 设置断点</li>
<li>memory read: 读取内存原始值</li>
<li>po: 打印变量，也可以执行函数并且获得返回值</li>
<li>bt: 打印当前调用栈</li>
<li>thread step over/in/out: 单步跳过/进入/跳出</li>
<li>register: 寄存器操作</li>
<li>next/ni/n/step/si: 同上</li>
</ul>
<p><strong>参考链接</strong></p>
<ul>
<li><a href="https://www.zetetic.net/sqlcipher/sqlcipher-api/">SQLCipher</a></li>
<li><a href="https://github.com/Tencent/wcdb">WCDB</a></li>
<li><a href="https://blog.csdn.net/baidu_38172402/article/details/80658811">C函数形参列表与汇编寄存器的对应关系</a></li>
</ul>
<h3 id="0x00">0x00 准备工作</h3>
<p>查看WCDB所用的SQLite的加密方式，直接在WCDB的README里写了:</p>
<blockquote>
<p>Encryption Support: WCDB supports database encryption via SQLCipher.</p>
</blockquote>
<p>于是查看SQLCipher的API，看到用的是<code>sqlite3_key()</code>和<code>sqlite3_key_</code></p></div>]]></description><link>https://xferris.cn/dao-chu-wei-xin-bei-fen-de-mac/</link><guid isPermaLink="false">5dbc589ba9c4d600015f2370</guid><category><![CDATA[iOS]]></category><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Sun, 03 Nov 2019 05:47:26 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p><strong>涉及到的LLDB命令</strong></p>
<ul>
<li>br: 设置断点</li>
<li>memory read: 读取内存原始值</li>
<li>po: 打印变量，也可以执行函数并且获得返回值</li>
<li>bt: 打印当前调用栈</li>
<li>thread step over/in/out: 单步跳过/进入/跳出</li>
<li>register: 寄存器操作</li>
<li>next/ni/n/step/si: 同上</li>
</ul>
<p><strong>参考链接</strong></p>
<ul>
<li><a href="https://www.zetetic.net/sqlcipher/sqlcipher-api/">SQLCipher</a></li>
<li><a href="https://github.com/Tencent/wcdb">WCDB</a></li>
<li><a href="https://blog.csdn.net/baidu_38172402/article/details/80658811">C函数形参列表与汇编寄存器的对应关系</a></li>
</ul>
<h3 id="0x00">0x00 准备工作</h3>
<p>查看WCDB所用的SQLite的加密方式，直接在WCDB的README里写了:</p>
<blockquote>
<p>Encryption Support: WCDB supports database encryption via SQLCipher.</p>
</blockquote>
<p>于是查看SQLCipher的API，看到用的是<code>sqlite3_key()</code>和<code>sqlite3_key_v2()</code>这2个函数，在源码里搜索，找到调用，一共有两处，在<code>WCTDatabase+Database.mm</code>文件里</p>
<pre><code class="language-objectivec">- (void)setCipherKey:(NSData *)cipherKey
{
    _database-&gt;setCipher(cipherKey.bytes, (int) cipherKey.length);
}

- (void)setCipherKey:(NSData *)cipherKey andCipherPageSize:(int)cipherPageSize
{
    _database-&gt;setCipher(cipherKey.bytes, (int) cipherKey.length, cipherPageSize);
}
</code></pre>
<p>然后是下面的<strong>C寄存器的含义对照表</strong>。其中函数参数也可以不用寄存器表示，可以直接<code>$arg1/$arg2/$arg3</code>来表示 。<br>
<img src="http://image.simapps.cn/2019/11/20180611222227365.jpg" alt="20180611222227365"></p>
<h3 id="0x01">0x01获取保存目录</h3>
<p>开始用LLDB动态调试，首先Hook微信进程</p>
<pre><code class="language-bash">$ ps aux | grep WeChat

// 25132   6.7  1.0  7341704 170876   ??  S    Fri04PM   8:36.19 /Applications/WeChat.app/Contents/MacOS/WeChat  进程id是25132

$ lldb -p 25132 //开始hook进程

(lldb) process attach --pid 25132
Process 25132 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
    frame #0: 0x00007fff6162722a libsystem_kernel.dylib`mach_msg_trap + 10
libsystem_kernel.dylib`mach_msg_trap:
-&gt;  0x7fff6162722a &lt;+10&gt;: retq
    0x7fff6162722b &lt;+11&gt;: nop

libsystem_kernel.dylib`mach_msg_overwrite_trap:
    0x7fff6162722c &lt;+0&gt;:  movq   %rcx, %r10
    0x7fff6162722f &lt;+3&gt;:  movl   $0x1000020, %eax          ; imm = 0x1000020
Target 0: (WeChat) stopped.

Executable module set to &quot;/Applications/WeChat.app/Contents/MacOS/WeChat&quot;.
Architecture set to: x86_64h-apple-macosx.
(lldb)
</code></pre>
<p>进入LLDB命令行模式<br>
打断点获取微信的数据库目录，看WCDB的初始化接口，<code>[WCTDatabase [alloc] initWithPath:path];</code>我们要获取path</p>
<pre><code class="language-bash">(lldb) br set -n '[WCTDatabase initWithPath:]'
//输出
Breakpoint 1: where = WCDB`-[WCTDatabase(Database) initWithPath:], address = 0x0000000110b38676
(lldb) br list //查看断点
Current breakpoints:
1: names = {'[WCTDatabase initWithPath:]', '[WCTDatabase initWithPath:]'}, locations = 1, resolved = 1, hit count = 0
  1.1: where = WCDB`-[WCTDatabase(Database) initWithPath:], address = 0x0000000110b38676, resolved, hit count = 0
</code></pre>
<p>点击微信的备份文件，恢复聊天记录至手机或者管理备份文件来触发断点。<br>
<img src="http://image.simapps.cn/2019/11/wei-ming-ming.png" alt="wei-ming-ming"></p>
<pre><code>Process 25132 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000110b38676 WCDB`-[WCTDatabase(Database) initWithPath:]
WCDB`-[WCTDatabase(Database) initWithPath:]:
-&gt;  0x110b38676 &lt;+0&gt;: pushq  %rbp
    0x110b38677 &lt;+1&gt;: movq   %rsp, %rbp
    0x110b3867a &lt;+4&gt;: pushq  %r15
    0x110b3867c &lt;+6&gt;: pushq  %r14
Target 0: (WeChat) stopped.
(lldb) po $arg3 //或者po $rdx 打印第三个参数
/Users/xxxx/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/Backup/91f05ea8dc79f929613c0beb267b789a/75563957-B5FD-4CC5-90F4-0F36D91DD190/Backup.db
</code></pre>
<p>获取到备份的数据库位置，直接在finder中打开，发现一共有三个文件</p>
<ul>
<li>Backup.db</li>
<li>BAK_0_MEDIA</li>
<li>BAK_0_TEXT</li>
</ul>
<p><em>为什么是第三个参数呢？</em><br>
OC的对象方法调用，实际调用的是底层的<code>objc_msgSend($arg1,$arg2,...)</code>,其中<code>$arg1</code>为调用者本身，$arg2为方法名，后面的参数表示传递的实际参数，因此是从$arg3开始的，可以打印整个寄存器和<code>$arg1</code>,<code>$arg2</code>出来看看</p>
<pre><code>(lldb) register read
General Purpose Registers:
       rax = 0x00006000027b98e0
       rbx = 0x00007fff5fd16680  libobjc.A.dylib`objc_msgSend
       rcx = 0x0000000000000000
       rdx = 0x00006000012e63a0
       rdi = 0x00006000027b98e0
       rsi = 0x00007fff337c64f7  &quot;initWithPath:&quot;
       rbp = 0x00007ffee1a3f060
       rsp = 0x00007ffee1a3efb8
        r8 = 0x0000cc851f0d98e0
        r9 = 0x00000000000007fb
       r10 = 0x0000000110c78ee8  (void *)0x001d800110c78ec1
       r11 = 0x00007fcb9badab70
       r12 = 0x00006000037ca9c0
       r13 = 0x0000000000000000
       r14 = 0x00006000027b9a40
       r15 = 0x00006000037ca9c0
       rip = 0x0000000110b38676  WCDB`-[WCTDatabase(Database) initWithPath:]
    rflags = 0x0000000000000246
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000
(lldb) po $arg1 //po $rdi
&lt;WCTDatabase: 0x6000027b98e0&gt;
(lldb) po $arg2 //po $rsi 
140734057178359 //字符串打印出被转成数字了，可以用memory read打印
(lldb) memory read $rsi
0x7fff337c64f7: 69 6e 69 74 57 69 74 68 50 61 74 68 3a 00 73 65  initWithPath:.se
0x7fff337c6507: 74 46 69 6c 65 6e 61 6d 65 3a 00 69 6d 61 67 65  tFilename:.image
</code></pre>
<p>因此实际的函数参数会从第三个<code>$arg3</code>开始。</p>
<p>用<a href="http://www.sqlitebrowser.org/">sqlitebrowser</a>打开这个db文件，发现是SQLCipher加密，要输入密码。</p>
<h3 id="0x02sqlite3_key">0x02 获取sqlite3_key</h3>
<p>继续加断点，如果加在<code>sqlite3_key</code>上，会发现拿不到PageSize，查看源码看调用链，pageSize是在<code>void Database::setCipher(const void *key, int keySize, int pageSize)</code>的时候接收的，断点打在<code>setCipher</code>上</p>
<pre><code>(lldb) br set -n setCipher
(lldb) c //继续执行
</code></pre>
<p>触发到<code>sqlite3_key</code>的断点，<br>
获取key和pageSize</p>
<pre><code>(lldb) memory read $arg2
0x600003f8fa90: 64 64 30 36 33 35 65 63 65 62 35 37 39 35 32 66  dd0635eceb57952f
0x600003f8faa0: 31 62 35 65 63 31 65 64 33 37 38 30 36 65 31 30  1b5ec1ed37806e10

(lldb) po $arg3
32

(lldb) po $arg4
4096
</code></pre>
<p>key是<code>dd0635eceb57952f1b5ec1ed37806e10</code>,取32位，也就是整个字符串，页长度是4096.<br>
<img src="http://image.simapps.cn/2019/11/2222.png" alt="2222"><br>
打开数据库。<br>
分析一下表，发现文本内容存在<code>BAK_0_TEXT</code>,媒体内容存在<code>BAK_0_MEDIA</code>，以偏移量记录某条消息，简单查看一下这2个文件，都是写二进制数据，看来还用了某种加密方式。</p>
<h3 id="0x03todo">0x03 Todo</h3>
<ul>
<li>静态分析WeChat.app，获取打开两个加密文件的方法。</li>
</ul>
<h3 id="0x04update">0x04 Update</h3>
<p>有网友反馈key错误，跟了下发现是<a href="http://www.sqlitebrowser.org/">sqlitebrowser</a>更新了，支持了最新的SQLCipher4算法，但是微信依旧使用的WCDB1.0.3，还是SQLCipher3，操作的时候先选择SQLCipher3，之后再选择custom，把pageSize改成响应大小即可。<br>
<img src="http://image.simapps.cn/2020/03/20200307235656.jpg" alt="20200307235656"></p>
</div>]]></content:encoded></item><item><title><![CDATA[html2canvas图片模糊解决方案]]></title><description><![CDATA[<div class="kg-card-markdown"><blockquote>
<p>html2canvas官方的<a href="http://html2canvas.hertzen.com/configuration/">配置介绍</a></p>
</blockquote>
<h3 id="viewport">ViewPort布局方案</h3>
<p>页面采用<code>ViewPort</code>方案，解决iOS上的1px的边框问题，采用这个方案，在iOS上渲染出来的Dom会自动乘以<code>devicePixelRatio</code>，因此iOS上的Canvas相当于被直接放大了，没有出现模糊的情况。</p>
<pre><code class="language-javascript">var viewport = document.querySelector(&quot;meta[name=viewport]&quot;);
var deviceRatio = window.devicePixelRatio || 1;
var scale = 1.0/deviceRatio;
viewport.setAttribute('content', 'width=device-width,initial-scale='+scale + ', maximum-scale='+scale+', minimum-scale='+ scale +', user-scalable=no')</code></pre></div>]]></description><link>https://xferris.cn/html2canvas_make_img/</link><guid isPermaLink="false">5dba7382a9c4d600015f236b</guid><category><![CDATA[前端]]></category><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Mon, 22 Jul 2019 07:06:37 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><blockquote>
<p>html2canvas官方的<a href="http://html2canvas.hertzen.com/configuration/">配置介绍</a></p>
</blockquote>
<h3 id="viewport">ViewPort布局方案</h3>
<p>页面采用<code>ViewPort</code>方案，解决iOS上的1px的边框问题，采用这个方案，在iOS上渲染出来的Dom会自动乘以<code>devicePixelRatio</code>，因此iOS上的Canvas相当于被直接放大了，没有出现模糊的情况。</p>
<pre><code class="language-javascript">var viewport = document.querySelector(&quot;meta[name=viewport]&quot;);
var deviceRatio = window.devicePixelRatio || 1;
var scale = 1.0/deviceRatio;
viewport.setAttribute('content', 'width=device-width,initial-scale='+scale + ', maximum-scale='+scale+', minimum-scale='+ scale +', user-scalable=no');
</code></pre>
<p>发现<code>#id</code>锚点异常，是因为CSS的属性<code>text-size-adjust</code>在作怪，导致，关掉即可。</p>
<pre><code class="language-css">-webkit-text-size-adjust: 100%;
</code></pre>
<p>安卓也想采用ViewPort方案，发现会引入更多的问题，首先是<code>text-size-adjust</code>导致的布局异常问题，可以通过关闭所有设备上的<code>text-size-adjust</code>解决</p>
<pre><code class="language-css"> -ms-text-size-adjust: 100%;
 -webkit-text-size-adjust: 100%;
 text-size-adjust: 100%;
</code></pre>
<p>但是依旧会发现在某些设备上有布局问题，还有因为这个<code>text-size-adjust</code>导致的闪动问题，以及滑动卡顿问题(<code>devicePixelRatio</code>太大了导致原始dom的尺寸太大渲染不流畅)。找了一圈后发现手淘的<a href="https://github.com/amfe/article/issues/17">使用Flexible实现手淘H5页面的终端适配</a>在安卓上的这个值始终认为是0，看来是不好走通了。</p>
<blockquote>
<p>其中initial-dpr会把dpr强制设置为给定的值。如果手动设置了dpr之后，不管设备是多少的dpr，都会强制认为其dpr是你设置的值。在此不建议手动强制设置dpr，因为在Flexible中，只对iOS设备进行dpr的判断，对于Android系列，始终认为其dpr为1。</p>
</blockquote>
<p>到这里iOS不用任何配置直接使用<code>Html2Canvas</code>就可以画出清晰的图了。<br>
安卓还需要另外适配。</p>
<h3 id="">图片模糊问题</h3>
<p><code>html2canvas</code>一开始用的最新版本，发现dom在屏幕之外的部分始终无法绘制，调了半天最后换了个版本(往下降了一个版本)直接就好了。目前项目中使用的是<code>1.0.0-alpha.12</code>。</p>
<pre><code class="language-javascript">&quot;html2canvas&quot;: &quot;^1.0.0-alpha.12&quot;
</code></pre>
<ol>
<li>设置html2canvas的选项</li>
</ol>
<pre><code class="language-javascript">const html2canvasOpts = {
  backgroundColor: null,
  useCORS: $fn.isDev(),
  allowTaint: $fn.isDev(),
  scale: window.devicePixelRatio || 1,
  ignoreElements: (element) =&gt; {
    if(element.className.indexOf('no-share-element') &gt; -1){
      return true;
    }
    return false;
  },
  copyStyles: true,
  removeContainer: true
}
</code></pre>
<p>主要是<code>scale</code>，需要引入<code>devicePixelRatio</code>，即可保证canvas清晰。</p>
<ol start="2">
<li>不要使用<code>background-image:url()</code>属性，实验发现用这个属性渲染出来的图片都很糊，用img标签就好了。</li>
</ol>
<h3 id="">其他问题</h3>
<h4 id="">跨域问题</h4>
<p>由于涉及到外源图片，目前是通过后台写了一个接口做图片下载后pipe()来解决的，在开发环境的时候直接打开跨域和允许污染Canvas的属性</p>
<pre><code class="language-javascript">useCORS: $fn.isDev(), //允许跨域
allowTaint: $fn.isDev(), //允许污染画布
</code></pre>
<p>这样在开发环境只能看到Canvas却无法调用<code>canvas.toDataURL(&quot;image/png&quot;);</code>函数。</p>
<p>正式环境将所有的图片替换成接口调用，使之同源</p>
<pre><code class="language-javascript">if(!isDev()){
    coverUrl = '/api/fetchImage?src='+encodeURIComponent(coverUrl);
}
</code></pre>
<p>接口层代码，express侧</p>
<pre><code class="language-javascript">//使用http或者https或者request库直接请求，然后直接
response.pipe(res)
</code></pre>
<p>注意需要白名单和各种过滤规则，否则就是典型的ssrf了。</p>
</div>]]></content:encoded></item><item><title><![CDATA[UITableView图文混排自动布局滑动优化实战]]></title><description><![CDATA[<div class="kg-card-markdown"><h2 id="autolayout">AutoLayout和手动计算高度</h2>
<p>毫无疑问，使用AutoLayout会明显的比手动计算高度慢，那么我为什么要用AutoLayout呢，因为实在太方便了，而且视图太复杂，产品改的太频繁，手动计算实在工作量太大，维护起来超级麻烦。<br>
而且新的技术出来了，不用不是亏了吗。</p>
<h2 id="">方案</h2>
<h4 id="1">1.缓存高度</h4>
<p>既然手动计算高度更快，那就在<code>Reuse</code>的时候用AutoLayout帮我们算过后的高度就行了，缓存一个高度字典(或者数组)，在算完渲染出来的时候取高度，在取高度的时候做个判断就行。</p>
<pre><code class="language-objectivec">//保存高度
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
    [_cellHeightsDic setObject:@(cell.height) forKey:indexPath];
}
//设置高度
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{
    NSNumber</code></pre></div>]]></description><link>https://xferris.cn/ioszhong-de-gif/</link><guid isPermaLink="false">5dba7382a9c4d600015f236a</guid><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Thu, 06 Dec 2018 11:42:31 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><h2 id="autolayout">AutoLayout和手动计算高度</h2>
<p>毫无疑问，使用AutoLayout会明显的比手动计算高度慢，那么我为什么要用AutoLayout呢，因为实在太方便了，而且视图太复杂，产品改的太频繁，手动计算实在工作量太大，维护起来超级麻烦。<br>
而且新的技术出来了，不用不是亏了吗。</p>
<h2 id="">方案</h2>
<h4 id="1">1.缓存高度</h4>
<p>既然手动计算高度更快，那就在<code>Reuse</code>的时候用AutoLayout帮我们算过后的高度就行了，缓存一个高度字典(或者数组)，在算完渲染出来的时候取高度，在取高度的时候做个判断就行。</p>
<pre><code class="language-objectivec">//保存高度
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
    [_cellHeightsDic setObject:@(cell.height) forKey:indexPath];
}
//设置高度
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{
    NSNumber *height = [_cellHeightsDic objectForKey:indexPath];
    if (height) return height.doubleValue;
    return 400;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    NSNumber *height = [_cellHeightsDic objectForKey:indexPath];
    if (height) return height.doubleValue;
    return UITableViewAutomaticDimension;
}
</code></pre>
<h4 id="2">2.图片和内容懒渲染</h4>
<p>看不见的东西就不要让他渲染出来，这一步的优化是基于<code>cellForRowAtIndexPath</code>函数比<code>willDisplayCell</code>会先调用，如果在构造<code>cell</code>的时候就把所有内容填充上去，是一种浪费。因此可以把很重的内容，比如图片放到<code>willDisplayCell</code>的时候再加载。</p>
<pre><code class="language-objectivec">- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
    [_cellHeightsDic setObject:@(cell.height) forKey:indexPath];
    [cell willDisplay];
}
-(void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
    if ([tableView.indexPathsForVisibleRows indexOfObject:indexPath] == NSNotFound)
    {
        [(ABMainPageBaseTableViewCell*)cell endDisplay];
    }
}
</code></pre>
<p>然后在<code>willDisplay</code>和<code>endDisplay</code>里做些特殊的处理，对于<code>UIImageView</code>就可以</p>
<pre><code class="language-objectivec">-(void)endDisplay{
    [imageView setImage:nil];
    [imageView stopAnimating];
}
</code></pre>
<h4 id="3">3.预加载</h4>
<p>预加载分为两种，一种是图片预加载，另一种是内容预加载，先说内容预加载，其实就是在指定滚动到第几个<code>cell</code>的时候开始分页请求，这样用户就会无感知的开开心心的刷刷刷了。</p>
<pre><code class="language-objectivec">- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
    [_cellHeightsDic setObject:@(cell.height) forKey:indexPath];
    [cell willDisplay];
    if(_dataSource.count - indexPath.row &lt; 6){
        //剩下五个内容就立马开始刷新
        [self loadMore];
    }
}
</code></pre>
<p>要注意的是控制好你的网络请求，保证一次只发起一次<code>loadMore</code>请求,不要重复加载了。<br>
图片预加载，可以直接使用<code>SDWebImagePrefetcher</code>,下载图片</p>
<pre><code class="language-objectivec">[[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:imgUrls];
</code></pre>
<p>会自动创建网络请求下载图片，下载完存入内存和本地缓存里，下次使用直接使用<code>sd_setImageWithUrl</code>会自动去内存里寻找下载完的图片。</p>
<h4 id="4gif">4.GIF特殊处理</h4>
<p>如果GIF太多了，做完以上优化，会发现滑动到GIF的时候还是很卡，原来是因为<code>SDWebImage</code>直接把下载完的GIF内容直接填充给<code>UIImageView</code>，会直接按帧把动画渲染出来，边滑动边渲染图片到<code>UIImageView</code>上，就会导致UI线程阻塞，用户就感觉到卡顿了。<br>
于是尝试手动解GIF数据，使用第三方库<a href="https://github.com/Flipboard/FLAnimatedImage">FLAnimatedImage</a>手动解GIF，在渲染的时候从内存读入缓存完的<code>NSData</code>,庆幸的是最新的<code>SDWebImage</code>已经支持了<code>FLAnimatedImage</code>，因此可以直接愉快的<code>sd_setImage</code>了。<br>
最后要做的就是把滑动和GIF动画分开，想到的是<code>NSRunLoop</code>，因为滑动事件是在<code>NSEventTrackingRunLoopMode</code>下的，使用<code>NSDefaultRunLoopMode</code>就可以保证不在UI动画的时候取帧和渲染GIF。<br>
直接设置<code>FLAnimatedImageView</code>的<code>runLoopMode</code>即可。</p>
</div>]]></content:encoded></item><item><title><![CDATA[Kali Linux的Parallels Tools填坑记录]]></title><description><![CDATA[<div class="kg-card-markdown"><h3 id="0">0.安装过程遇到的主要问题：</h3>
<ul>
<li>1./media/cdrom0权限问题</li>
<li>2.apt-get源问题</li>
<li>3.无法安装linux-headers</li>
<li>4.makefile编译失败</li>
</ul>
<h3 id="1mediacdrom0">1./media/cdrom0权限问题</h3>
<p>点击安装<code>parallels tools</code>的时候，会有提示框，提示权限问题，如果直接运行<code>install</code>脚本，提示权限不够，官方推荐的做法：</p>
<ul>
<li>先卸载<code># umount /media/cdrom0</code></li>
<li>再挂载<code># mount -o exec /media/cdrom0</code><br>
按以上操作，依旧提示<code># mount: /media/cdrom0: WARNING: device write-protected, mounted read-only.</code></li>
</ul>
<p>解决方案：<br>
很简单，直接把文件复制到出来，然后<code>chmod</code></p></div>]]></description><link>https://xferris.cn/kali-pt-tools/</link><guid isPermaLink="false">5dba7382a9c4d600015f2368</guid><category><![CDATA[杂谈]]></category><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Sun, 03 Jun 2018 14:39:50 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><h3 id="0">0.安装过程遇到的主要问题：</h3>
<ul>
<li>1./media/cdrom0权限问题</li>
<li>2.apt-get源问题</li>
<li>3.无法安装linux-headers</li>
<li>4.makefile编译失败</li>
</ul>
<h3 id="1mediacdrom0">1./media/cdrom0权限问题</h3>
<p>点击安装<code>parallels tools</code>的时候，会有提示框，提示权限问题，如果直接运行<code>install</code>脚本，提示权限不够，官方推荐的做法：</p>
<ul>
<li>先卸载<code># umount /media/cdrom0</code></li>
<li>再挂载<code># mount -o exec /media/cdrom0</code><br>
按以上操作，依旧提示<code># mount: /media/cdrom0: WARNING: device write-protected, mounted read-only.</code></li>
</ul>
<p>解决方案：<br>
很简单，直接把文件复制到出来，然后<code>chmod 777 -R .</code>赋权即可~</p>
<h3 id="2aptget">2.apt-get源问题</h3>
<p>以下可用源填入<code>/etc/apt/sources.list</code>即可</p>
<pre><code class="language-bash">deb http://mirrors.ustc.edu.cn/kali kali-rolling main non-free contrib
deb-src http://mirrors.ustc.edu.cn/kali kali-rolling main non-free contrib

#阿里云
deb http://mirrors.aliyun.com/kali kali-rolling main non-free contrib
deb-src http://mirrors.aliyun.com/kali kali-rolling main non-free contrib

#清华大学
deb http://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main contrib non-free
deb-src https://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main contrib non-free

#浙大
deb http://mirrors.zju.edu.cn/kali kali-rolling main contrib non-free
deb-src http://mirrors.zju.edu.cn/kali kali-rolling main contrib non-free

#东软大学
deb http://mirrors.neusoft.edu.cn/kali kali-rolling/main non-free contrib
deb-src http://mirrors.neusoft.edu.cn/kali kali-rolling/main non-free contrib

#官方源
deb http://http.kali.org/kali kali-rolling main non-free contrib
deb-src http://http.kali.org/kali kali-rolling main non-free contrib
</code></pre>
<p>更新完依次执行</p>
<pre><code class="language-bash">apt-get update
apt-get upgrade -y
apt-get dist-upgrade -y
apt-get clean #可选
</code></pre>
<h3 id="3linuxheaders">3.无法安装linux-headers</h3>
<p>接下来的错误都是要查看日志文件了</p>
<pre><code class="language-bash"># cat /var/log/parallels-tools-install.log
</code></pre>
<p>如果是无法安装<code>linux-headers</code>的话，就要手动安装。<br>
先查看内核版本</p>
<pre><code class="language-bash"># uname -a
</code></pre>
<p>然后来这里<a href="http://http.kali.org/kali/pool/main/l/linux/">http://http.kali.org/kali/pool/main/l/linux/</a>下载三个对应内核版本的安装包手动安装</p>
<ul>
<li>linux-kbuild: linux-kbuild-xxxx_amd64.deb</li>
<li>linux-header-common: linux-headers-xxxx-common_xxxx_amd64.deb</li>
<li>linux-compiler-gcc: linux-compiler-gcc-xxx-amd64.deb</li>
<li>linux-headers: linux-headers-xxxx_amd64.deb<br>
下载完成后，用dpkg命令安装deb包。</li>
</ul>
<pre><code class="language-bash"># dpkg -i xxxxx.deb
</code></pre>
<h3 id="4makefile">4.makefile编译失败</h3>
<p>依旧查看日志文件，发现错误在<code>make</code>命令。</p>
<h5 id="parallelsdesktop">Parallels Desktop版本过低</h5>
<p>这种情况下，make错误会在诸如<code>get_user_pages()</code>等linux接口，之前一直用的是Parallels Desktop11，这次重新下了最新的kali，内核号是<code>4.15</code>，于是升级了Parallels Desktop,重新安装。</p>
<h5 id="linux">Linux版本过高</h5>
<p>尽管升级了PD，还是会有make错误，看日志发现死在了<code>prl_xxx</code>下的某些函数，原因是因为Parallels Tools不支持4.15的Linux内核，只能改源码了。具体修改如下：</p>
<ul>
<li>解压<code>kmods/prl_mod.tar</code></li>
</ul>
<pre><code class="language-bash"># tar -xzf kmods/prl_mod.tar.gz
# rm prl_mod.tar.gz
</code></pre>
<ul>
<li>修改<code>prl_eth/pvmnet/pvmnet.c</code></li>
</ul>
<pre><code class="language-bash"># vi kmods/prl_eth/pvmnet/pvmnet.c
# 编辑第438行，将其中的“Parallels”替换为“GPL”
#MODULE_LICENSE(&quot;Parallels&quot;)
MODULE_LICENSE(&quot;GPL&quot;)
</code></pre>
<ul>
<li>修改<code>prl_tg/Toolgate/Guest/Linux/prl_tg/prltg.c</code></li>
</ul>
<pre><code class="language-bash"># vi prl_tg/Toolgate/Guest/Linux/prl_tg/prltg.c
# 编辑第1535行，同样是将“Parallels”替换为“GPL”
</code></pre>
<ul>
<li>修改<code>prl_fs_freeze/Snapshot/Guest/Linux/prl_freeze/prl_fs_freeze.c</code></li>
</ul>
<pre><code class="language-C">//第一步：增加函数
//第212行
void thaw_timer_fn(unsigned long data)
{
   struct work_struct *work = (struct work_struct *)data;
   schedule_work(work);
}
//后面增加以下函数
void thaw_timer_fn_new_kernel(struct timer_list *data)
{
   struct work_struct *work = data-&gt;expires;
   schedule_work(work);
}

//第二步：修改宏
//刚刚的位置往下两行的
DEFINE_TIMER(thaw_timer, thaw_timer_fn, 0, (unsigned long)&amp;(thaw_work));
//改为
#if LINUX_VERSION_CODE &gt;= KERNEL_VERSION(4, 15, 0)
DEFINE_TIMER(thaw_timer, thaw_timer_fn_new_kernel);
#else
DEFINE_TIMER(thaw_timer, thaw_timer_fn, 0, (unsigned long)&amp;(thaw_work));
#endif
</code></pre>
<ul>
<li>重新打包<code>prl_mod.tar.gz</code></li>
</ul>
<pre><code class="language-bash"># tar -zcvf prl_mod.tar.gz . dkms.conf Makefile.kmods
</code></pre>
</div>]]></content:encoded></item><item><title><![CDATA[微信小程序圆环形进度条组件]]></title><description><![CDATA[利用CSS实现Web圆环形进度条]]></description><link>https://xferris.cn/circle-progress/</link><guid isPermaLink="false">5dba7382a9c4d600015f2366</guid><category><![CDATA[前端]]></category><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Thu, 10 May 2018 10:01:50 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>首先理解小程序的<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html">自定义组件</a>。</p>
<h3 id="">原理</h3>
<p>看了网上的一些教程，实现圆环用的是两个半圆的旋转，通过<code>overflow: hidden</code>来控制的。<br>
首先绘制底层容器，一个正方形，通过圆角变圆，用来作为未读进度的圆环。然后在上面绘制两个半矩形，在圆形矩形中绘制两个半圆（通过<code>border-left/right/top/bottom</code>加上旋转角来实现，默认的旋转角为135°）。<br>
<img src="http://image.simapps.cn/2018/05/1.png" alt="底部圆和半圆"><br>
<img src="http://image.simapps.cn/2018/05/yuan-huan-he-ban-yuan-huan.png" alt="圆环和半圆环">  通过控制半圆矩形(即半圆的父容器)，由于<code>overflow: hidden</code>，就会有如下的效果。<br>
<img src="http://image.simapps.cn/2018/05/xuan-zhuan-ban-yuan.png" alt="旋转矩形"><br>
<code>WXML</code>代码：<br>
由于必须通过js来控制style，所以不能写进<code>wxss</code>文件里，实在是很丑。</p>
<pre><code class="language-html">  &lt;view class='circle-progress-container' style=&quot;width: {{size}}rpx;height: {{size}}rpx;&quot;&gt;
  &lt;view class=&quot;circle-progress-outter-circle&quot; style=&quot;width: {{size}}rpx;height: {{size}}rpx;border: {{borderSize}}rpx solid {{normalColor}};&quot;&gt;
  &lt;/view&gt;
  &lt;view class='circle-progress-half-rect right-rect' style=&quot;width: {{size/2+1}}rpx;height: {{size}}rpx;&quot;&gt;
    &lt;view wx:if=&quot;{{currentProgress&gt;0}}&quot; class='circle-progress-half-circle right-circle' style=&quot;transform: rotate({{rightCircleRadius}}deg);width: {{size}}rpx;height: {{size}}rpx; border: {{borderSize}}rpx solid transparent;border-right: {{borderSize}}rpx solid {{borderColor}};border-bottom: {{borderSize}}rpx solid {{borderColor}};&quot; &gt;&lt;/view&gt;
  &lt;/view&gt;
  &lt;view class='circle-progress-half-rect left-rect' style=&quot;width: {{size/2+1}}rpx;height: {{size}}rpx;&quot;&gt;
    &lt;view wx:if=&quot;{{currentProgress&gt;0.5}}&quot; class='circle-progress-half-circle left-circle' style=&quot;transform: rotate({{leftCircleRadius}}deg);width: {{size}}rpx;height: {{size}}rpx; border: {{borderSize}}rpx solid transparent;border-left: {{borderSize}}rpx solid {{borderColor}};border-top: {{borderSize}}rpx solid {{borderColor}};&quot;&gt;&lt;/view&gt;
  &lt;/view&gt;
  &lt;/view&gt;
</code></pre>
<p><code>CSS</code>代码如下：</p>
<pre><code class="language-css">.circle-progress-outter-circle{
  border-radius: 50%;
  box-sizing: border-box;
}
.circle-progress-half-rect{
  position: absolute;
  overflow: hidden;
}
.right-rect{
  top: 0;
  right: 0;
}
.left-rect{
  top: 0;
  left: 0;
}
.circle-progress-half-circle{
  border-radius: 50%;
  box-sizing: border-box;
  position: absolute;   
}
.right-circle{
  top:0;  
  right: 0;
  transform: rotate(-45deg);
}
.left-circle{
  top:0;  
  left: 0;
  transform: rotate(-45deg);
}
</code></pre>
<p>组件的JS：</p>
<pre><code class="language-javascript">Component({
  properties: {
    currentProgress:{
      type: Number,
      value: 0,
      observer: '_progressDidChange',
    },
    size:{
      type: Number,
      value: 200
    },
    borderSize:{
      type: Number,
      value: 20
    },
    borderColor:{
      type: String,
      value: &quot;green&quot;
    },
    normalColor:{
      type: String,
      value: &quot;gray&quot;
    }
  },
  data: {
    rightCircleRadius: 135,
    leftCircleRadius: 135,
  },
  methods: {
    _progressDidChange: function(newVal,oldVal){
      var that = this;
      var newLeftRadius = that.data.leftCircleRadius;
      var newRightRadius = that.data.rightCircleRadius;
      var radius = 360 * newVal;
      if(newVal &lt; 0.5 &amp;&amp; newVal &gt;= 0){
        //只需要旋转右边的值
        newLeftRadius = 135;
        newRightRadius = 135 + radius;
      }else if(newVal &lt;= 1 &amp;&amp; newVal &gt;=0.5){
        //两边都需要旋转
        newLeftRadius = radius - 45;
        newRightRadius = -45;
      }
      that.setData({rightCircleRadius:newRightRadius,leftCircleRadius:newLeftRadius});
    }
  }
})
</code></pre>
<p>顺时针还是逆时针的话，只需要调整<code>_progressDidChange</code>里的函数就可以了。</p>
<h3 id="">使用组件</h3>
<pre><code class="language-html"> &lt;circle-progress wx:if=&quot;{{isPlaying}}&quot;  size=&quot;54&quot; normalColor=&quot;#eaeaea&quot; borderColor=&quot;#BF831E&quot; borderSize=&quot;4&quot; currentProgress=&quot;{{progress}}&quot;&gt;&lt;/circle-progress&gt;
</code></pre>
<ul>
<li>borderSize: 表示进度条粗细。</li>
<li>borderColor: 表示进度条颜色。</li>
<li>normalColor: 表示未读进度条颜色。</li>
<li>progress: 在外部通过<code>page.setData()</code>函数来设置实时进度。</li>
<li>size：圆环的尺寸。<br>
别忘了在<code>page.json</code>里声明</li>
</ul>
<pre><code class="language-javascript">  &quot;usingComponents&quot;: {
      &quot;circle-progress&quot;: &quot;/components/circle-progress/circle-progress&quot;
  }
</code></pre>
</div>]]></content:encoded></item><item><title><![CDATA[AVPlayer初体验之边下边播与视频缓存]]></title><description><![CDATA[上篇文章介绍了AVPlayer的基本播放和解码纹理，本文主要利用AVAssetResourceLoaderDelegate实现AVPlayer的边下边播和缓存机制。]]></description><link>https://xferris.cn/avplayer_cache/</link><guid isPermaLink="false">5dba7382a9c4d600015f2364</guid><category><![CDATA[iOS]]></category><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Wed, 28 Feb 2018 03:15:55 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p><a href="http://xferris.cn/avplayer_video_texture/">上篇文章</a>介绍了AVPlayer的基本播放和解码纹理，本文主要利用<code>AVAssetResourceLoaderDelegate</code>实现AVPlayer的边下边播和缓存机制。</p>
<h2 id="">基本原理</h2>
<p><code>AVUrlAsset</code>在请求自定义的URLScheme资源的时候会通过<code>AVAssetResourceLoader</code>实例来进行资源请求。它是AVUrlAsset的属性,声明如下：</p>
<pre><code class="language-swift">var resourceLoader: AVAssetResourceLoader { get }
</code></pre>
<p>而<code>AVAssetResourceLoader</code>请求的时候会把相关请求(<code>AVAssetResourceLoadingRequest</code>)传递给<code>AVAssetResourceLoaderDelegate</code>(如果有实现的话)，我们可以保存这些请求，然后构造自己的<code>NSUrlRequset</code>来发送请求，当收到响应的时候，把响应的数据设置给<code>AVAssetResourceLoadingRequest</code>,并且对数据进行缓存，就完成了边下边播，整个流程大体如下图。<br>
<img src="http://image.simapps.cn/2018/02/bian-xia-bian-bo-yuan-li-tu.jpg" alt="bian-xia-bian-bo-yuan-li-tu"><br>
其中最为复杂的部分是数据偏移处理，因为数据是分块下载和分块填充的，我们的需要填充的对象是<code>AVAssetResourceLoadingDataRequest</code>，需要控制好<code>currentOffset</code>。</p>
<h2 id="">实现</h2>
<h4 id="">必要的配置</h4>
<p>手动实现<code>AVAssetResourceLoaderDelegate</code>协议需要URL是自定义的URLScheme,只需要把源URL的<code>http://</code>或者<code>https://</code>替换成<code>xxxx://</code>，然后再实现<code>AVAssetResourceLoaderDelegate</code>协议函数才可以生效，否则不会生效。</p>
<pre><code class="language-swift">//首先判断是否有缓存，如果没有缓存才走下面的步骤，有缓存直接从`file://`读取
let asset = AVURLAsset(url: urlWithCustomScheme)
//urlWithCustomScheme = &quot;xxxx://xxxx.mp4&quot;
asset.resourceLoader.setDelegate(resourceLoaderDelegate, queue: self.queue)
</code></pre>
<h4 id="avassetresourceloaderdelegate">AVAssetResourceLoaderDelegate协议</h4>
<p><code>AVAssetResourceLoaderDelegate</code>是<code>AVPlayer</code>在向媒体服务器请求数据时的代理，为了实现边下边播，需要实现自定义请求，需要实现的两个方法如下：</p>
<ul>
<li><code>optional func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -&gt; Bool</code><br>
该函数表示代理类是否可以处理该请求，这里需要返回True表示可以处理该请求，然后在这里保存所有发出的请求，然后发出我们自己构造的<code>NSUrlRequest</code>。</li>
<li><code>optional func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest)</code><br>
该函数表示<code>AVAssetResourceLoader</code>放弃了本次请求，需要把该请求从我们保存的原始请求列表里移除。</li>
</ul>
<p>以上两个是必须要实现的方法，其他的<a href="https://developer.apple.com/documentation/avfoundation/avassetresourceloaderdelegate">函数</a>依照具体的场景（比如需要鉴权则需要实现两个鉴权函数来处理<code>URLAuthenticationChallenge</code>）具体看是否需要实现。</p>
<h4 id="">一个最简单的实例</h4>
<p>下面实现一个不带分块下载功能的最简单的边下边播代理，帮助理解<code>AVAssetResourceLoaderDelegate协议</code>。<br>
注意，以下代码不带分块功能，是因为只发送一个请求，利用<code>NSUrlSession</code>直接请求视频资源，针对元信息在视频文件头部的视频可以实现边下边播，而元信息在视频尾部的视频则会下载完才播放，关于这个视频元信息(<em>moov</em>)接下来会再讨论，以下代码缓存也是放在下载完整个视频做，而不是分块写入文件。</p>
<pre><code class="language-swift">func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -&gt; Bool {
    if session == nil {
        //由于使用了自定义UrlScheme，需要构造出原始的URL
        guard let interceptedUrl = loadingRequest.request.url,
            let initialUrl = interceptedUrl.withScheme(self.initialScheme) else {
                fatalError(&quot;internal inconsistency&quot;)
        }
        let configuration = URLSessionConfiguration.default
        configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
        configuration.networkServiceType = .video
        configuration.allowsCellularAccess = true
        var urlRequst = URLRequest.init(url: initialUrl, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 20) // 20s超时
        urlRequst.setValue(&quot;application/octet-stream&quot;, forHTTPHeaderField: &quot;Content-Type&quot;)
        urlRequst.httpMethod = &quot;GET&quot;
        session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
        session?.dataTask(with: urlRequst).resume()
    }
    //保存原始请求
    self.pendingRequests.insert(loadingRequest)
    //每次发送请求都遍历处理一遍原始请求数组
    self.processPendingRequests()
    return true
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {
    //移除原始请求
    self.pendingRequests.remove(loadingRequest)
}
</code></pre>
<p><code>NSUrlRequest</code>响应回调处理</p>
<pre><code class="language-swift">// MARK: URLSession delegate
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    self.mediaData?.append(data)
    self.processPendingRequests()
    //print(&quot;数据下载成功 已下载\( mediaData!.count) 总数据\(Int(dataTask.countOfBytesExpectedToReceive))&quot;)
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -&gt; Void) {
    //只会调用一次，在这里构造下载完成的数据
    //这里传allow告知session持续下载而不是当做下载任务
    completionHandler(Foundation.URLSession.ResponseDisposition.allow)
    self.mediaData = Data()
    self.response = response
    self.processPendingRequests()
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    if let errorUnwrapped = error {
        print(&quot;下载失败\(errorUnwrapped)&quot;)
        return
    }
    self.processPendingRequests()
    //下载完成，保存文件
    let fileName = self.fileCachePath
    if let data = self.mediaData{
        VideoCacheManager.share.saveData(data:data,url:self.url)
    }else{
        print(&quot;数据为空&quot;)
    }
}
</code></pre>
<p>填充响应以及判断请求是否完成</p>
<pre><code class="language-swift">func processPendingRequests() {
    self.queue.async {
        let requestsFulfilled = Set&lt;AVAssetResourceLoadingRequest&gt;(self.pendingRequests.flatMap {
            if let res = self.response{
                $0.response = res
            }
            self.fillInContentInformationRequest($0.contentInformationRequest)
            if self.haveEnoughDataToFulfillRequest($0.dataRequest!) {
                if(!$0.isFinished){
                    $0.finishLoading()
                }
                //print(&quot;请求填充完成 结束本次请求&quot;)
                return $0
            }
            return nil
        })
        _ = requestsFulfilled.map { self.pendingRequests.remove($0) }
    }
}
//填充请求
func fillInContentInformationRequest(_ contentInformationRequest: AVAssetResourceLoadingContentInformationRequest?) {
    self.queue.async {
        guard let responseUnwrapped = self.response else {
            return
        }
        contentInformationRequest?.contentType = responseUnwrapped.mimeType
        contentInformationRequest?.contentLength = responseUnwrapped.expectedContentLength
        contentInformationRequest?.isByteRangeAccessSupported = true
    }
}
//判断是否完整
func haveEnoughDataToFulfillRequest(_ dataRequest: AVAssetResourceLoadingDataRequest) -&gt; Bool {
    
    let requestedOffset = Int(dataRequest.requestedOffset)
    let requestedLength = dataRequest.requestedLength
    let currentOffset = Int(dataRequest.currentOffset)
    //print(&quot;下载数据 = \(mediaData?.count) 当前偏差\(currentOffset)&quot;)
    guard let dataUnwrapped = mediaData,
        dataUnwrapped.count &gt; currentOffset else {
            //没有新的内容可以填充
            return false
    }
    
    let bytesToRespond = min(dataUnwrapped.count - currentOffset, requestedLength)
    let dataToRespond = dataUnwrapped.subdata(in: Range(uncheckedBounds: (currentOffset, currentOffset + bytesToRespond)))
    dataRequest.respond(with: dataToRespond)
    //print(&quot;原始请求获得响应\(dataToRespond.count)&quot;)
    return dataUnwrapped.count &gt;= requestedLength + requestedOffset
}
</code></pre>
<p>再次注意，以上代码在收到原始请求后，并没有每次都发送请求，而是在第一次收到的时候只发送一次请求，利用<code>NSUrlSessionDatatask</code>的<em>continues task</em>特性来下载完整个媒体，所以是视频文件的头部开始下载，并且缓存也是在视频文件都下载完成之后才一次性写入文件的。因此，先不谈分块下载，以上代码会非常容易理解。接下来谈谈视频的格式问题。</p>
<h4 id="mp4">为什么以上代码不能边下边播所有MP4</h4>
<p>以上代码本质上只发送了一个<code>NSUrlRequest</code>，这个HTTP请求的头部没有带有<code>Byte-Range</code>信息，因此媒体服务器并不知道你需要请求的长度，就会把它当做一个文件流从头部请求到尾部，因此我们指定<code>Foundation.URLSession.ResponseDisposition.allow</code>告诉这个<code>URLSession</code>把它当做一个<em>continues task</em>来下载，于是从文件头部开始下载，但是真正的视频流并不是这么下载的。<br>
尝试用Safari播放在线视频，抓包查看请求细节，如下图：<br>
<img src="http://image.simapps.cn/2018/02/request-header.jpg" alt="request-header"><br>
在请求头里有一个<code>Range:byte</code>字段来告诉媒体服务器需要请求的是哪一段特定长度的文件内容，对于MP4文件来说，所有数据都封装在一个个的<em>box</em>或者<em>atom</em>中，其中有两个atom尤为重要，分别是<code>moov atom</code>和<code>mdat atom</code>。</p>
<ul>
<li>moov atom：包含媒体的元数据的数据结构，包括媒体的块(box)信息，格式说明等等。(<em>Meta data about where the video and audio atoms are, as well as information about how to play the video like the dimensions and frames per second, is all stored in a special atom called the moov atom。</em>)</li>
<li>mdat atom: 包含媒体的媒体信息，对于视屏来说就是视频画面了。</li>
</ul>
<p>虽然<code>moov</code>和<code>mdat</code>都只有一个，但是由于MP4文件是由若干个这样的<em>box</em>或者<em>atom</em>组成的，因此这两个<code>atom</code>在不同媒体文件中出现的顺序可能会不一样，为了加快流媒体的播放，我们可以做的优化之一就是手动把<code>moov</code>提到<code>mdat</code>之前。<br>
对于<code>AVPlayer</code>来说，只有到<code>AVPlayerItemStatusReadyToPlay</code>状态时，才可以开始播放视频，而进入<code>AVPlayerItemStatusReadyToPlay</code>状态的必要条件就是播放器读到了媒体的<code>moov</code>块。<br>
那么以上代码不能边下边播的视频，是否都是<code>mdat</code>位于<code>moov</code>之后呢，答案显然是肯定的，用二进制打开一个不能边下边播的视频，查找<code>mdat</code>和<code>moov</code>的位置如下：<br>
<img src="http://image.simapps.cn/2018/02/mdat-before-moov.jpg" alt="mdat-before-moov"><br>
<code>mdat</code>位于<code>0x000018</code>的位置。<br>
<img src="http://image.simapps.cn/2018/02/mdat-before-moov-2.jpg" alt="mdat-before-moov-2"><br>
<code>moov</code>位于<code>0xA08540</code>文件的尾部，也就是说，针对不指定<code>Byte-Range</code>的请求，只有请求到文件尾的时候才能开始播放视频<br>
查看一个能播放的视频，位置如下图：<br>
<img src="http://image.simapps.cn/2018/02/moov-before-mdat1.png" alt="moov-before-mdat1"><br>
<img src="http://image.simapps.cn/2018/02/moov-before-mdat2.png" alt="moov-before-mdat2"><br>
<code>moov</code>和<code>mdat</code>都位于文件头部，且<code>moov</code>位于<code>mdat</code>之前。<br>
那么是不是用一个请求就可以播放所有的<code>moov</code>位于<code>mdat</code>之前的视频了呢？如果不Seek的话，答案是可以的，但是如果加入<code>Seek</code>的话，情况就复杂多了，所以还是要加入分块下载，才能完美解决边下边播，缓存以及Seek。</p>
<h4 id="">分块下载</h4>
<p>引入分块下载最大的复杂点在于对响应数据的<code>contentOffset</code>的处理上，好在<code>AVAssetResourceLoader</code>帮我们处理了大量工作，我们只需要用好<code>AVAssetResourceLoadingRequest</code>就可以了。</p>
<ul>
<li>首先获取原始请求的<code>Range-Byte</code></li>
<li>构造新的请求</li>
<li>获取响应<code>HTTPUrlResponse</code>
<ul>
<li>填充到<code>loadingRequest.contentInformationRequest</code></li>
</ul>
</li>
<li>获取响应数据
<ul>
<li>获取响应头中的<code>Content-Length</code></li>
<li>计算<code>content-offset</code>,填充响应到原始请求，写入文件</li>
<li>填充到<code>loadingRequest.dataRequest</code></li>
</ul>
</li>
<li>请求完成</li>
</ul>
<p>下面是代码部分，首先是获取原始请求和发送新的请求</p>
<pre><code class="language-swift">func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -&gt; Bool {
    if self.session == nil {
        //构造Session
        let configuration = URLSessionConfiguration.default
        configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
        configuration.networkServiceType = .video
        configuration.allowsCellularAccess = true
        self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
    }
    //构造 保存请求
    var urlRequst = URLRequest.init(url: self.initalUrl!, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 20) // 20s超时
    urlRequst.setValue(&quot;application/octet-stream&quot;, forHTTPHeaderField: &quot;Content-Type&quot;)
    urlRequst.httpMethod = &quot;GET&quot;
    //设置请求头
    guard let wrappedDataRequest = loadingRequest.dataRequest else{
        //本次请求没有数据请求
        return true
    }
    let range:NSRange = NSMakeRange(Int.init(truncatingBitPattern: wrappedDataRequest.requestedOffset), wrappedDataRequest.requestedLength)
    let rangeHeaderStr = &quot;byes=\(range.location)-\(range.location+range.length)&quot;
    urlRequst.setValue(rangeHeaderStr, forHTTPHeaderField: &quot;Range&quot;)
    urlRequst.setValue(self.initalUrl?.host, forHTTPHeaderField: &quot;Referer&quot;)
    guard let task = session?.dataTask(with: urlRequst) else{
        fatalError(&quot;cant create task for url&quot;)
    }
    task.resume()
    self.tasks[task] = loadingRequest
    return true
}
</code></pre>
<p>收到响应请求后，抓包查看响应的请求头，下图是2个响应的请求头：<br>
<img src="http://image.simapps.cn/2018/02/response.png" alt="response"><br>
其中的<code>Content-Length</code>和<code>Content-Range</code>是我们需要处理的内容。</p>
<ul>
<li>Content-Length表示本次请求的数据长度</li>
<li>Content-Range表示本次请求的数据在总媒体文件中的位置，格式是<em>start-end/total</em>，因此就有<em>Content-Length = end - start + 1</em>。</li>
</ul>
<p>接下来是处理响应的部分代码。</p>
<pre><code class="language-swift">func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -&gt; Void) {
    completionHandler(Foundation.URLSession.ResponseDisposition.allow)
    //第一次请求成功会带回来视频总大小 在响应头里Content-Range: bytes 0-1/20955211
    self.queue.async {
        if let urlRsp = response as? HTTPURLResponse{
            let contentRange:String = urlRsp.allHeaderFields[&quot;Content-Range&quot;]
            let lengthStr = contentRange.substring(from: contentRange.index(after: contentRange.index(of: &quot;/&quot;)!))
            let length = Int(lengthStr)
            self.totalLength = length
            //这里需要构造length大小的文件
            VideoCacheManager.share.createFile(name: self.fileCachePath, size: length)
            //填充响应
            let loadingReq = self.tasks[dataTask]
            loadingReq?.contentInformationRequest?.isByteRangeAccessSupported = true
            loadingReq?.contentInformationRequest?.contentType = rsp.allHeaderFields[&quot;Content-Type&quot;]
            loadingReq?.contentInformationRequest?.contentLength = self.totalLength
        }
    }
}
</code></pre>
<p>收到响应数据后</p>
<pre><code class="language-swift">func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    self.queue.async {
        let loadingRequest = self.tasks[dataTask]
        self.didDownLoadMediaDat(dataTask: dataTask, data: data)
    }
}
func didDownLoadMediaDat(dataTask:URLSessionDataTask,data:Data){
    //填充数据 写入文件
    let loadingReq = self.tasks[dataTask]
    loadingReq?.dataRequest?.respond(with: data)
    VideoCacheManager.share.saveFileData(name: self.fileCachePath, data: data, position: loadingReq?.dataRequest?.requestedOffset+1)
    //结束本次请求
    loadingReq?.finishLoading()
    //移除请求
    self.tasks.removeValue(forKey: dataTask)
}
</code></pre>
<p>当然，请求遇到错误和请求取消的回调里也要做相应的处理，只需要从数组里移除相应的请求，然后中断我们发送的<code>UrlRequest</code>即可。剩下的内容<code>AVPlayer</code>会帮我们处理，包括Seek也是这样的流程，当Seek的时候，原始请求的<code>Range-Byte</code>会变，并且会取消旧的原始请求。<br>
以上就是实现分块下载和缓存的基本思路。<a href="https://github.com/search?utf8=%E2%9C%93&amp;q=AVAssetResourceLoaderDelegate&amp;type=">github</a>上搜索也会发现很多优秀成熟的完整代码，自己实现一整套逻辑遇到的坑会比较多，理解了整套机制后，在第三方的基础上修改是个不错的选择。</p>
</div>]]></content:encoded></item><item><title><![CDATA[AVPlayer初体验之视频解纹理]]></title><description><![CDATA[AVPlayer是苹果提供的用来管理多媒体播放的控制器，提供了播放所需要的控制接口和支持KVO的属性，支持播放本地和网络视频，以及实时视频流。本文主要说明了如何从视频源通过AVPlayer解码来获取纹理。]]></description><link>https://xferris.cn/avplayer_video_texture/</link><guid isPermaLink="false">5dba7382a9c4d600015f2362</guid><category><![CDATA[iOS]]></category><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Mon, 26 Feb 2018 07:58:24 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p><code>AVPlayer</code>是苹果提供的用来管理多媒体播放的控制器，提供了播放所需要的控制接口和支持KVO的属性，支持播放本地和网络视频，以及实时视频流。它一次只能播放一个AVPlayerItem，如果需要切换媒体源，需要使用<code>replaceCurrentItem(with:)</code>函数。如果需要播放多个视频，可以考虑使用<code>AVQueuePlayer</code>。在不同性能的设备上，甚至相同设备的不同iOS版本上，AVPlayer的最大支持清晰度都会不一样，例如在iOS10的某些机器上不支持4k播放，但是到iOS11就支持了，关于测定视频是否可以用AVPlayer来解码，可以直接在safari中输入视频网址来测试。</p>
<p>如果只需要播放视频，可以直接使用<code>CALayer</code>的子类<code>AVPlayerLayer</code>。这里不做过多的说明，可以查看苹果的<a href="https://developer.apple.com/library/content/samplecode/AVFoundationSimplePlayer-iOS/Introduction/Intro.html">Demo代码</a>。<br>
这里主要说明从<code>AVPlayerOutput</code>中获取视频纹理的以用于<code>OpenGl</code>的下一步处理。</p>
<h2 id="">进度、播放状态控制</h2>
<h4 id="">播放信息监听</h4>
<p>利用KVO和通知中心监听以下<code>Key</code>即可，虽然KVO的机制不太推荐使用，但是看了官方文档，确实说这么用。<img src="http://image.simapps.cn/2018/02/3.jpg" alt="3"></p>
<pre><code class="language-swift">//已缓存进度
self.playerItem!.addObserver(self, forKeyPath: &quot;loadedTimeRanges&quot;, options: NSKeyValueObservingOptions.new, context: nil)
//状态改变
self.playerItem!.addObserver(self, forKeyPath: &quot;status&quot;, options: NSKeyValueObservingOptions.new, context: nil)
//缓冲
self.playerItem!.addObserver(self, forKeyPath: &quot;playbackBufferEmpty&quot;, options: NSKeyValueObservingOptions.new, context: nil)
//缓冲可播
self.playerItem!.addObserver(self, forKeyPath: &quot;playbackLikelyToKeepUp&quot;, options: NSKeyValueObservingOptions.new, context: nil)
//播放完成
NotificationCenter.default.addObserver(self, selector: #selector(didPlayToEnd(notify:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
</code></pre>
<h4 id="">状态控制</h4>
<p>所有的状态控制都需要在<code>AVPlayerItemStatus</code>变成<code>readyToPlay</code>的时候才可以使用，并且只有这个时候可以取到视频的Size，所以在KVO的回调里</p>
<pre><code class="language-swift">if keyPath == &quot;status&quot;{
    switch (object as! AVPlayerItem).status {
        case .readyToPlay:
            // 只有在这个状态下才能播放
            //准备就绪
            let pixelBuffer:CVPixelBuffer? = self.videoOutPut.copyPixelBuffer(forItemTime:(self.playerItem!.currentTime()), itemTimeForDisplay: nil)
            if(pixelBuffer != nil){
                //获取size
                let width:Int = CVPixelBufferGetWidth(pixelBuffer!)
                let height:Int = CVPixelBufferGetHeight(pixelBuffer!)
                self.playerItem?.videoSize = CGSize.init(width: width, height: height)
            }
            self.notify(state: .prepared)
            if(self.shouldPlayAfterPrepared)
            {
                self.play()
            }
        case .unknown:
                self.notify(state: .unknown)
                //print(&quot;视频加载未知错误&quot;)
        case .failed:
                self.notify(state: .failed,error: self.avPlayer?.error)
                //print(&quot;视频加载错误,\(String(describing: self.avPlayer?.error))&quot;)
            }
}
</code></pre>
<p>如果播放遇到错误可以用<code>self.avPlayer?.error</code>来查看错误类型。</p>
<h2 id="">输出纹理</h2>
<h4 id="yuv">YUV纹理</h4>
<p>由于视频的编码格式基本都是<code>YUV420</code>，可以查看苹果的<a href="https://developer.apple.com/library/content/samplecode/AVBasicVideoOutput/Introduction/Intro.html">Demo代码</a> ,通过<code>AVPlayerItemVideoOutput</code>获取<code>Y-Pannel</code>和<code>UV-Pannel</code>两张纹理，最后在Shader中对两种纹理组合处理。</p>
<p>设置<code>AVPlayerItemVideoOutput</code>的部分代码</p>
<pre><code class="language-objectivec">NSDictionary *pixBuffAttributes = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)};
self.videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:pixBuffAttributes];
</code></pre>
<p>输出纹理的部分代码</p>
<pre><code class="language-objectivec">//Y-Plane
glActiveTexture(GL_TEXTURE0);
err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _videoTextureCache, pixelBuffer, NULL, GL_TEXTURE_2D, GL_RED_EXT, frameWidth, frameHeight, GL_RED_EXT, GL_UNSIGNED_BYTE, 0, &amp;_lumaTexture);
//UV-plane
glActiveTexture(GL_TEXTURE1);
err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _videoTextureCache, pixelBuffer, NULL, GL_TEXTURE_2D, GL_RG_EXT, frameWidth / 2, frameHeight / 2, GL_RG_EXT, GL_UNSIGNED_BYTE, 1, &amp;_chromaTexture);
</code></pre>
<p>其中的<code>kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange</code>是CoreVideo中指定的<a href="https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers?language=objc">Pixel Format Identifiers</a> 类型，在<code>OpenGLES2</code>环境下其对应的参数是<code>GL_RED_EXT</code>和<code>GL_RG_EXT</code>。视频支持的PixelFormat格式如下<br>
<img src="http://image.simapps.cn/2018/02/1.jpg" alt="1"></p>
<p>获取纹理之后，还要使用Shader混合两张纹理，片元着色器(.fsh)代码如下</p>
<pre><code class="language-c">void main()
{
	mediump vec3 yuv;
	lowp vec3 rgb;
	
	// Subtract constants to map the video range start at 0
	yuv.x = (texture2D(SamplerY, texCoordVarying).r - (16.0/255.0))* lumaThreshold;
	yuv.yz = (texture2D(SamplerUV, texCoordVarying).rg - vec2(0.5, 0.5))* chromaThreshold;
	
	rgb = colorConversionMatrix * yuv;

	gl_FragColor = vec4(rgb,1);
}
</code></pre>
<h4 id="rgb">RGB纹理</h4>
<p>首先要明白一点，上图中明确说明，<code>BGRA</code>的输出格式是<code>420v</code>的两倍多带宽(<em>More than 2x bandwidth</em>)，并且在该图来源,WWDC的<a href="https://developer.apple.com/videos/play/wwdc2011/419/">这个视频</a>的<code>27:00</code>位置明确说明420v的输出格式效率会明显高于BGRA的输出格式(<em>It does come across if you can avoid using BGRA and doing your work in YUV, it's more efficient from bandwidth standpoint</em>),但是反过来，对于OpenGL来说，两张纹理的性能又会低于一张纹理。而且直接使用使用<code>BGRA</code>毕竟会方便很多，因为输出的直接就是一张纹理，个人认为在iOS5时代可能需要考虑420和BGRA的输出效率，但是现在毕竟都iOS11时代了，所以影响可以忽略不计。</p>
<p>设置<code>AVPlayerItemVideoOutput</code>的代码</p>
<pre><code class="language-objectivec">NSDictionary *pixBuffAttributes = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)};
self.videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:pixBuffAttributes];
</code></pre>
<p>输出纹理的代码</p>
<pre><code class="language-objectivec">CVReturn textureRet = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.videoTextureCache, pixelBuffer, nil, GL_TEXTURE_2D, GL_RGBA, width, height, GL_BGRA, GL_UNSIGNED_BYTE, 0, &amp;_textureOutput);
</code></pre>
<p><code>BGRA</code>对应的输出格式是<code>kCVPixelFormatType_32BGRA</code>,其对应的从Buffer读纹理的参数是<code>GL_RGBA</code>和<code>GL_BGRA</code>。</p>
<p>完整的从<code>VideoOutput</code>中获取纹理的代码如下</p>
<pre><code class="language-objectivec">-(CVOpenGLESTextureRef)getVideoTextureWithOpenGlContext:(EAGLContext *)context{
    if(self.videoOutput == nil){
        NSLog(@&quot;ferrisxie: 输出对象为空&quot;);
        return nil;
    }
    //step1:构造缓存
    if(self.videoTextureCache == nil){
        CVReturn ret = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, nil, context, nil, &amp;_videoTextureCache);
        if(ret != 0){
            NSLog(@&quot;构造缓存失败&quot;);
            return nil;
        }
    }
    //step2: 取纹理
    CMTime currentTime = self.currentItem.currentTime;
    if(![self.videoOutput hasNewPixelBufferForItemTime:currentTime]){
        //没有新的纹理 返回上一帧
        return self.textureOutput;
    }
    CVPixelBufferRef pixelBuffer = [self.videoOutput copyPixelBufferForItemTime:currentTime itemTimeForDisplay:nil];
    CGFloat width = CVPixelBufferGetWidth(pixelBuffer);
    CGFloat height = CVPixelBufferGetHeight(pixelBuffer);
    if(CGSizeEqualToSize(CGSizeZero, self.videoSize)){
        self.videoSize = CGSizeMake(width, height);
    }
    CVOpenGLESTextureCacheFlush(self.videoTextureCache, 0);
    if(self.textureOutput != nil){
        //释放上一帧
        CFRelease(self.textureOutput);
        self.textureOutput = nil;
    }
    CVReturn textureRet = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.videoTextureCache, pixelBuffer, nil, GL_TEXTURE_2D, GL_RGBA, width, height, GL_BGRA, GL_UNSIGNED_BYTE, 0, &amp;_textureOutput);
    if(textureRet != 0){
        NSLog(@&quot;解析纹理失败%u,%@&quot;,textureRet,self.textureOutput);
        if(self.textureOutput != nil){
            //解析纹理失败不需要Release
            //CFRelease(self.textureOutput);
            self.textureOutput = nil;
        }
        return nil;
    }
    if(pixelBuffer != nil){
        CVPixelBufferRelease(pixelBuffer);
    }
    
    return self.textureOutput;
}
//usage
CVOpenGLESTextureRef texureRef = [self.player getVideoTextureWithOpenGlContext:[EAGLContext currentContext]];
GLuint target = CVOpenGLESTextureGetTarget(texureRef);
GLuint name = CVOpenGLESTextureGetName(texureRef);
    //用完记得释放
CFRelease(texureRef);
</code></pre>
<p>Swift由于取消了<code>CFRelease</code>等CoreFoundation的内存管理接口，在取纹理的时候需要使用<code>Unmanaged</code>对象,利用<code>takeUnretainedValue</code>，可以不需要释放代码了。</p>
<pre><code class="language-swift">if let videoPlayer = self.videoPlayer{
    if let unmangaed:Unmanaged&lt;CVOpenGLESTexture&gt; = videoPlayer.getVideoTexture(withOpen: self.context){
        let testure:CVOpenGLESTexture = unmangaed.takeUnretainedValue()
        let target:GLuint = CVOpenGLESTextureGetTarget(testure)
        let name:GLuint = CVOpenGLESTextureGetName(testure)
    }
}
//不再需要释放了
</code></pre>
<h2 id="">其他</h2>
<h4 id="">切换播放源</h4>
<p>针对需要切换播放源的场景，重新构造播放器显然是最简单易行的，但是测试发现，频繁的构造和销毁<code>AVPlayer</code>对象虽然不会导致内存增加，但是很奇怪的是，会导致<code>OtherProccesses</code>的内存增大，从而导致<code>Free</code>内存减小，减小到某个值的时候，就会触发<code>didReceiveMemeoryWarning</code>内存警告，<strong>暂时还没有发现原因</strong>，因此这种方法不可取。<br>
<img src="http://image.simapps.cn/2018/02/4.jpg" alt="4"></p>
<p>其实<code>AVPlayer</code>本身提供了切换播放源的函数。</p>
<pre><code class="language-swift">func replaceCurrentItem(with item: AVPlayerItem?)
</code></pre>
<p>当要切换播放源时，需要指定新的<code>AVPlayerItem</code>,这时候又会面临状态问题，之前说过只有在<code>AVPlayerItemStatus</code>变成<code>readyToPlay</code>的时候才可以调用<code>play</code>和<code>seek</code>等函数，可以使用<code>AVUrlAsset</code>来预加载这个Item:</p>
<pre><code class="language-swift">func loadValuesAsynchronously(forKeys keys: [String], completionHandler handler: (() -&gt; Void)? = nil)
</code></pre>
<p>通过预加载<code>duration</code>(视频总进度)来判断视频是否可播放，当加载完成后再<code>replaceCurrentItem</code></p>
<pre><code class="language-swift">// Load the asset's &quot;playable&quot; key
asset.loadValuesAsynchronously(forKeys: [&quot;duration&quot;]) {
    var error: NSError? = nil
    let status = asset.statusOfValue(forKey: &quot;duration&quot;, error: &amp;error)
    switch status {
    case .loaded:
    // Sucessfully loaded, continue processing
    //在这里替换播放源，并且直接开始播放
    let playerItem = AVPlayerItem.init(asset: asset)
    self.videoPlayer?.replaceCurrentItem(with: playerItem)
    self.resumePlay()
    case .failed:
    // Examine NSError pointer to determine failure
    case .cancelled:
    // Loading cancelled
    default:
        // Handle all other cases
    }
}
</code></pre>
<p>如果实在需要控制多个播放源，可以考虑使用<code>AVQueuePlayer</code>来处理。</p>
<h4 id="">声音优先级</h4>
<p>默认的声音优先级为视频播放的默认优先级<code>AVAudioSessionCategoryAmbient</code>，静音状态不会有声音，退出后台就停止播放。<a href="https://developer.apple.com/library/content/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionCategoriesandModes/AudioSessionCategoriesandModes.html">AudioSessionCategoriesandModes</a>有关于声音优先级的介绍。<br>
使用如下函数切换</p>
<pre><code class="language-swift">AVAudioSession.sharedInstance().setCategory(_ category: String)
</code></pre>
<p>一般的，如果需要静音状态下也有声音可以直接使用<code>AVAudioSessionCategoryPlayback</code>这个Value。</p>
<h4 id="">硬件加速</h4>
<p>iOS6以后可以使用底层框架<code>VideoToolbox</code>来实现硬解码，具体<a href="https://objccn.io/issue-23-3/">视频工具箱和硬件加速</a>有很清楚的解释，基本的场景，使用AVPlayer即可满足需求。</p>
</div>]]></content:encoded></item><item><title><![CDATA[Swift3中的Array内存地址和关联对象的问题]]></title><description><![CDATA[Swift中的Swift类型，结构体是不可以用OC关联对象的，如果继承自OC对象的话，可以直接使用，另外，基本Swift类型的内存地址也很奇妙。]]></description><link>https://xferris.cn/swift_associated_object/</link><guid isPermaLink="false">5dba7382a9c4d600015f235f</guid><category><![CDATA[iOS]]></category><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Mon, 29 Jan 2018 02:12:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><h2 id="oc">直接用OC的关联对象</h2>
<h3 id="">空数组</h3>
<pre><code class="language-swift">//
//  ViewController.swift
//  SwiftRunner
//
//  Created by Ferris on 2018/1/27.
//  Copyright © 2018年 Ferris. All rights reserved.
//

import UIKit

var objc_associate_ket_array:UInt8 = 0
var objc_asssciate_key_object:UInt8 = 1
extension Array{
    var fg_identify:String{
        set{
            objc_setAssociatedObject(self, &amp;objc_associate_ket_array, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
        get{
            if let rs = objc_getAssociatedObject(self, &amp;objc_associate_ket_array) as! String?{
                return rs
            }else{
                return &quot;没有关联对象&quot;
            }
        }
    }
}
extension NSObject{
    var fg_tag:String{
        set{
            objc_setAssociatedObject(self, &amp;objc_asssciate_key_object, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
        get{
            return objc_getAssociatedObject(self, &amp;objc_asssciate_key_object) as! String
        }
    }
}
class ViewController: UIViewController {

    let object_a = NSObject()
    let object_b = NSObject()
    let object_c = NSObject()
    
    var array_a:[NSObject] = []
    var array_b:[NSObject] = []
    var array_c:[NSObject] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        object_a.fg_tag = &quot;a&quot;
        object_b.fg_tag = &quot;b&quot;
        object_c.fg_tag = &quot;c&quot;
        
        array_a.fg_identify = &quot;a&quot;
        array_b.fg_identify = &quot;b&quot;
        array_c.fg_identify = &quot;c&quot;
        // Do any additional setup after loading the view, typically from a nib.
    }
    override func viewDidAppear(_ animated: Bool){
        super.viewDidAppear(animated)
        print(&quot;object_a = \(object_a.fg_tag)&quot;)
        print(&quot;object_b = \(object_b.fg_tag)&quot;)
        print(&quot;object_c = \(object_c.fg_tag)&quot;)
        print(&quot;array_a = \(array_a.fg_identify)&quot;)
        print(&quot;array_b = \(array_b.fg_identify)&quot;)
        print(&quot;array_c = \(array_c.fg_identify)&quot;)
    }
}
</code></pre>
<p>输出结果如下</p>
<pre><code class="language-swift">object_a = a
object_b = b
object_c = c
array_a = c
array_b = c
array_c = c
</code></pre>
<p>也就是说三个数组全都指向同一个关联对象，为了证实三个数组的内存地址是否一致，直接打印地址<br>
修改get函数</p>
<pre><code class="language-swift">        get{
                        print(&quot;\(UnsafeRawPointer(self))&quot;)
            if let rs = objc_getAssociatedObject(self, &amp;objc_associate_ket_array) as! String?{
                return rs
            }else{
                return &quot;没有关联对象&quot;
            }
        }
</code></pre>
<p>得到输出</p>
<pre><code class="language-swift">0x02504cb0
array_a = c
0x02504cb0
array_b = c
0x02504cb0
array_c = c
</code></pre>
<p>居然真的一样！！</p>
<h3 id="">非空数组</h3>
<h4 id="oc">内含OC对象</h4>
<p>给数组加上<code>object_a</code>对象</p>
<pre><code class="language-swift">        array_a.append(object_a)
        array_b.append(object_b)
        array_c.append(object_c)
</code></pre>
<p>得到的结果</p>
<pre><code class="language-swift">object_a = a
object_b = b
object_c = c
0x7af37274
array_a = a
0x7c241854
array_b = b
0x7c241884
array_c = c
</code></pre>
<p>完全正常，和预想的一致</p>
<h4 id="swift">内含Swift对象</h4>
<p>将数组改成</p>
<pre><code class="language-swift">    var array_a:[Any] = []
    var array_b:[Any] = []
    var array_c:[Any] = []
</code></pre>
<p>其余代码不变<br>
输出结果变为</p>
<pre><code class="language-swift">0x7a968424
array_a = 没有关联对象
0x7a874964
array_b = 没有关联对象
0x7a874994
array_c = 没有关联对象
</code></pre>
<p>关联对象失效了！<br>
将Any换为String等Swift对象类型，依旧一样</p>
<h3 id="">查看内存地址</h3>
<pre><code class="language-swift">    var fg_address:String{
        get{
            return &quot;\(UnsafeRawPointer(self))&quot;
        }
    }
</code></pre>
<p>修改代码如下</p>
<pre><code class="language-swift">   let object_a = NSObject()
    let object_b = NSObject()
    let object_c = NSObject()
    
    var array_a:[Any] = []
    var array_b:[Any] = []
    var array_c:[Any] = []
    
    var mix_array:[[Any]] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        object_a.fg_tag = &quot;a&quot;
        object_b.fg_tag = &quot;b&quot;
        object_c.fg_tag = &quot;c&quot;
        
        
        array_a.append(object_a)
        array_b.append(object_b)
        array_c.append(object_c)
        
        array_a.fg_identify = &quot;a&quot;
        array_b.fg_identify = &quot;b&quot;
        array_c.fg_identify = &quot;c&quot;
        
        mix_array.append(array_a)
        mix_array.append(array_b)
        mix_array.append(array_c)
        
        mix_array.fg_identify = &quot;mix&quot;
        // Do any additional setup after loading the view, typically from a nib.
    }
    override func viewDidAppear(_ animated: Bool){
        super.viewDidAppear(animated)
        print(&quot;object_a = \(object_a.fg_tag)&quot;)
        print(&quot;object_b = \(object_b.fg_tag)&quot;)
        print(&quot;object_c = \(object_c.fg_tag)&quot;)
        print(&quot;array_a = \(array_a.fg_identify) + \(array_a.fg_address)&quot;)
        print(&quot;array_b = \(array_b.fg_identify) + \(array_b.fg_address)&quot;)
        print(&quot;array_c = \(array_c.fg_identify) + \(array_c.fg_address)&quot;)
        
        print(&quot;mix_array0 = \(mix_array[0].fg_identify) + \(mix_array[0].fg_address)&quot;)
        print(&quot;mix_array1 = \(mix_array[1].fg_identify) + \(mix_array[1].fg_address)&quot;)
        print(&quot;mix_array2 = \(mix_array[2].fg_identify) + \(mix_array[2].fg_address)&quot;)
    }

</code></pre>
<p>输出</p>
<pre><code class="language-swift">array_a = 没有关联对象 + 0x7bf86cc4
array_b = 没有关联对象 + 0x7bf86a64
array_c = 没有关联对象 + 0x7bf86a94
mix_array0 = 没有关联对象 + 0x7bf86cc4
mix_array1 = 没有关联对象 + 0x7bf86a64
mix_array2 = 没有关联对象 + 0x7bf86a94
</code></pre>
<p><strong>当数组被放进另一个数组时，会发现内存地址是一样的。<br>
如果把<code>array_a</code>的类型改成<code>[NSObject]</code>呢，神奇的事情出现了</strong></p>
<pre><code class="language-swift"> var array_a:[NSObject] = []
</code></pre>
<p>输出</p>
<pre><code class="language-swift">array_a = a + 0x0000610000053e80
array_b = 没有关联对象 + 0x0000610000260da0
array_c = 没有关联对象 + 0x0000610000260de0
mix_array0 = 没有关联对象 + 0x00006080002664a0
mix_array1 = 没有关联对象 + 0x0000610000260da0
mix_array2 = 没有关联对象 + 0x0000610000260de0
</code></pre>
<p>当a被放进另外一个数组的时候，内存地址变了！<strong>并且a本身也能拿到关联对象</strong></p>
<pre><code class="language-swift">    var array_a:[NSObject] = []
    var array_b:[NSObject] = []
    var array_c:[NSObject] = []
</code></pre>
<p>输出</p>
<pre><code class="language-swift">object_a = a
object_b = b
object_c = c
array_a = a + 0x00006180000496e0
array_b = b + 0x0000618000049260
array_c = c + 0x0000618000048540
mix_array0 = 没有关联对象 + 0x000061800026c320
mix_array1 = 没有关联对象 + 0x000061800026c460
mix_array2 = 没有关联对象 + 0x000061800026c4a0
</code></pre>
<p>输出！</p>
<pre><code class="language-swift">object_a = a
object_b = b
object_c = c
array_a = a + 0x000061000005a8d0
array_b = b + 0x000061000005ae10
array_c = c + 0x000061000005ae40
mix_array0 = a + 0x000061000005a8d0
mix_array1 = b + 0x000061000005ae10
mix_array2 = c + 0x000061000005ae40
</code></pre>
<h4 id="">如果给空数组设置关联对象呢？</h4>
<p>测试代码：改变一下位置</p>
<pre><code class="language-swift">        array_a.fg_identify = &quot;a&quot;
        array_b.fg_identify = &quot;b&quot;
        array_c.fg_identify = &quot;c&quot;
        
        array_a.append(object_a)
        array_b.append(object_b)
        array_c.append(object_c)

</code></pre>
<p>输出</p>
<pre><code class="language-swift">object_a = a
object_b = b
object_c = c
array_a = 没有关联对象 + 0x0000618000244610
array_b = 没有关联对象 + 0x00006180002441f0
array_c = 没有关联对象 + 0x00006180002444f0
mix_array0 = 没有关联对象 + 0x0000618000244610
mix_array1 = 没有关联对象 + 0x00006180002441f0
mix_array2 = 没有关联对象 + 0x00006180002444f0
</code></pre>
</div>]]></content:encoded></item><item><title><![CDATA[Unity5.6与Xcode8.3原生工程整合交互]]></title><description><![CDATA[基于Unity5.6.0f3和Xcode8.3.2的工程混合方案]]></description><link>https://xferris.cn/unity_with_xcode/</link><guid isPermaLink="false">5dba7382a9c4d600015f235c</guid><category><![CDATA[iOS]]></category><category><![CDATA[Unity]]></category><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Thu, 11 May 2017 03:03:19 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><h2 id="">环境</h2>
<ul>
<li>Unity5.6.0f3个人免费版。</li>
<li>Xcode8.3.2。</li>
</ul>
<h2 id="">参考</h2>
<ul>
<li><a href="https://the-nerd.be/2015/08/20/a-better-way-to-integrate-unity3d-within-a-native-ios-application/">the_nerd.be</a>上的这篇文章，还带视频。</li>
<li>Unity官方参考文档的<a href="https://docs.unity3d.com/Manual/iphone.html">iOS部分</a>，这里有很多资料，包括Unity导出Xcode工程的目录结构以及在Unity和iOS交互问题等。</li>
</ul>
<h2 id="">需求</h2>
<ul>
<li>Unity需求较多，Native需求较少：直接在Unity导出的Xcode工程中开发。</li>
<li>Unity需求较多，Native需求只有一两个页面：可以直接将写好的OC代码文件放到Unity的<code>Assets/Plugins/</code>文件夹里。</li>
<li>Unity需求较少，Native需求较多：需要将Unity导出的Xcode工程整合入原生的Xcode工程，也是本文接下来的内容。</li>
</ul>
<h2 id="">实战</h2>
<h3 id="unity">导出Unity工程</h3>
<p><code>File</code>-&gt;<code>Build &amp; Run</code><br>
<img src="http://image.simapps.cn/image/7/8a/271e805b227d113206160da2291bf.jpg" alt=""><br>
在这里添加场景，然后选择<code>Player Settings</code>进入设置。主要设置以下三项，其他按需求来。<br>
<img src="http://image.simapps.cn/image/e/35/efdf2f37b32c8caf71b2acc2a63dc.jpg" alt=""><br>
导出后的位置如下图，我把两个工程放在同一个根目录下，这样对后期比较方便。<br>
<img src="http://image.simapps.cn/image/d/d6/7d2811985932beaaee811a3dd48f6.png" alt=""><br>
<img src="http://image.simapps.cn/image/3/85/2303c5134c1517f93d58d2f870c24.jpg" alt=""></p>
<h3 id="native">配置Native工程</h3>
<h4 id="">复制文件</h4>
<p>这一步最复杂，不过可以参考上面的视频教程，有些地方可能由于Unity和Xcode的版本需要变动一下。<br>
首先拖入Unity工程的<code>Classes</code>和<code>Libraries</code>，为了确保以后维护起来方便，请不要勾选<code>copy</code>,如下图。<br>
<img src="http://image.simapps.cn/image/2/0d/7a817da1b279badb0d07b915e7b4d.png" alt=""><br>
<img src="http://image.simapps.cn/image/3/89/29d54f3b2e1b26ffc17dbc96d0edb.png" alt=""><br>
这样做的好处是，只保留文件的引用而不复制文件，减少依赖关系。<br>
接下来修改一些文件：</p>
<ul>
<li>
<p>把<code>Classes/main.mm</code>文件的内容全部复制到你的<code>main.m</code>文件里，并且把<code>main.m</code>改名为<code>main.mm</code>，然后把里面的<code>UnityAppController</code>改成你的<code>AppDelegate</code>。<br>
<img src="http://image.simapps.cn/image/c/d1/bf7fa3a8ee9d93527235de5622218.jpg" alt=""></p>
</li>
<li>
<p>新建<code>PrefixHeader</code>，把Classes/Prefix.pch文件的内容全部复制到新建的<code>refixHeader</code>里。<br>
<img src="http://image.simapps.cn/image/1/b4/a647d2c86604a065a09d44924c30e.jpg" alt=""><br>
接下来删除一些没用的文件，这里的所有删除都只是删除引用。<br>
<img src="http://image.simapps.cn/image/d/25/e871a6e432c8f65fc151d7d6c4158.png" alt=""><br>
要删除的内容如下：</p>
</li>
<li>
<p>Classes/main.mm。</p>
</li>
<li>
<p>Classes/Prefix.pch。</p>
</li>
<li>
<p>Classes/Native下的所有.h文件，可以在下方的Filter过滤器里输入<code>.h</code>来过滤。</p>
</li>
<li>
<p>Libraries/libil2cpp文件夹</p>
</li>
</ul>
<h4 id="buildsetting">Build Setting</h4>
<ul>
<li>Build Setting里的<code>Linking</code>,<code>Apple LLVM</code>都按照Unity导出的工程来设置。</li>
<li>添加<code>User-Defined</code>字段，也和Unity导出的工程一致。（在最上面有个+号）<br>
<img src="http://image.simapps.cn/image/3/12/e820d2850fe487d2ee7a146917e4f.jpg" alt=""></li>
<li><code>Prefix Header</code>如下设置。<br>
<img src="http://image.simapps.cn/image/9/c4/0b7dc0632946b51513b6b8473bdc2.jpg" alt=""><br>
如果有自己的头文件需要包含，需要放在如下位置：</li>
</ul>
<pre><code class="language-C">#ifdef __OBJC__
    #import &lt;Foundation/Foundation.h&gt;
    #import &lt;UIKit/UIKit.h&gt;
#endif
</code></pre>
<ul>
<li><code>Header Search Paths</code>添加到Unity工程的引用。<br>
<img src="http://image.simapps.cn/image/1/1a/89fe3aa0630e71b35be59888fdaf3.jpg" alt=""></li>
<li><code>Library Search Paths</code>只需要添加一行<code>${SRCROOT}/../Unity2iOS/Libraries</code>指向Unity工程的Libraries目录。</li>
</ul>
<h4 id="buildphase">Build Phase</h4>
<ul>
<li>添加2行<code>Run Script</code></li>
</ul>
<pre><code>rm -rf &quot;$TARGET_BUILD_DIR/$PRODUCT_NAME.app/DATA&quot;
cp -Rf &quot;$PROJECT_DIR/../Unity2iOS/Data&quot; &quot;$TARGET_BUILD_DIR/$PRODUCT_NAME.app/Data&quot;
</code></pre>
<p>注意修改其中的目录到自己的Unity工程。</p>
<ul>
<li>Link Binary With Libraries按Unity工程一个个添加，其中<code>Libiconv.2.dylib</code>，在Xcode8中已经找不到，从<code>/usr/lib</code>中找到然后拖进去，注意optional的设置。</li>
<li>如果你是参考视频教程的话，视频中还要添加<code>-fn-objc-arc</code>,这一步千万不要添加，不然Build成功以后运行也会失败。可能是由于Unity版本导致的。</li>
</ul>
<h4 id="build">开始Build</h4>
<p>到现在为止如果配置完全正确的话。是这个Build成功的，注意如果Unity导出的时候选择DeviceSDK的话，只能在真机上Build，选择模拟器就只能在模拟器上Build。</p>
<h4 id="">可能遇到的错误</h4>
<p>有很多啦，比较Dirty的一个就是<code>libiPhone-lib.a not found</code>，在Build Phase里的<code>Link Binary With Libraries</code>把<code>libiPhone-lib.a</code>移除再添加，然后clean一下就好了，视频里有说。<br>
如果还有其他问题也都可以一个个解决，千万不要放弃。</p>
<h4 id="unity">运行Unity界面</h4>
<p>Unity界面存在于<code>UnityAppController.window</code>里，因此只需要控制<code>AppDelegate.window</code>和<code>UnityAppController.window</code>的显示顺序就行，UIWindow本质就是UIView，因此可以直接使用<code>hide</code>等方法，只是要加上<code>[window makeKeyAndVisable]</code>方法。<br>
另外要在App的生命周期方法里调用UnityAppController对应的周期方法。</p>
<pre><code class="language-objectivec">- (void)applicationWillResignActive:(UIApplication *)application {
    [self.unityController applicationWillResignActive:application];
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}


- (void)applicationDidEnterBackground:(UIApplication *)application {
    [self.unityController applicationDidEnterBackground:application];
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}


- (void)applicationWillEnterForeground:(UIApplication *)application {
    [self.unityController applicationWillEnterForeground:application];
    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}


- (void)applicationDidBecomeActive:(UIApplication *)application {
    [self.unityController applicationDidBecomeActive:application];
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}


- (void)applicationWillTerminate:(UIApplication *)application {
    [self.unityController applicationWillTerminate:application];
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
</code></pre>
<p>别忘了在<code>DidFinishLaunch</code>里给<code>self.unityController</code>赋值。<br>
运行会发现有运行期错误，<code>UnityAppController</code>的<code>GetAppController()</code>方法得到了nil，修改如下。</p>
<pre><code class="language-objectivec">inline UnityAppController*  GetAppController()
{
    return [(AppDelegate*)[UIApplication sharedApplication].delegate unityController];
}
</code></pre>
<p>显示Unity和隐藏Unity界面最简单的方法。</p>
<pre><code class="language-objectivec">//显示
        ApplicationDelegate.unityWindow.hidden = NO;
       [ApplicationDelegate.unityWindow makeKeyAndVisible];
//隐藏
        [ApplicationDelegate.window makeKeyAndVisible];
        ApplicationDelegate.unityWindow.hidden = YES;
</code></pre>
<h2 id="unitynative">Unity和Native交互</h2>
<h3 id="unityios">Unity调用iOS方法</h3>
<ul>
<li>C#中</li>
</ul>
<pre><code class="language-C#">[DllImport (&quot;__Internal&quot;)]
private static extern void sim_showSelectTitleDialog();
</code></pre>
<ul>
<li>iOS中，文件名：<code>UnityFunctionManager.mm</code>，注意是.mm，该文件需要放到unity的Plugins目录下，这样打包时会被自动打包到Xcode工程里。</li>
</ul>
<pre><code class="language-objectivec">extern &quot;C&quot; void sim_showSelectTitleDialog(char* title,char* msg){
    SIMUnityDialogManager* dialogManager = [SIMUnityDialogManager shareManager];
    [dialogManager vrb_showSelectTitleDialogWithTitle:title Message:msg];
    
}
</code></pre>
<p>这里建议在Native工程里实现一个单例<code>SIMUnityDialogManager</code>来实现该文件中的方法，这样就实现了具体的代码和接口分离，<code>UnityFunctionManager.mm</code>这个文件可以由Unity的同学负责，iOS同学只需要负责<code>SIMUnityDialogManager</code>里具体的方法实现。</p>
<h5 id="iosunity">iOS调Unity方法</h5>
<ul>
<li>iOS里，任意文件都可以</li>
</ul>
<pre><code class="language-objectivec">UnitySendMessage(&quot;GameObject&quot;, &quot;Function&quot;,[sendMsg UTF8String]);  
</code></pre>
<p>第一个参数是GameObject，第二个参数是方法名，第三个参数是传输的数据。</p>
<ul>
<li>Unity里</li>
</ul>
<pre><code class="language-C#">void Function(string message)  
{ 
//挂载在相应GameObject上的脚本
}
</code></pre>
<h2 id="">代码更新方案</h2>
<p>由于Unity代码里需要更新维护，这样每次重新合并工程就很繁琐，并且不易做CI。<br>
但是如果是通过以上教程实现工程合并，就会发现Unity工程和Native工程实际上<mark>并没有文件的关联，只存在文件的引用关系</mark>。每次Unity更新直接重新打包覆盖原来的工程就可以了。但是存在以下问题。</p>
<ul>
<li>C#文件的增删<br>
文件增删会导致导出的<code>Classes</code>文件夹中的文件的增删，因此在做CI的时候，可以考虑每次Unity工程更新都重新添加引用，但是要记得删除<code>Classes/Native</code>里的头文件。</li>
<li>UnityAppController被覆盖<br>
每一次导出会重新生成UnityAppController文件，但是这个文件我们改了其中的<code>GetAppController()</code>方法，虽然只改了一行，但是每次这个文件都被覆盖。这里我的做法是，把该文件复制到Native工程目录，然后删除Classes里面该文件，这样每次重新打包Unity工程的时候只要多执行一行<code>rm /Classes/UnityAppController</code>就可以了。</li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[利用Electron把Web项目打包成桌面应用]]></title><description><![CDATA[利用Electron把Web项目打包成跨平台的桌面应用，包含MacOS，Windows的过程详解。]]></description><link>https://xferris.cn/electron_package/</link><guid isPermaLink="false">5dba7382a9c4d600015f235a</guid><category><![CDATA[小记]]></category><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Fri, 24 Feb 2017 14:46:10 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><h3 id="">参考文档</h3>
<ul>
<li><a href="http://electron.atom.io/docs/">Electron官方文档</a>。</li>
<li>使用的Demo:<a href="https://github.com/electron/electron-quick-start">electron-quick-start</a></li>
<li>打包工具:<a href="https://github.com/electron-userland/electron-packager">electron-packager</a></li>
</ul>
<h3 id="">安装</h3>
<p>1.Electron是基于<a href="http://nodejs.cn">Node.js</a>开发的，第一步当然要安装node盒npm了，就不多说了。</p>
<p>2.安装Electron,推荐使用全局安装，直接安装</p>
<pre><code class="language-bash">sudo npm install -g electron-prebuilt
</code></pre>
<p>如果卡在<code>install.js</code>了，执行以下替换个npm源，参考了<a href="https://segmentfault.com/q/1010000007594059">这里</a></p>
<pre><code>electron_mirror=&quot;https://npm.taobao.org/mirrors/electron/&quot;
</code></pre>
<p>3.下载demo工程，然后运行。</p>
<pre><code>git clone https://github.com/electron/electron-quick-start
cd electron-quick-start
electron . //运行项目
</code></pre>
<p>4.Electron的基本语法和目录层级结构，官网的<a href="http://electron.atom.io/docs/tutorial/quick-start/">快速开始</a>已经说的很明白了，也比较简单，就不复述了。<br>
5.开始打包，官网的<a href="http://electron.atom.io/docs/tutorial/application-packaging/">打包文档</a>，只说了把源文件隐藏，不暴露给用户，就是打包成<code>asar Archives</code>，但我们想打包成<code>.exe</code>和<code>.app</code>。官网的<a href="http://electron.atom.io/docs/tutorial/application-distribution/">分发(distribute)文档</a>介绍了两种打包工具。</p>
<ul>
<li>electron-builder</li>
<li>electron-packager</li>
</ul>
<p>第一个工具是建立安装程序，打包成<code>.exe</code>和<code>.app</code>的话，我们选择第二个。<br>
6.参考了网上的很多教程，其实也就几个版本，说的都一样，我都没搞定，还是自己动手，丰衣足食。仔细看看，<a href="https://github.com/electron-userland/electron-packager">项目仓库</a>的<code>README.md</code>说的很清楚，有几点需要注意。</p>
<ul>
<li>在非win32平台上要打包exe程序，需要<code>Wine 1.6 or later</code>。</li>
<li>基本用法</li>
</ul>
<pre><code class="language-bash">electron-packager &lt;sourcedir&gt; &lt;appname&gt; --platform=&lt;platform&gt; --arch=&lt;arch&gt; [optional flags...]
</code></pre>
<p>简单的使用</p>
<pre><code class="language-bash">cd electron-quick-start //项目目录
electron-packager ./ Hello -all //-all 其实就是  --platform=all --arch=all （在usage.txt里有解释）
</code></pre>
<p>可能会重新下载Electron安装包，几十M，等了十几分钟，速度还是几K，看看当前的Electron版本，强制使用当前的版本。</p>
<pre><code>electron -v  //输出v1.4.13
electron-packager ./ oral -all --electron-version=1.4.13
</code></pre>
<p>会发现目录里多了个目录，打开里面有个<code>.app</code>在mac里可以直接运行了。</p>
<p><em>我的博客即将搬运同步至腾讯云+社区，邀请大家一同入驻：<a href="https://cloud.tencent.com/developer/support-plan?invitecode=1eld822cduraz">https://cloud.tencent.com/developer/support-plan?invitecode=1eld822cduraz</a></em></p>
</div>]]></content:encoded></item><item><title><![CDATA[CentOS的SVN服务器搭建和自动部署]]></title><description><![CDATA[服务器搭建 安装服务 yum install subversion 配置服务 mkdir -p /data/wwwsvn/myrepo #创建svn仓库的目录 这里可以自定义创建的目录，注意不是网站的文件目录。 svnadmin create /data/wwwsvn/myrepo #与上面的目录相同。 这里要注意该目录不能是空目录。 成功以后会得到以下文件 # ls conf db format hooks locks README.txt 进入conf修改配置文件 vi pass...]]></description><link>https://xferris.cn/centos_svn_env/</link><guid isPermaLink="false">5dba7382a9c4d600015f2358</guid><category><![CDATA[服务器]]></category><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Thu, 05 Jan 2017 16:24:20 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><blockquote>
<p>花了一个晚上时间折腾svn，网上的教程太乱太杂，还有很多是错误的，终于搞定了，把过程记录下来~</p>
</blockquote>
<h2 id="">服务器搭建</h2>
<h4 id="">安装服务</h4>
<pre><code class="language-bash">yum install subversion
</code></pre>
<h4 id="">配置服务</h4>
<pre><code class="language-bash">mkdir -p /data/wwwsvn/myrepo #创建svn仓库的目录
</code></pre>
<p>这里可以自定义创建的目录，注意<strong>不是网站的文件目录</strong>。</p>
<pre><code class="language-bash">svnadmin create /data/wwwsvn/myrepo #与上面的目录相同。
</code></pre>
<p>这里要注意该目录<strong>不能是空目录</strong>。<br>
成功以后会得到以下文件</p>
<pre><code class="language-bash"># ls
conf  db  format  hooks  locks  README.txt
</code></pre>
<p>进入conf修改配置文件</p>
<ul>
<li><code>vi passwd</code>添加在末尾</li>
</ul>
<pre><code class="language-bash">[users]
# harry = harryssecret
# sally = sallyssecret
youname = yourpassword #你的用户和密码
</code></pre>
<ul>
<li><code>vi authz</code>添加在末尾</li>
</ul>
<pre><code class="language-bash">...
[/]
yourname = rw
</code></pre>
<ul>
<li><code>vi svnserve.conf</code>关闭注释以及修改变量</li>
</ul>
<pre><code class="language-bash">   anon-access = read #匿名用户可读
   auth-access = write #授权用户可写
   password-db = passwd #使用哪个文件作为账号文件
   authz-db = authz #使用哪个文件作为权限文件
   realm = /data/wwwsvn/myrepo # 认证空间名，版本库所在目录，和之前的一样
</code></pre>
<h4 id="">开启和关闭服务</h4>
<pre><code class="language-bash"> svnserve -d -r /data/wwwroot/myrepo #开启
 killall svnserve  #关闭
 ps aux | grep svnserve #查看是否运行
</code></pre>
<h4 id="">打开端口</h4>
<p>这一步很重要，如果你都配置完了却发现连接不上，那一定是端口没有打开，默认端口是3690.</p>
<pre><code class="language-bash">iptables -I INPUT -i eth0 -p tcp --dport 3690 -j ACCEPT  #开放端口
service iptables save #保存 iptables 规则（如不能保存请使用其他方法保存）
</code></pre>
<h2 id="">客户端连接</h2>
<h4 id="windows">Windows</h4>
<p>使用<code>TortoiseSVN</code>,url填写<code>svn://你的服务器ip</code>，账号密码填刚刚设置的。</p>
<h4 id="mac">Mac</h4>
<p>使用<code>CornerStone</code>,url填写<code>svn://你的服务器ip</code>，账号密码填刚刚设置的。</p>
<h2 id="">自动部署</h2>
<p>每一次commit提交代码之后都会执行钩子<code>post-commit</code>,根据这个原理可以修改<code>post-commit</code>，让服务器上的web目录在每次有人<code>commit</code>之后自动<code>update</code>。</p>
<pre><code class="language-bash">cd /data/wwwsvn/myrepo/hooks  #你的版本仓库目录
cp post-commit.tmpl post-commit
vi post-commit
</code></pre>
<p>内容如下</p>
<pre><code>export LANG=zh_CN.UTF-8 #必须要这行
echo &quot;hello world&quot; &gt;&gt; /tmp/svn.log  #用来测试钩子是否有执行，调试使用，如果正常就不需要这行了
/usr/bin/svn  update /data/wwwroot/yourWebDir  --username autoweb --password autoweb --no-auth-cache #也可以用其他方法，总之要保证web目录能正常update
</code></pre>
<p>钩子文件里的其他都可以不要了,可以都把他们注释掉。<br>
能这么使用的前提是你的<code>yourWebDir</code>已经<code>checkout</code>过了</p>
<pre><code class="language-bash">cd /data/wwwroot/yourWebDir
svn checkout svn://你的服务器ip
... #根据提示完成checkout
</code></pre>
<p>另外，<code>post-commit</code>脚本必须有<code>x</code>权限。</p>
<pre><code class="language-bash">chmod +x post-commit
</code></pre>
<p>至此全部搞定，每一次commit到服务器会自动更新网站内容了。<br>
这也是上次服务器数据丢失之后第一次记录了。</p>
</div>]]></content:encoded></item><item><title><![CDATA[UIViewController生命周期分析]]></title><description><![CDATA[<div class="kg-card-markdown"><p>做一个实验，通过实验来分析viewController的生命周期。</p>
<h5 id="">和生命周期几个相关的方法</h5>
<pre><code class="language-objectivec">- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@&quot;FirstVC viewDidLoad&quot;);
    
}
-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear];
    NSLog(@&quot;FirstVC viewWillAppear&quot;);
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    NSLog(@&quot;FirstVC didReceiveMemoryWarning&quot;);
}
-(void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:YES];
    NSLog(@&quot;FirstVC viewWillDisappear&quot;);
}
-(void)</code></pre></div>]]></description><link>https://xferris.cn/uiviewc/</link><guid isPermaLink="false">5dba7382a9c4d600015f2356</guid><category><![CDATA[iOS]]></category><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Fri, 23 Dec 2016 07:48:48 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>做一个实验，通过实验来分析viewController的生命周期。</p>
<h5 id="">和生命周期几个相关的方法</h5>
<pre><code class="language-objectivec">- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@&quot;FirstVC viewDidLoad&quot;);
    
}
-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear];
    NSLog(@&quot;FirstVC viewWillAppear&quot;);
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    NSLog(@&quot;FirstVC didReceiveMemoryWarning&quot;);
}
-(void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:YES];
    NSLog(@&quot;FirstVC viewWillDisappear&quot;);
}
-(void)loadView
{
    [super loadView];
    NSLog(@&quot;FirstVC loadView&quot;);
}
-(void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    NSLog(@&quot;FirstVC viewDidLayoutSubviews&quot;);
}
-(void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    NSLog(@&quot;FirstVC viewWillLayoutSubviews&quot;);
}
-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:YES];
    NSLog(@&quot;FirstVC viewDidAppear&quot;);
}
-(void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:YES];
    NSLog(@&quot;FirstVC viewDidDisappear&quot;);
}
-(void)awakeFromNib
{
    [super awakeFromNib];
        NSLog(@&quot;FirstVC awakeFromNib&quot;);
}
</code></pre>
<p><strong>请注意</strong>:为了保证代码顺利执行，且保证模拟器顺利加载ViewController，请务必添加<code>[super viewxxxxx]</code>的代码来初始化。<br>
我试过去掉所有的<code>[super viewxxxx]</code>代码，控制台打印如下</p>
<pre><code>2016-03-24 10:31:28.328 SIMAlbum[33599:524075] FirstVC awakeFromNib
2016-03-24 10:31:28.333 SIMAlbum[33599:524075] SecondView awakeFromNib
2016-03-24 10:31:28.771 SIMAlbum[33599:524075] FirstVC loadView
2016-03-24 10:31:28.771 SIMAlbum[33599:524075] FirstVC viewDidLoad
2016-03-24 10:31:28.834 SIMAlbum[33599:524075] FirstVC loadView
2016-03-24 10:31:28.834 SIMAlbum[33599:524075] FirstVC viewDidLoad
2016-03-24 10:31:28.835 SIMAlbum[33599:524075] FirstVC loadView
2016-03-24 10:31:28.835 SIMAlbum[33599:524075] FirstVC viewDidLoad
2016-03-24 10:31:28.835 SIMAlbum[33599:524075] FirstVC loadView
2016-03-24 10:31:28.835 SIMAlbum[33599:524075] FirstVC viewDidLoad
2016-03-24 10:31:28.835 SIMAlbum[33599:524075] FirstVC viewWillAppear
2016-03-24 10:31:28.835 SIMAlbum[33599:524075] FirstVC loadView
2016-03-24 10:31:28.835 SIMAlbum[33599:524075] FirstVC viewDidLoad
2016-03-24 10:31:28.842 SIMAlbum[33599:524075] FirstVC loadView
2016-03-24 10:31:28.842 SIMAlbum[33599:524075] FirstVC viewDidLoad
2016-03-24 10:31:28.897 SIMAlbum[33599:524075] FirstVC viewDidAppear
</code></pre>
<p><strong>但是你会发现模拟器加载的是一个黑色的没有任何内容的ViewController</strong><br>
相应的在第一个，即将push出来的ViewController也放入上面的代码。<br>
以上便是与viewController生命周期相关的方法。</p>
<h5 id="storyboard">从StoryBoard加载</h5>
<p>先放出进入第一个viewController时的控制台输出</p>
<pre><code>2016-03-24 10:55:17.503 SIMAlbum[35103:546098] FirstVC awakeFromNib
2016-03-24 10:55:17.506 SIMAlbum[35103:546098] SecondView awakeFromNib
2016-03-24 10:55:17.625 SIMAlbum[35103:546098] FirstVC loadView
2016-03-24 10:55:17.626 SIMAlbum[35103:546098] FirstVC viewDidLoad
2016-03-24 10:55:17.658 SIMAlbum[35103:546098] FirstVC viewWillAppear
2016-03-24 10:55:17.676 SIMAlbum[35103:546098] FirstVC viewWillLayoutSubviews
2016-03-24 10:55:17.676 SIMAlbum[35103:546098] FirstVC viewDidLayoutSubviews
2016-03-24 10:55:17.678 SIMAlbum[35103:546098] FirstVC viewWillLayoutSubviews
2016-03-24 10:55:17.678 SIMAlbum[35103:546098] FirstVC viewDidLayoutSubviews
2016-03-24 10:55:17.769 SIMAlbum[35103:546098] FirstVC viewDidAppear
</code></pre>
<p>没错，和我们熟悉的生命周期大致内容是一致的。<br>
接下来push进第二个viewController：</p>
<pre><code>2016-03-24 10:55:38.848 SIMAlbum[35103:546098] SecondView awakeFromNib
2016-03-24 10:55:38.853 SIMAlbum[35103:546098] SecondView loadView
2016-03-24 10:55:38.865 SIMAlbum[35103:546098] SecondView viewDidLoad
2016-03-24 10:55:38.865 SIMAlbum[35103:546098] FirstVC viewWillDisappear
2016-03-24 10:55:38.867 SIMAlbum[35103:546098] SecondView viewWillAppear
2016-03-24 10:55:38.884 SIMAlbum[35103:546098] SecondView viewWillLayoutSubviews
2016-03-24 10:55:38.903 SIMAlbum[35103:546098] SecondView viewDidLayoutSubviews
2016-03-24 10:55:38.904 SIMAlbum[35103:546098] FirstVC viewWillLayoutSubviews
2016-03-24 10:55:38.904 SIMAlbum[35103:546098] FirstVC viewDidLayoutSubviews
2016-03-24 10:55:38.905 SIMAlbum[35103:546098] SecondView viewWillLayoutSubviews
2016-03-24 10:55:38.905 SIMAlbum[35103:546098] SecondView viewDidLayoutSubviews
2016-03-24 10:55:39.413 SIMAlbum[35103:546098] FirstVC viewDidDisappear
2016-03-24 10:55:39.413 SIMAlbum[35103:546098] SecondView viewDidAppear
</code></pre>
<p>接下来push进第三个ViewController，为了看到第二个viewController的过程，没有在第三个viewController添加任何代码，控制台输出如下：</p>
<pre><code>2016-03-24 10:55:57.906 SIMAlbum[35103:546098] SecondView viewWillDisappear
2016-03-24 10:55:57.920 SIMAlbum[35103:546098] SecondView viewWillLayoutSubviews
2016-03-24 10:55:57.920 SIMAlbum[35103:546098] SecondView viewDidLayoutSubviews
2016-03-24 10:55:58.424 SIMAlbum[35103:546098] SecondView viewDidDisappear
</code></pre>
<p>返回第二个viewController:</p>
<pre><code>2016-03-24 10:56:10.539 SIMAlbum[35103:546098] SecondView viewWillAppear
2016-03-24 10:56:10.541 SIMAlbum[35103:546098] SecondView viewWillLayoutSubviews
2016-03-24 10:56:10.552 SIMAlbum[35103:546098] SecondView viewDidLayoutSubviews
2016-03-24 10:56:11.055 SIMAlbum[35103:546098] SecondView viewDidAppear
</code></pre>
<p>返回第一个viewController</p>
<pre><code>2016-03-24 10:56:48.577 SIMAlbum[35103:546098] SecondView viewWillDisappear
2016-03-24 10:56:48.577 SIMAlbum[35103:546098] FirstVC viewWillAppear
2016-03-24 10:56:48.587 SIMAlbum[35103:546098] FirstVC viewWillLayoutSubviews
2016-03-24 10:56:48.587 SIMAlbum[35103:546098] FirstVC viewDidLayoutSubviews
2016-03-24 10:56:49.091 SIMAlbum[35103:546098] SecondView viewDidDisappear
2016-03-24 10:56:49.091 SIMAlbum[35103:546098] FirstVC viewDidAppear
2016-03-24 10:56:49.093 SIMAlbum[35103:546098] FirstVC viewWillLayoutSubviews
2016-03-24 10:56:49.093 SIMAlbum[35103:546098] FirstVC viewDidLayoutSubviews
</code></pre>
<h5 id="viewcontroller">代码加载viewController</h5>
<p>依旧做了个实验，进入代码生成的viewController时控制台输出如下：</p>
<pre><code>2016-03-24 11:09:49.361 SimDraw[36310:564381] FirstVC loadView
2016-03-24 11:09:49.370 SimDraw[36310:564381] FirstVC viewDidLoad
2016-03-24 11:09:49.381 SimDraw[36310:564381] FirstVC viewWillAppear
2016-03-24 11:09:49.393 SimDraw[36310:564381] FirstVC viewWillLayoutSubviews
2016-03-24 11:09:49.393 SimDraw[36310:564381] FirstVC viewDidLayoutSubviews
2016-03-24 11:09:49.395 SimDraw[36310:564381] FirstVC viewWillLayoutSubviews
2016-03-24 11:09:49.395 SimDraw[36310:564381] FirstVC viewDidLayoutSubviews
2016-03-24 11:09:49.929 SimDraw[36310:564381] FirstVC viewDidAppear
</code></pre>
<p>退出时</p>
<pre><code>2016-03-24 11:10:20.636 SimDraw[36310:564381] FirstVC viewWillDisappear
2016-03-24 11:10:21.166 SimDraw[36310:564381] FirstVC viewDidDisappear
</code></pre>
<h4 id="">分析与总结</h4>
<p>以上的结果简单粗暴，虽然和印象中的一样，但是还是有些许出入，我系统的做了个viewControll的图：<br>
<img src="http://image.simapps.cn/2019/12/1863913-2187d6b7ccea079e-png.jpeg" alt="1863913-2187d6b7ccea079e-png"><br>
注意到其中的viewWillLayoutSubviews和viewDidLayoutSubviews，调用情况视具体的viewDidLoad和viewWillAppear等方法中的代码而定。</p>
<h6 id="viewwilllayoutsubviews">viewWillLayoutSubviews调用情况分析</h6>
<ul>
<li>init初始化不会触发layoutSubviews</li>
<li>addSubview会触发layoutSubviews</li>
<li>设置view的Frame会触发layoutSubviews，当然前提是frame的值设置前后发生了变化</li>
<li>滚动一个UIScrollView会触发layoutSubviews</li>
<li>旋转Screen会触发父UIView上的layoutSubviews事件</li>
<li>改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件</li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[如何理解CGAffineTransform]]></title><description><![CDATA[<div class="kg-card-markdown"><h4 id="cgaffinetransform">CGAffineTransform</h4>
<blockquote>
<p>A structure for holding an affine transformation matrix.</p>
</blockquote>
<p>以上是它的定义，其实就是一个矩阵的结构体，经常用于动画，形状变换。<br>
包含如下参数：</p>
<pre><code class="language-objectivec">struct CGAffineTransform { CGFloat a; CGFloat b; CGFloat c; CGFloat d; CGFloat tx; CGFloat ty; }; typedef struct CGAffineTransform CGAffineTransform;  
</code></pre>
<p>下面直观的描述这个这个矩阵和坐标之间的关系。<br>
<img src="http://upload-images.jianshu.io/upload_images/1863913-bafa1e68467ca37e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""></p>
<h4 id="">一个实验</h4>
<ul>
<li>给一个<code>UIImageView</code>添加手势</li>
</ul>
<pre><code class="language-objectivec">    //zoom手势
    UIPinchGestureRecognizer* zoomer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(editImageWithZoom:)];
    
    UIRotationGestureRecognizer* rotation = [[UIRotationGestureRecognizer</code></pre></div>]]></description><link>https://xferris.cn/cgaffinetransform_note/</link><guid isPermaLink="false">5dba7382a9c4d600015f2354</guid><category><![CDATA[iOS]]></category><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Fri, 23 Dec 2016 07:48:21 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><h4 id="cgaffinetransform">CGAffineTransform</h4>
<blockquote>
<p>A structure for holding an affine transformation matrix.</p>
</blockquote>
<p>以上是它的定义，其实就是一个矩阵的结构体，经常用于动画，形状变换。<br>
包含如下参数：</p>
<pre><code class="language-objectivec">struct CGAffineTransform { CGFloat a; CGFloat b; CGFloat c; CGFloat d; CGFloat tx; CGFloat ty; }; typedef struct CGAffineTransform CGAffineTransform;  
</code></pre>
<p>下面直观的描述这个这个矩阵和坐标之间的关系。<br>
<img src="http://upload-images.jianshu.io/upload_images/1863913-bafa1e68467ca37e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""></p>
<h4 id="">一个实验</h4>
<ul>
<li>给一个<code>UIImageView</code>添加手势</li>
</ul>
<pre><code class="language-objectivec">    //zoom手势
    UIPinchGestureRecognizer* zoomer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(editImageWithZoom:)];
    
    UIRotationGestureRecognizer* rotation = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(editImageWithRotation:)];
    [imageView addGestureRecognizer:zoomer];
    
    [imageView addGestureRecognizer:rotation];
  
</code></pre>
<ul>
<li>手势实现方法</li>
</ul>
<pre><code class="language-objectivec">//缩放
-(void)editImageWithZoom:(UIPinchGestureRecognizer*)sender
{
    CGAffineTransform transform= CGAffineTransformScale(originTransform, sender.scale, sender.scale);
    imageView.transform=transform;
}
//旋转
-(void)editImageWithRotation:(UIRotationGestureRecognizer*)sender
{
    CGAffineTransform transfrom = CGAffineTransformRotate(originTransform, sender.rotation);
    imageView.transform=transfrom;
}
</code></pre>
<p>其中的两个方法<code>CGAffineTransformScale</code>和<code>CGAffineTransformRotate</code>是生成旋转和缩放的矩阵，当然也可以直接使用通用方法</p>
<pre><code class="language-objectivec">CGAffineTransform CGAffineTransformMake ( CGFloat a, CGFloat b, CGFloat c, CGFloat d, CGFloat tx, CGFloat ty );
</code></pre>
<p>生成对应的矩阵。</p>
<ul>
<li>继续变换</li>
</ul>
<p>不修改任何代码，继续缩放和旋转。会发现每次都重新归位后旋转。<br>
原来是<code>CGAffineTransformIdentity</code>这个常量搞的鬼。<br>
每一次的<code>rotate</code>和<code>scale</code>都是在这个常量的基础上变换的。<br>
<img src="http://upload-images.jianshu.io/upload_images/1863913-d2938fe8f8e1ba69.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""><br>
这个是它的定义。<br>
解决这个问题只要在手势代码中加入</p>
<pre><code class="language-objectivec">    if(sender.state==UIGestureRecognizerStateEnded || sender.state==UIGestureRecognizerStateCancelled)
    {
        //结束手势
        originTransform=imageView.transform;
    }
</code></pre>
<p>其中的<code>originTransform</code>可以定义为成员变量，初始化代码。</p>
<pre><code class="language-objectivec">originTransform = CGAffineTransformIdentity;
</code></pre>
<ul>
<li>
<p>坐标变换之后出现的问题</p>
<p>意识到<code>CGAffineTransform</code>所做的变换其实是对坐标系做的变换。因此变换完以后使用平移操作会发现坐标系变换以后产生的影响。解决方案：</p>
</li>
<li>
<p>取父view的坐标系，更改<code>imageView.center</code>，因为不论是<code>scale</code>还是<code>rotation</code>，<code>center</code>的点是不变的。</p>
</li>
</ul>
<h4 id="">获取变换后的参数</h4>
<p>变换以后需要取得变换以后的<code>scale</code>和<code>rotation</code>。<br>
打变量观察。</p>
<pre><code class="language-objectivec">(lldb) po transistion
 (a = 0.69003591274966281, b = -1.6204680103221447, c = 1.6204680103221447, d = 0.69003591274966281, tx = 0, ty = 0)  
</code></pre>
<p>其中<code>scale</code>是(双指缩放sx=sy)：<br>
<img src="http://upload-images.jianshu.io/upload_images/1863913-0e5e7a976eeab05d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""><br>
<code>rotation</code>是：<br>
<img src="http://upload-images.jianshu.io/upload_images/1863913-71b452842132a40f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""><br>
联合作用在单位对角矩阵上：可以得到最终的<code>transfrom</code>:<br>
<img src="http://upload-images.jianshu.io/upload_images/1863913-4a4313ae636b1be1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""><br>
可以解得：<br>
好吧根本解不出来。另寻他路。</p>
<p>打算用成员变量接受每一次旋转和缩放后的参数。<br>
打出每一次旋转和缩放操作的<code>scale</code>和<code>rotation</code>。发现每一次都是重新从1和0开始计算。<br>
于是简单了，在每一次手势结束的时候加上原来的参数。</p>
<pre><code class="language-objectivec">-(void)editImageWithRotation:(UIRotationGestureRecognizer*)sender
{

    CGAffineTransform transfrom = CGAffineTransformRotate(originTransform, sender.rotation);
    imageView.transform=transfrom;
   // NSLog(@&quot;%lf&quot;,sender.rotation);
    if(sender.state==UIGestureRecognizerStateEnded || sender.state==UIGestureRecognizerStateCancelled)
    {
        //结束手势
        radians = radians+sender.rotation;
        originTransform=imageView.transform;
    }
}
</code></pre>
<p><code>scale</code>类似方法获得。<br>
输出最后<code>imageView</code>的<code>frame</code>和最开始的<code>frame</code>。</p>
<pre><code class="language-objectivec">frame = (247.357 307.2; 273.285 409.6)  //最初的
frame = (142.016 271.144; 483.968 481.711)  //变换后的
r = 0.79710480433663233  //旋转参数
</code></pre>
<p>在<code>swift</code>的牛逼的<code>playground</code>下调试</p>
<pre><code class="language-swift">let r = 0.79710480433663233
let w = 273.285
let h = 409.6
let nw = h*cos(r)+w*sin(r)
let nh = h*sin(r)+w*cos(r)
</code></pre>
<p>发现<code>rect</code>旋转后的<code>rect</code>其实是这样：<br>
<img src="http://upload-images.jianshu.io/upload_images/1863913-a19ce482ad84ab51.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""><br>
所以要获取用户变换以后的图片，可以这么来。</p>
<pre><code class="language-objectivec">    UIImage* editedImge = [image imageByScalingToSize:CGSizeMake(originRect.size.width*scale, originRect.size.height*scale)];
    editedImge = [editedImge imageRotatedByRadians:rotation];
    
    //获取最终点的坐标
    [editedImge drawInRect:rect];
</code></pre>
<p>大功告成。</p>
</div>]]></content:encoded></item><item><title><![CDATA[UITableView实现QQ好友列表实战(动态插入删除Cell)]]></title><description><![CDATA[<div class="kg-card-markdown"><h4 id="">实现选择</h4>
<p>网上大部分的教程，都是基于修改section的hearderView来实现的，但是看QQ的好友列表，style是grouped，显然不是使用section的header来处理。使用section的hearderView来实现的，十分简单，网上也有很多源码和教程，只要刷新一下dataSource然后调用就可以了。不在本次讨论的范围之内。</p>
<pre><code class="language-objectivec">- (void)reloadSections:(NSIndexSet *)sections
</code></pre>
<p>这次我直接使用grouped的cell来做父cell，点击后展开相应的子cell，还有动画特效。(目测QQ好友列表没有使用动画特效，可能是因为好友列表过于大，内存占用问题或者是用户体验问题。)</p>
<h4 id="">封装测试数据</h4>
<p>使用FMDB(或者CoreData)从<a href="http://objccn.io/">objc中国</a>获取主issue作为父级cell，文章作为subCell，具体获取使用python和BeautifulSoup，不在本次的讨论范围之内，需要的可以查看相应的资料或者留言我，也可以在文末的项目源码里获取python代码。</p>
<h4 id="">具体实现分析</h4>
<h6 id="tableview">TableView一些相关方法介绍</h6>
<p>delegate里和点击有关的方法如下。</p>
<pre><code class="language-objectivec">- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:</code></pre></div>]]></description><link>https://xferris.cn/uitableview_insert_cell/</link><guid isPermaLink="false">5dba7382a9c4d600015f2352</guid><category><![CDATA[iOS]]></category><dc:creator><![CDATA[xferris]]></dc:creator><pubDate>Fri, 23 Dec 2016 07:47:11 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><h4 id="">实现选择</h4>
<p>网上大部分的教程，都是基于修改section的hearderView来实现的，但是看QQ的好友列表，style是grouped，显然不是使用section的header来处理。使用section的hearderView来实现的，十分简单，网上也有很多源码和教程，只要刷新一下dataSource然后调用就可以了。不在本次讨论的范围之内。</p>
<pre><code class="language-objectivec">- (void)reloadSections:(NSIndexSet *)sections
</code></pre>
<p>这次我直接使用grouped的cell来做父cell，点击后展开相应的子cell，还有动画特效。(目测QQ好友列表没有使用动画特效，可能是因为好友列表过于大，内存占用问题或者是用户体验问题。)</p>
<h4 id="">封装测试数据</h4>
<p>使用FMDB(或者CoreData)从<a href="http://objccn.io/">objc中国</a>获取主issue作为父级cell，文章作为subCell，具体获取使用python和BeautifulSoup，不在本次的讨论范围之内，需要的可以查看相应的资料或者留言我，也可以在文末的项目源码里获取python代码。</p>
<h4 id="">具体实现分析</h4>
<h6 id="tableview">TableView一些相关方法介绍</h6>
<p>delegate里和点击有关的方法如下。</p>
<pre><code class="language-objectivec">- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
- (NSIndexPath *)tableView:(UITableView *)tableView willDeselectRowAtIndexPath:(NSIndexPath *)indexPath
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
</code></pre>
<p>当有点击事件发生时，运行顺序为。</p>
<ul>
<li>willSelect</li>
<li>willDeselect</li>
<li>didDeselect</li>
<li>didSelect</li>
</ul>
<p>插入删除cell的方法为</p>
<pre><code class="language-objectivec">- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
</code></pre>
<p>记得把他们放在</p>
<pre><code class="language-objectivec">[table beginUpdates];
//input insert/delete code here
[table endUpdates];
</code></pre>
<h6 id="">逻辑分析</h6>
<p>在didSelect的时候执行插入代码。</p>
<pre><code class="language-objectivec">        [tableView beginUpdates];
        [tableView insertRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationTop];
        [tableView endUpdates];
</code></pre>
<pre><code class="language-objectivec">        [tableView beginUpdates];
        [tableView deleteRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationRight];
        [tableView endUpdates];
</code></pre>
<p>其中的indexArray需要动态计算。类似这样。</p>
<pre><code class="language-objectivec">        NSMutableArray* indexArray = [NSMutableArray array];
        for (int i=1; i&lt;=masterModel.subIuuses.count; i++) {
            NSIndexPath* path = [NSIndexPath indexPathForRow:i+indexPath.row inSection:indexPath.section];
           [indexArray addObject:path];
        }
</code></pre>
<p>可以实现这样的效果。<br>
<img src="http://upload-images.jianshu.io/upload_images/1863913-c4c5d6ee116a1243.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""></p>
<h6 id="">问题分析</h6>
<p>看起来没有什么问题。<br>
但是当点击的是展开的cell下方的cell时，indexPath就会出现问题。像下面这样。<br>
<img src="http://upload-images.jianshu.io/upload_images/1863913-9d9ef9ab5717653c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""><br>
我要点击的是2x，但是实际上点击的却是4x，问题出在哪里？看这个图片就会发现问题，原来还是那几个方法的执行顺序问题。<br>
<strong>在执行的时候，先执行didDeselect里面的代码，导致插入的cell被删除，indexPath变化，然后再didSelect，当然选中的不是我们想要选中的那个cell了。</strong></p>
<h6 id="">解决方案</h6>
<p>如下图。<br>
<img src="http://upload-images.jianshu.io/upload_images/1863913-6c30d74ad0739bf3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""><br>
只要willSelect的时候return一个新的indexPath即可，这个indexPath通过计算得出。下面是我的willSelect里的实现代码。</p>
<pre><code class="language-objectivec">-(NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath];
    if (cell &amp;&amp; [cell isMemberOfClass:[SIMMasterTableViewCell class]]) {
        //点击masterCell
        if (_isShowIssues) {
            //展开状态
            if(indexPath.row&gt;_lastSelectMatserIndexPath.row)
                return [NSIndexPath indexPathForRow:indexPath.row-_lastModelIssuesCount inSection:indexPath.section];
            else
                return indexPath;
        }
        return  indexPath;
    }
    else if(cell)
    {
        _isSelectSubCell = YES;
        return indexPath;
        //点击到uitableviewcell
    }
    return  indexPath;
}
</code></pre>
<h2 id="">最后放上一张效果图。<br>
<img src="http://upload-images.jianshu.io/upload_images/1863913-a2b6eb8320179554.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""></h2>
<p>项目源码:<a href="https://github.com/XFerris/SIMObjc">https://github.com/XFerris/SIMObjc</a></p>
</div>]]></content:encoded></item></channel></rss>