从6分钟到20秒-做照片编辑软件时的性能优化经验

前几天手搓了一个照片调色的小 App,还发了个视频出来。虽然这个视频没有人感兴趣,不过这个开发的过程中有一些点我觉得还是值得分享出来的。[视频链接]

这个 App 大致的功能就是:
这个主要的功能在开发的过程中也是遇到了一些坑,最终效果呢距离预期也有那么一点点差距。这个事儿之后找机会再分享吧。总之,做到了现在这个——效果上将将可以接受的程度。

我遇到的问题是什么呢?
说起来挺简单的哈,做的时候其实也不是特别难,没花多少时间就撸出来了。跑起来一看,妈耶~,我都惊呆了!!
跑起来之后这个功能是正常的,但是呢,一张普通的 4K 图片,四千乘三千那种,跑了有6分多钟。对,我没说错,是6分钟,超过360秒。

虽然时间是挺长的,但是至少它是自动化的对不对?比一直用手扒拉(bu lu)那小滑块也还是方便了不少的,是吧?

我当然是不会就这样善罢甘休的,就开始了一些小小的优化。最终,达到了一张 4K 图片可以在 20秒左右处理完成,这基本上就达到了一个可以接受的程度。最终效果可以看我之前发的那个[视频],就是那个样子。

接下来就是比较技术的内容了,稍微分享一下这次优化的思路和技术实现。

我首先做的一件事就是把可以预处理的部分提取出来,我用来传给模型的参数里有一些是通过对原图片的颜色和直方图进行运算得出的数据,所以这些对原图的运算是可以提前到图片载入的时候就提前进行运算的。当然,这个操作只是给运算压力换一个位置,并没有解决问题。然而通过遍历图片所有像素来做这些运算是非常非常耗时的。

然后我就跟 GPT 沟通了一下,怹给了我一些建议,可以试试 Accelerate 这个库。
Accelerate 这个是非常强大的,专门为大规模数学运算和图像运算设计的一个库。而且苹果的文档也是很给力的,有针对具体使用场景提供的文档

我对预处理部分由该后的代码片段:

分离 RGB 各个通道
vImageConvert_ARGB8888toPlanar8

var sourceBuffer = try! vImage_Buffer(cgImage: cgImage)
var rChannel = [UInt8] (repeating: 0, count: pixelCount)
var gChannel = [UInt8] (repeating: 0, count: pixelCount)
var bChannel = [UInt8] (repeating: 0, count: pixelCount)
var aChannel = [UInt8] (repeating: 0, count: pixelCount)

rChannel.withUnsafeMutableBytes { rChannelPointer in
   gChannel.withUnsafeMutableBytes { gChannelPointer in
       bChannel.withUnsafeMutableBytes { bChannelPointer in
           aChannel.withUnsafeMutableBytes { aChannelPointer in
               var redBuffer = vImage_Buffer(data: rChannelPointer.baseAddress, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: Int(width))
               var greenBuffer = vImage_Buffer(data: gChannelPointer.baseAddress, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: Int(width))
               var blueBuffer = vImage_Buffer(data: bChannelPointer.baseAddress, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: Int(width))
               var alphaBuffer = vImage_Buffer(data: aChannelPointer.baseAddress, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: Int(width))

               // 分离 RGB 通道
               vImageConvert_ARGB8888toPlanar8(&sourceBuffer, &redBuffer, &greenBuffer, &blueBuffer, &alphaBuffer, vImage_Flags(kvImageNoFlags))
           }
       }
   }
}

计算各个通道的直方图
vImageHistogramCalculation_ARGB8888

var buffer = try! vImage_Buffer(cgImage: image)
        
var histogramRed = [vImagePixelCount](repeating: 0, count: 256)
var histogramGreen = [vImagePixelCount](repeating: 0, count: 256)
var histogramBlue = [vImagePixelCount](repeating: 0, count: 256)
var histogramAlpha = [vImagePixelCount](repeating: 0, count: 256)

histogramRed.withUnsafeMutableBufferPointer { zeroPtr in
   histogramGreen.withUnsafeMutableBufferPointer { onePtr in
       histogramBlue.withUnsafeMutableBufferPointer { twoPtr in
           histogramAlpha.withUnsafeMutableBufferPointer { threePtr in
               
               var histogramBins = [zeroPtr.baseAddress, onePtr.baseAddress,
                                    twoPtr.baseAddress, threePtr.baseAddress]
               
               histogramBins.withUnsafeMutableBufferPointer { histogramBinsPtr in
                   // `buffer` is a `vImage_Buffer` structure.
                   _ = vImageHistogramCalculation_ARGB8888(&buffer,
                                                           histogramBinsPtr.baseAddress!,
                                                           vImage_Flags(kvImageNoFlags))
               }
           }
       }
   }
}

数据归一化
vDSP_normalize

let floatChannelData = channel.map { Float($0) }
        
// 计算均值和标准差
var mean: Float = 0
var stdDev: Float = 0
vDSP_normalize(floatChannelData, 1, nil, 1, &mean, &stdDev, vDSP_Length(floatChannelData.count))

用了这个工具,直接让我的数据预处理部分的耗时从分钟级下降到了毫秒级。

接下来就到了重新组织数据的部分,这部分也是所有环节中最耗时的一个。之前用的是传统的方法,把像素数据放到数据数组里之后,遍历之前预处理的数据,一个一个复制到这个数据数组里。这无疑就是在逐像素遍历的基础上又在每个图块都又遍历了一遍额外的数据,这个预处理数据也有上百个元素呢,就是一个相当沉重的负担了。

这部分的优化主要做了两件事,一个是隐性的,把像素写入数据数组的代码尽量写成对并行友好的形式,这样的话,一方面编译器的优化能力可以发挥一些作用,能在一定程度上提高多核的并行度;另一方面以后可以再用多线程去进一步优化。

nonisolated func copyBlockDataToMLArray(pixelData: [UInt8], mlArray: MLMultiArray, blockX: Int, blockY: Int, blockSize: Int, blockIndex: Int, width: Int, height: Int, bytesPerPixel: Int) {
        
   for ih in 0..<blockSize {
      for iw in 0..<blockSize {
          let x = blockX * blockSize + iw
          let y = blockY * blockSize + ih
          
          let isOut = x >= width || y >= height
          
          let pixelIndex = MyHelper.getIndexFromXY(x: x, y: y, width: width) * bytesPerPixel
          
          let r = isOut ? -1 : Double(pixelData[pixelIndex])
          let g = isOut ? -1 : Double(pixelData[pixelIndex + 1])
          let b = isOut ? -1 : Double(pixelData[pixelIndex + 2])
          
          let di = MyHelper.getIndexFromXY(x: iw, y: ih, width: blockSize)
          let batchIndex = blockIndex * inputWidth + di
          
          mlArray[batchIndex    ] = NSNumber(value: r)
          mlArray[batchIndex + 1 * blockSize * blockSize] = NSNumber(value: g)
          mlArray[batchIndex + 2 * blockSize * blockSize] = NSNumber(value: b)
          
      }
   }
}

另外一件事,是用 Swift 的指针操作直接将预处理数据拷贝到数据数组,时间复杂度直接从O(N) 变成 O(1) 。

当然,Swift 的指针没有 C++ 那么自由,写起来也需要适应一下。但是至少它可以用,并且是好用的。

for ibh in 0..<bh {
   for ibw in 0..<bw {

       copyBlockDataToMLArray(pixelData: pixelData, mlArray: inputArr, blockX: ibw, blockY: ibh, blockSize: blockSize, blockIndex: blockedCount, width: width, height: height, bytesPerPixel: bytesPerPixel)
       
       // 将遍历赋值改为整块内存拷贝,速度大幅提升,4K图片从6分钟缩短为20秒
       let extraDataHeadIndex = blockedCount * inputWidth + blockSize * blockSize * 3
       inputArr.withUnsafeMutableBufferPointer(ofType: Double.self) { inputArrPtr, _ in
           extraDatas.withUnsafeBufferPointer { extraDataPtr in
               inputArrPtr.baseAddress!
                   .advanced(by: extraDataHeadIndex)
                   .update(from: extraDatas, count: extraDatas.count)
           }
       }
       
       blockedCount += 1

       // .......
       // 其他代码
   }
}

最后的部分就是将模型输出的数据重新拼接成图片,这里的思路是,每一个图块写到图片的像素缓存里可以没有固定的顺序,完全是可以并行执行的。

所以我就用了一个线程池,(所谓的线程池就是一个 OperationQueue),让每一个图块的写入任务并行起来,这块儿最终也提升了一些运行时间。

let fullOutputQueue = OperationQueue()
fullOutputQueue.maxConcurrentOperationCount = 10

// ......
// 其他代码

fullOutputQueue.addOperation {
   // outPixelData 应该保持弱引用,暂时不管警告
   self.copyMLArrayToOutputArray(input: result, output: &outPixelData, width: width, height: height, blockSize: self.blockSize, blockWidthCount: bw, blockedCount: _blockedCount, blockStartIndex: blockStartIndex, bytesPerPixel: bytesPerPixel)
}

最终,效果就像我之前发的[视频]里的那样,可以在20秒左右的时间处理完成一张 4K 的照片。达到了一个基本可用的状态。

最后总结一下关键点:
1. 使用 Accelerate 库优化大规模运算
2. 整块拷贝内存
3. 多线程并行处理

Xcode 16.1
Swift 5

视频版

Gitlab 配置自定义 clone 地址

前情提要

  • 我是通过 docker 安装的 gitlab_ce ,版本 14.4.2。因为机器还要跑其他服务,所以不能使用 80、22 等端口,需要映射到其他端口。
  • 通过 gitlab.rb 配置文件配置的 external_url 只能配置在 clone 标签下得 host,不能带有端口号。

Clone with HTTP

  1. 首先必须是 Admin 账号。
  2. 进入 Admin Area。
  3. 在 Settings 的 General 中选择 Visibility and access controls。
  4. 往下拉,有一个标题为 Custom Git clone URL for HTTP(S) 的输入框。
  5. 可以在输入框中输入带端口号的 url。
  6. 然后保存就 OK 了。

Clone with SSH

  1. 需要在 gitlab.rb 中配置。
  2. 搜索 gitlab_ssh_host。
    // 配置主机地址 
    gitlab_rails['gitlab_ssh_host'] = '192.168.72.90'
  3. 然后搜索 ssh_port。
    // 配置主机的 ssh 端口 
    gitlab_rails['gitlab_shell_ssh_port'] = 40022
  4. 然后保存,重启 gitlab,就完成了。

Windows 编译 AOSP 趟坑笔记

  • 首先 AOSP 是不支持在 windows 系统下直接编译的,目前官方也不再支持 Mac OS ,所以只能搞个 Ubuntu 环境。
  • 我首先尝试了 wsl ,安装 Microsoft Store 官方的 Ubuntu 。这条路实际上并没有走通,不过我还是分享一些关键点。

首先,要保证代码所在文件夹必须是大小写敏感的。使用管理员模式运行命令行,执行下面命令,开启文件夹的大小写敏感。

fsutil.exe file setCaseSensitiveInfo E:\xxx\aosp enable

这里要注意一点,建议先处理好文件夹再下载代码,因为这条命令只能操作一层文件夹,如果已经下载完的代码,再想改变所有文件夹的大小写敏感状态就需要写脚本去处理了,会很慢。

其次,Ubuntu 要切换到 wsl2 ,具体方法可以搜索。

然后按照官方文档的方法安装环境和同步代码,如果访问官方代码库速度不理想也可以用国内清华或者科大的镜像,具体操作在他们镜像站都有。

访问 windows 文件夹的话,盘符在 /mnt 文件夹下。

我因为代码下载到了 windows 路径中,导致无法启动编译。会一直报系统不支持的错误:

Failed to listen for path logs: listen unix 
out/.path_interposer_log: bind: operation not supported

已经提 issue
https://github.com/microsoft/WSL/issues/7283


现在切回到主线,我后来尝试了使用 docker 。可以成功的启动编译了,但是访问 windows 路径会及其慢,一次编译用了10几个小时(配置:i7 + 32G 内存)。

  • windows 下不论是 wsl 直接安装的 Ubuntu 还是用 docker,都不建议直接访问 windows 路径。
  • 在 Ubuntu 下直接同步代码,按照官方文档进行编译,可以很顺利的完成。
  • 不过如何从 docker 启动模拟器我目前还没找到好办法,可能会考虑电脑直接装双系统了。

解决 iOS 的 SSH 连接不上的问题

要用 SSH 连接 iPhone 手机,当然要越狱才行。当然越狱不是这篇的重点,就不多说了。
然后手机需要装有 OpenSSH ,通过 Cydia 就可以安装,很简单。
然后就遇到问题了,用的是一个 iPhone 6,系统 10.3.3 。发现即使都安装好了,使用 SSH 还是无法连接。

查资料的过程就不介绍了,解决方案如下:
首先需要在手机上安装一个命令行 App ,自己找 Cydia 的源就好了,只要能用就行。
使用命令行先停掉 sshd 的 plist

launchctl unload /Library/LaunchDaemons/com.openssh.sshd.plist

然后用如下命令启动 sshd

/usr/libexec/sshd-keygen-wrapper

之后就可以在电脑上使用 ssh 操作我的苹果手机了。
没有在其他设备和系统测试过,不知道能不能解决所有问题,希望对看到这篇博文的你能有一点帮助。

Unity3D 解决默认碰撞检测的缺陷 —— 实现理想的匀速直线运动

首先介绍一下默认碰撞检测存在的问题。先不提【我只想要你丫告诉我碰撞盒撞上了,却非得要我挂个钢体组件,还得接受重力,阻尼,角速度的影响。不是在逗我?】,那么我们手动勾选上钢体的 Is Kinematic 属性,自己来控制。
我们看下边的图:
collider1
当这个圆走到实线圆圈位置的时候,就会触发碰撞检测回调。当然,如果是在虚线圆圈的位置,回调是不会被触发的。那么问题来了,如果一帧的移动距离刚好是从虚线位置移动到实线位置,情况就是这样的,移动前一帧不会触发碰撞回调,而后一帧两个碰撞体已经嵌到一起了。
当然,如果只是小小的插入一下,也并没有什么问题,接下来看下面的图:
collider2
当一帧的移动距离超过碰撞目标的时候,奇迹发生了,这时碰撞检测的回调根本不会触发。也就是说在速度足够快的情况下,你的墙是挡不住你的球的。
为了实现一个在理想条件下的匀速直线运动,我就用 Unity 本身的射线检测功能来解决这个问题。
首先取出自己的碰撞盒

var selfCollider = GetComponent<Collider2D>();
Collider 中有 Raycast 和 Cast 两个方法,参数差不多,具体的区别,还得画两张图:
ray1
图一
ray2
图二
ray3
图三
图一、图二中蓝色的线就是 Raycast 的结果,是从自己的中心射到目标碰撞盒上,如果按照这个射线的距离运动,结果就是图二的样子。
图三种的蓝色线是 Cast 方法的结果,射到目标碰撞盒上的是自己碰撞盒与目标碰撞盒的交点。这个距离就是从当前点移动到发生碰撞的点的距离。
简单一点就用 Cast 方法就够了,Raycast 比 Cast 提供了更多细节操作,比如专门针对某一层的检测等,如果需要做更细节的优化,就得自己用 Raycast 方法来处理。

RaycastHit2D[] casthits = new RaycastHit2D[maxRaycastCache]; 
int num = selfCollider.Cast(forward, casthits);
RaycastHit 中比较重要的属性:
collider —— 目标碰撞盒
distance —— 到碰撞点的距离(上面图3中蓝线的长度)
normal —— 碰撞点的法线
point —— 目标碰撞点
接下来的逻辑就是:
比较本帧移动距离和 distance ,如果本帧移动距离比较小,就直接移动。

float stepDistance = speed * Time.fixedDeltaTime; 
if (stepDistance < distance) 
{ 
   gameObject.transform.Translate(forward * stepDistance); distance -= stepDistance; 
}
当然之后要把总距离减去这步的移动距离。
如果这一帧的距离大于剩余的距离了,就需要处理反射了。
先移动到碰撞点→方向向量反射→继续移动完。

while (stepDistance > nextDistance) 
{ 
    gameObject.transform.Translate(forward * nextDistance); 
    stepDistance -= nextDistance;

    forward = Vector2.Reflect(forward, normal); 
    /* 找下一个碰撞目标 */ 
    …… 
}
打完收工。
当然这只是简单的描述,实际上还会遇到一些坑,比如当前自己和一个碰撞盒有重叠的时候,取到的第一个 RaycastHit 永远是有接触的这个碰撞盒,此时与方向向量无关,所以需要根据自己的逻辑处理。还有同时碰撞两个或多个碰撞盒等,都要小心处理。

Android 自动化批量打包精粹

在项目即将上线的时候,都会遇到这样的问题:
有好多个有自己 SDK 的商店,而且都要赶同一个档期上线。
个别商店可能会有一些独特的小需求,比如 icon 的角标、开机闪屏 logo 、或者独有的功能接口要调用。
当10个 SDK 接完8个了,第1个接入的有更新了,需要接入最新版本才给上线。最要命的是接口有变动。
双方联调,找问题,各种沟通不畅。
下一个项目这些问题再重演一遍。

所以这个事情如果要做的优雅,高逼格,高效简洁,我们应该做到:
一次接入可以让所有项目通用。
各个部分充分解耦(SDK、项目、服务器),单独接入单独调试单独测试。
渠道商店配置可以由非技术人员(市场、运营)来管理。
当然不可少的打包应该有工具自动化完成。

简简单单几句话来描述这个事情,看起来挺简单,但是仔细想想,都挺空的。所以,还是要细化一下具体的东西。

一、 一次接入所有项目通用
要实现通用,就需要有一个比较稳定的接口将功能性 SDK 封装起来。封装起来的 SDK 我们可以姑且命名为 SDK 插件,各个项目调用统一的接口,而不需要关心 SDK 插件之中的具体的 SDK 。

二、各个部分解耦
实际上稳定统一的接口就是完成了解耦的第一步,我们需要梳理清楚具体部分的划分。渠道商店的 SDK 往往都会有服务器端接口部分,用作用户校验和支付验证。所以大致可以分为三个部分:SDK 插件开发、渠道商店对接服务器、客户端使用的 API。
各部分详细的设计参考下面的示意图:
结构图

开发 SDK 插件的流程:
前提条件:模板工程、可用的 SDK 、测试服务器接口
流程:通过模板创建工程 → 接入 SDK → 测试通过 → 导出 SDK 插件
SDK开发模板
当完成这一部分的时候,SDK 的对接就可以完全不需要核心项目团队的资源来支持了。这样就实现了一定程度的解耦,而核心项目部分,不用关心这些 SDK 功能的具体实现,只要接入一个用于测试各种 SDK 功能的测试 SDK 就可以无忧了。
测试SDK
从上面两个图可以看到,独立出一个 Platform Server 平台服务器来做与 SDK 相关的对接。这样的好处是,服务器端也实现了解耦;一个平台服务器可以服务于多个项目;并且不需要项目团队拿出资源来维护这个部分。而区分不同项目不同商店渠道只要以自定义标准参数的形式,制定出一套自己的标准就可以通用于所有接入的项目了。

对于测试 SDK 的设计,有必要叨唠两句。这个东西的目的是把通过 SDK 获取的数据直接给客户端,所以有些东西可以更直接一些。比如,需要登录获取一个 user id ,这种需求直接做一个输入框或者下拉列表,直接让开发人员输入或者选择 user id,就可以了。没有必要还让测试人员走一个完整的登录流程是不是?并且这样可以随意调整需要登录的账号,方便测试。支付同理,客户端需要处理在成功时会怎样,失败时会怎样,那么直接用两个按钮,返回给客户端结果就可以了。真的没有必要还调个支付宝啥的真去走个第三方支付流程。

三、 由非技术人员管理
因为实际上对于渠道商店出包的需求都是由非技术人员(运营、市场等)发起的,所以最好由非技术人员负责管理渠道商店的参数、数据、出包等。这样不仅可以解放出一小部分技术人员的生产力,最关键的是减少了沟通造成的损耗,当运营取得渠道相关参数之后,直接在工具中填写并提交然后工具自动读取后将需求的包打出来,这样这个流程一个人就可以完成了。
这个工作流的设计的关键当然是非技术人员以什么形式来提交给工具这些参数?最好的方案,当然是有一个非常友好的界面,可以让用户根据提示正确的填写各种参数以及提交资源等。但是如果在开发资源不足的情况下,退而求其次可以考虑使用 excel 表格,一般运营人员经过简单培训也是完全可以 hold 住的,只要将标准制定的全面且完善。

四、 自动化打包
首先只打一包的话使用 ant 一个命令就可以实现,并且同时完成包括 编译 打包 混淆 签名 等完整的流程。然而,最重要的是我们要根据之前填好的数据自动打出所有我们需要的包。
要实现这个需求,单靠 ant 脚本就不太现实了,虽然理论上也是可以实现的。这里还是建议使用强逻辑型的语言实现这个程序。在最后打包的环节再调用 ant 脚本完成打包的最后一步。
这个程序大框架上看是一个很简单的流水线,可以看下面的图。然后有很多的细节需要处理,这其中的任何小细节被忽略都有可能无法良好的工作。
流水线

编译 C++ 我的习惯是只编译一次,所有包使用同一个 so 这样可以最大程度的保证编译速度。需要传给 C++ 部分的一些参数可以通过 JNI 传递,运行效率是会有一些小影响,几乎可以忽略不计,可以让打包部分的复杂度降低不少。不过确实有一些特殊情况需要在 so 文件中打入一些数据,那就根据自己的情况再具体解决了。

我习惯在打每个包之前都删掉生成文件夹 bin 和 gen ,因为曾经有发生过不少次因为之前的生成文件的存在导致后来的改动并没有被编译进生成文件中。并且不要忘记,如果项目有引入 Library 的话,最好递归的去清理一些。

在对工程做任何改动之前都不要忘记做现场的保护。我的做法是,如果是对一个已经存在的文件做修改,我会把那个文件做备份和操作记录。再这个包打完或者出问题被中断后可以把现场恢复到最初的状态;如果是加入一个新的文件也要记录下来,在打完这个包将其删除。这样做可以最大程度的保障不会让当前操作污染到以后的事情。

可能有人注意到了,流水线这张图并没有提到 SDK Plugin 。实际上我赞成把 SDK Plugin 看做一个实体,只是为了描述更方便,理解更直观一些,具体存在形式最好是根据自己系统的情况量身定制。它可以只是散乱的一堆代码和一堆资源,也可以是其他合适的形式甚至是代码片段或者 patches ,如何使用需要根据自己的情况具体问题具体分析,而这个流水线图展示的是最纯粹的重要工作节点。

引入 Library 只需要修改 project.properties 中的 android.library.reference.* 参数,当然不要忘了在后边添加新的之前要先解析一下旧的,取到一个当前数字再添加新的数字。然而为什么是这个文件呢?它的秘密都在 build.xml 这个默认的 ant 脚本文件里,这个文件的注释读一读可以获得好多重要的信息。

<!-- The project.properties file is created and updated by the 'android' tool, as well as ADT. 

This contains project specific properties such as project target, and library dependencies. Lower level build properties are stored in ant.properties (or in .classpath for Eclipse projects). 

This file is an integral part of the build system for your application and should be checked into Version Control Systems. --> 

<loadproperties srcFile="project.properties" />

默认的 ant 构建系统是需要自己生成 local.properties 这个文件,并且设定 sdk.dir 来显示指出 Android SDK 的路径。我喜欢把 build.xml 做个小改动,让这个变量直接访问环境变量:

<property environment="SystemVariable" /> 
<property name="sdk.dir" 
value="${SystemVariable.ANDROID_SDK_ROOT}" />

不管是选择使用传统的 local.properties 还是修改 build.xml ,都必须要保证所有引入的 library 都有一份。我选择修改 build.xml 这个做法就是为了在不同环境中不用再多修改一个文件。

每一个渠道商店的包都必然会有一些常量参数,而使用这些参数的大多是相关的 SDK 。所以我的方案是生成一个常量类写到代码文件中替换掉之前埋在工程中用于测试的那个类。而这里最好将生成后的类做个 log,以便在出包后可以再检查一下数据是否有误,毕竟打错参数这种事情有时候并不容易被测试发现。

有许多 SDK 都会有要求在 AndroidManifest.xml 加一些东西,比如,声明 Activity、 Receiver 、权限等等,也有包括包名、版本号这些需要修改的东西。所以读写这个 xml 也是这个工具必不可少的一部分。那么数据是从哪里来的呢?首先,参数类的数据,肯定是从配置信息里获取的(包名、版本号、其他参数等);然后,与 SDK 有关的,肯定是不能让非技术人员来配置的,这部分就要作为 SDK Plugin 的一部分,可以在 SDK Plugin 中放一个专属这个 SDK 的 xml 配置,在这一步的时候从里边读取与工程中的 AndroidManifest.xml 合并。(有人选择使用替换特殊标示的方式来操作这个文件,这样的话就需要提前在这个文件里写很多特殊标识符,其后果是这个工程可能就不能直接编译运行调试了。想运行一下还需要先替换标识符感觉就有点累了,所以我认为还是读写 xml 是最靠谱的方案)

写 AndroidManifest.xml 的时候经常会遇到命名空间不见了的情况,表现为原文件的 android:xxxx 变成了 ns0:xxxx 。原因就是原来的 xml 命名空间被丢掉了,修改这个文件一定不要忘记手动设置一下 manifest 的命名空间:
xmlns:android=”http://schemas.android.com/apk/res/android”

签名的配置修改 ant.properties 这个文件,key.store 这个变量就是签名文件的路径,不过签名文件是需要提前生成好的,这个常打包的同学应该并不陌生,这里就不展开了。

要强调的一点是 .properties 文件写中文的话是会出错的,这个文件的编码是 ascii 所以如果传入的路径或文件名有包含非 ascii 字符的话,是需要转换成 \u4e00 这种形式的编码的。

解决了 WordPress 中文标签或分类目录返回404的问题

WordPress 版本: 4.4.1 中文版

问题描述:
在访问中文的标签[tag] 或分类目录[category] 的时候,系统返回404。
举个例子,可以试一下访问 https://ftxtool.org/index.php/tag/测试/ ,显示的页面是“未找到” ,然而是一个正常返回的页面,并不是404;而如果访问一个根本没有的标签或分来目录,比如 https://ftxtool.org/index.php/tag/根本没有/ ,显示的页面是 “有点尴尬诶!该页无法显示。”,而且 http 响应不是200而是404。
所以正确的姿势应该是,访问一个存在的标签,就应该返回200而不是404。

解决方法:
代码文件:wp-includes/class-wp.php
代码:

$req_uri = str_replace($pathinfo, '', $req_uri);

修改为:

$req_uri = str_replace($pathinfo, '', urldecode( $req_uri ));

吐槽:
我不知道网上分享解决方案的那些人是在什么样的环境下解决了问题的,我很愿意相信他们是真的亲自操作遇到问题并解决了。然而我的环境下的这个问题,用那些搜出来的长得都差不多的信息对于从根本上解决问题并没有什么帮助。

调试过程:
经过艰苦卓绝的调试,各种打 log ,最后发现 $_SERVER[‘PATH_INFO’] 和 $_SERVER[‘REQUEST_URI’] 这两个变量的值在编码上是有区别的。PATH_INFO 取出的值是其中的中文是经过 urldecode 之后的,就是正常的中文;而 REQUEST_URI 的中文并没有经过 urldecode ,是那种百分号和16进制数的那种形式。因为这样的区别,所以导致 str_replace 并没有达到其所期望的效果。
因为 replace 后的字符串是错的,导致后边一系列错误导致没有正确匹配到数据库中的数据。

最后吐槽一下 WordPress 官方。在解决完这个问题后,我想把修改的代码提交给官方,然后发现他们的 github 是不接受 pull request 的,只能去官方的网站提交 trac [https://core.trac.wordpress.org/] 。写好之后他们的回复到是挺快的,大叔告诉我这个问题已经有人提过 issus 了,在 #10249#17450,有兴趣的同学可以点进去看。尼玛都有5年的历史了,有木有……新的代码里还是没有加进来,有木有……这是准备了5年的愚人节惊喜么?

初撸WordPress小记

撸这个 WordPress 站有一段时间了,也解决了一些比较蛋疼的问题,大晚上的一股鸡血上脑,就撸了一发小文,记录一下这段时间解决的几个比较有用的问题。

1. 中文标签和分类的问题
偶然发现直接点击中文标签或分类链接,wordpress 是根本找不到相关内容的。相当于访问 https://ftxtool.org/index.php/category/技术随笔/ 的时候服务器给我返回的是 404 ,然而,使用 https://ftxtool.org?category=技术随笔 传参数的方式却可以正常访问到,估计是 url 地址重定向时候的问题。
所以,网上能找到一类解决方案就是修改 rewrite.php 的代码。然而,就像某个复制粘贴的博主说的那样,根本没有找到别人说的那块代码在哪。应该是经过很多版本的更新,那部分代码早就不一样了,然而这种复制粘贴的精神依然屹立,永不过时。
另外一个解决方案,是修改 class-wp.php ,把 $_SERVER[‘PATH_INFO’] 和 $_SERVER[‘REQUEST_URI’] 转码,把 GBK 转成 UTF-8 。然而,经过测试好像并没有什么卵用。
方案三,修改别名。把别名改成一小串英文,亲测有效。具体操作方法就是:
仪表盘→分类目录/标签→选一个具体的分类或标签名字,点下边的快速编辑→别名(随便写个英文的名字),然后更新就好了。
方案四,插件。方案三虽然可以解决问题,但是每个标签都要手动改一遍还是有些蛋疼的。找来找去找到个插件,用起来还不错。插件名叫:IIS Chinese Tag Permalink 直接去安装插件里搜索这个名字就能找到。目前用起来还没啥问题,应该算比较靠谱的一个方案。然而还是有个别标签一直 404 ,只能通过改别名的办法解决,不知道有没有更好一些的插件可以解决这个问题。

2. sitemap
赵家百度的 sitemap 插件简直渣到爆,跟 google 的 XML-Sitemap 插件简直没法比。从设置页面就已经被秒成渣渣了。最蛋疼的是生成 sitemap 文件,我去点了按钮也没反应,一直是:没有生成 sitemap 文件。麻痹的没有任何提示了,只好去代码里下 log。
原来生成文件是在 wordpress 的跟目录,代码注释里竟然叫我把目录整个权限设成777。简直日了狗,你家运维都是这么干活的么?
然后,还是手动在根目录创建了个 sitemap 文件,然后加个写权限,就可以用了。
然而,还是 google 家的 sitemap 好使。自己自动生成好之后还能自动通知 google 和 bing 。

3. 友情链接
新版本添加友情链接还是蛮蛋疼的,网上能搜到的基本上都是老版本的方案,或者 link manager 插件,但是插件并不好使。真心想不明白,这帮人复制来复制去也没有经过验证,然后还被搜索引擎排的那么靠前。
新版本添加链接其实很简单,只要在侧边栏加上一个自定义菜单就可以了:
外观→自定义→小工具→侧边栏
点击【添加小工具】选择【自定义菜单】
然后选上自己添加好链接的菜单就可以了。

然而怎么添加一个菜单呢?
外观→菜单
点击【创建新菜单】
然后 填一个名字
然后 在【自定义链接】中添加
能看懂提示文字基本上不会错。

注意:这个菜单不要勾选 主菜单 。

MacOS OpenGL窗口程序的正确打开方式

当小白挖开gl渲染这个坑的时候,最先遇到的问题一定是窗口。然后就是虐死强迫症系列的剧情。
教程最多的一定是glut系列,然而,放到Xcode里一片黄,原因是苹果在未来要不支持这玩意了。再之后就是glew、glfw等一堆库,到这个时候一个没什么基础的小白玩家应该已经有流失的想法了。
这篇博客主要介绍怎么直接搞出一个干净的glview窗口程序,简简单单的开始OpenGL的新手引导。

然后也可以解决一些问题:
1. 如何创建一个OpenGL窗口
2. glut的警告问题
3. 可以使用4.0以上版本的OpenGL
4. 对于非小白玩家而言可能第3条是最重要的

废话少说,书归正传:
第一步、创建工程
1. 新建一个Cocoa Application工程
130298
2. 拖一个OpenGL View 替换掉原来的默认View
582077
这样这个窗口就可以启动运行了。

第二步、写代码
1. 新建一个Cocoa Class,选择继承自 NSOpenGLView
306506
2. 把上边建好的GLView关联上我们自己的类
742882
这样就可以在我们自己类中调用渲染代码了。

第三步、支持GL4.1
需要实现的函数:
initWithCoder
defaultPixelFormat
prepareOpenGL
drawRect

重点就是在初始化部分,需要设置一下属性才能真正的支持4.1

- (nullable instancetype)initWithCoder:(NSCoder *)coder { 

   [[self openGLContext] makeCurrentContext]; 

   self = [super initWithCoder:coder]; 

   return self; 
} 

+ (NSOpenGLPixelFormat*)defaultPixelFormat { 
   NSOpenGLPixelFormatAttribute attrs[] = 
   { 
      NSOpenGLPFADoubleBuffer, // 可选地,可以使用双缓冲 
      NSOpenGLPFAOpenGLProfile, // Must specify the 3.2 Core Profile to use OpenGL 3.2 
      NSOpenGLProfileVersion4_1Core, 
      0 
   }; 

   return [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; 
}

这样就是一个可以正常撸的GL窗口程序了。非常适合强迫症玩家入门使用,编译器完全没有警告。

在这就不贴完整代码了。有一个可运行的程序,在github上。
https://github.com/XiaochenFTX/glFirst

C++ 如何让自己的类支持foreach

首先,我们先看看使用 foreach 的时候,都发生了什么。

新建一个类,根据经验,我们 for 一个对象应该是这样的:
for (XX::iterator iter = xx.begin(); iter != xx.end(); ++iter)
所以,迭代器, beigin(), end() 应该是会用得到的,我们做个试验:
Generator.h
class Generator 
{
public:
    typedef int *iterator;
    iterator begin();
    iterator end();
};

Generator.cpp

Generator::iterator Generator::begin()
{
    std::cout << "begin()" << std::endl;
    return nullptr;
}

Generator::iterator Generator::end()
{
    std::cout << "end()" << std::endl;
    return nullptr;
}
main.cpp
Generator g;
for (auto i: g)
{
    std::cout << i << std::endl;
}
运行结果:
begin() 
end()
说明我们想的没错,foreach 跟上边的 for 的行为是一样的。
插个体外话,(发现字打错了,不过不改了,还是挺哏儿的)
1. 之前以为 for (const auto i : g) 会调用 const 版本的 cbegin cend ,然而 并不会。
2. iterator 叫什么名字都没什么所谓,起名叫 红太阳 也ok。
接下来,处理一下迭代器:(我想确认一下丫具体被如何操 做)
class iterator
{
public:
    iterator(int*p)
    : _p(p)
    {
    }
    int * operator()()
    {
        std::cout << "operator()" << std::endl;
        return _p;
    }
    int operator*()
    {
        std::cout << "openator*" << std::endl;
        return 0;
    }
    iterator & operator++()
    {
        std::cout << "++operator" << std::endl;
        ++_p;
        return *this;
    }
    iterator operator++(int)
    {
        std::cout << "operator++" << std::endl;
        return _p++;
    }
    bool operator==(const iterator& it)
    {
        std::cout<< "operator==" <<std::endl;
        return true;
    }
    bool operator!= (const iterator& it)
    {
        std::cout<< "operator!=" <<std::endl;
        return true;
    }
private:
    int *_p;
};
输出结果:
begin() 
end() 
operator!= 
openator* 
0 
++operator 
operator!= 
openator* 
0 
++operator 
operator!= 
openator* 
0

已经足够说明问题了,自己把剩下的细节补充上就好了。

总结一下要点:
1. begin() end()
2. 迭代器
3. 迭代器重载 !=
4. 迭代器重载 前++
5. 迭代器重载 *(解引用)
RSS
Follow by Email
YouTube
YouTube
Pinterest
fb-share-icon
LinkedIn
Share
VK
Weibo
WeChat
WhatsApp
Reddit
FbMessenger
Copy link
URL has been copied successfully!