整理更新 MuPDF 和修改相关 C 代码过程中遇到的问题。
最编程
2024-05-02 12:18:10
...
问题起源
- 项目中涉及到PDF阅读器并需在原有基础上添加解密的模块,所以之前的开发人员在MuPDF的读取数据流原逻辑上添加了解密模块,然后重新生成SO库之后再配合业务来进行使用。(16年期间开发的,期间未曾改动)
- 近期APP用户在使用时打开PDF文件过程中白屏或闪退出现频率严重,所以不得不重视以及快速更新优化。更新过程中难免会遇到些问题,而这些问题又会牵连到新的问题,目前所要做的呢,就是根据更新过程中产生的新旧问题一个个的解决,不能有丝毫模糊的解决与处理,因为模糊的解决方案势必会引导新的问题产生,反而会得不偿失。
更新过程中所涉及到的官方文档及资源链接
- MuPDF官方文档及涉及的项目地址
- mupdf.com/
- mupdf.com/docs/androi…
- www.mupdf.com/downloads/i…
- Git 地址,recursive介绍(git-scm.com/book/zh/v2/…)
- git clone --recursive git://git.ghostscript.com/mupdf-android-fitz.git
- git clone --recursive git://git.ghostscript.com/mupdf-android-viewer.git
- git clone --recursive git://git.ghostscript.com/mupdf-android-viewer-mini.git
- Cygwin官方地址及安装介绍(因为我是Windows操作系统,使用虚拟机装了Unbtun后操作发现需要移植过多资源,并且我目前问题的终极目的只是需要编译下C库,那么Cygwin完全可以胜任,也省去了我移植Android开发环境与数据传送的时间)
- www.cygwin.com/
- blog.****.net/lvsehaiyang…
- Android NDK官方地址及对应配置文档(降旧版本NDK主要是因为R17及以上版本已移除了armeabi的ABI)
- developer.android.com/ndk/downloa…
- developer.android.com/ndk/guides/…
- developer.android.com/ndk/guides/…
从零开始更新(看似漫漫之路,实而不过如此)
- 一,MuPDF项目库不做任何改动的情况下生产SO库,想解决这次问题,那么就需要清晰整套流程,先从简单的步骤做起。
- 若读者当前系统环境是Mac或Linux就用不到Cygwin,(一般自带CMake环境,若没有请自行查阅解决),直接可以使用CMake相关的指令。
- 生成SO库的所需的C文件,以下是我的项目路径,请自行对应更改。
- 通过 make generate 指令生成所需C文件:
- 自己简单搭建Android PDF阅读器,我使用的是1.16.1版本的MuPDF代码
- 在此之前,你需要搞清楚MuPDF在Android项目中所需依赖的子项目及其关系。
- 官方介绍:
- 它们关系也就是:app(PDF展示层) --> mupdfLib(UI封装层) --> mupdfJni(C核心库逻辑层)
- 附MuPDFSimpleDemo项目地址:
- github.com/GenialSir/M…
- 在此之前,你需要搞清楚MuPDF在Android项目中所需依赖的子项目及其关系。
- 二,现在已经生成好最基本的MuPDF的环境,现在我需要做的就是在原有MuPDF的逻辑上进行适合自身项目的读取解密的操作。
- 这块我遇到了个棘手的问题,本原想将git链接到的mupdf-android-viewer项目直接放在Android Studio上面运行,但是在运行过程中出现如下图的问题:
- 总的来说还是,对mk文件这块不够了解,若有解决的同学请评论下面留言,不胜感激。
- 那么无法通过Android Studio运行1.16.1版本的mupdf-android-viewer项目,那我如果想修改库的C代码逻辑还有什么其它的方案呢?答案当然是有的,那就需要使用对C/C++等语言开发相较于Android Studio更专业的工具Visual Studio了,我这次用的是VS2019版本。
- 在MuPDF官方分别下载1.10.0版本至1.16.1版本,通过每个版本尝试,发现1.13.0以上版本在Visual Studio上无法编译通过,具体解决繁琐且不明确产生问题,然后调至1.10.0版本进行编译运行,从而达到调试修改MuPDF项目中源代码逻辑。测试在1.10.0版本解密成功后将改动代码移植到1.16.1的MuPDF,然后通过CMake指令来生成SO库,也就规避了1.13.0以上版本在VS编译受阻的问题。
- Visutal Studio运行MuPDF的入口,点击mupdf.sln自动导入即可:
- 然后在具体的C代码及头文件位置进行自己的解密算法处理,本篇文章只阐述改动代码后生成SO遇到的问题及思路,因涉及非个人的项目,所以不会公开改动的代码。
- 在MuPDF官方分别下载1.10.0版本至1.16.1版本,通过每个版本尝试,发现1.13.0以上版本在Visual Studio上无法编译通过,具体解决繁琐且不明确产生问题,然后调至1.10.0版本进行编译运行,从而达到调试修改MuPDF项目中源代码逻辑。测试在1.10.0版本解密成功后将改动代码移植到1.16.1的MuPDF,然后通过CMake指令来生成SO库,也就规避了1.13.0以上版本在VS编译受阻的问题。
- 这块我遇到了个棘手的问题,本原想将git链接到的mupdf-android-viewer项目直接放在Android Studio上面运行,但是在运行过程中出现如下图的问题:
- 三,现所需更新的MuPDF项目及其增添的解密逻辑添加且已经生成SO库(C解密逻辑受到公司连老师的大力帮助,若只靠自己对C的片面了解只会越改越糟,避免了很多的试错、改错的时间成本),因为公司项目的所有SO库只有对armeabi的支持,所以在更新替换过程中产生了以下几点问题:
- 目前我的开发环境NDK是紧跟官方更新版本,那么就需要自身降到r17以下的版本,所以我将r17之前的旧版本NDK都下载下来,以防对应NDK版本对C99、C11的兼容各不同所遇到自身不能解决的问题而“卡壳”。
- 官方在NDKr17版本中已经完全移除armeabi。
- NDKr17及以上版本已无法生存armeabi的ABI,此次我选择NDKr15c版本
- 通过命令ndk-build来执行,因为要频繁更换NDK版本,所以这块我没有直接使用设置好的系统环境变量,而是直接指定当前ndk所在的路径, G:/ndk_64version/android-ndk-r15c/ndk-build APP_BUILD_SCRIPT=libmupdf/platform/java/Android.mk APP_ABI=armeabi
- 如图:使用APP_ABI指定当前所需生成的ABI类型,如果生成全部则不用追加。
- 若指定多种ABI使用逗号间隔即可:
- 将在MuPDF1.16.1版本改动后的SO放在自己用来测试的项目MuPDFSimpleDemo上后,出现了个小插曲(测试手机用的是小米8,处理器为高通骁龙845,64位处理器),在使用arm64-v8a的SO库阅读PDF时一切正常,但使用armeabi的SO库时,打开PDF文件时会白屏闪退掉。
- 因为没有对应的日志,我无法定位问题,所以我假想应该不会是java.lang.UnsatisfiedLinkError这个找不到对应SO库文件的问题,我还特意写了个简单的Demo,配置了对应Android.mk及gradle文件测试,用来排查问题。
- 为了排除不是externalNativeBuild没有在64位处理器上兼容armeabi的问题,我在MuPDFSimpleDemo使用 ndk{} 这种形式来指定
- 然而经过以上处理后在MuPDFSimpleDemo项目中采用刚指定armeabi的ABI情况下打开PDF文件,依旧白屏闪退,也无法定位到问题日志,配置上arm64-v8a的SO库及指定后则正常。(那么依照这种现象说明这块应该是自己追加的解密C代码标准化版本的问题,以至于NDKr15c版本编译MuPDF1.16.1时在armeabi的ABI解释上产生了问题?因为在AndroidStudio中缺少有效C代码日志,无法定位到具体的问题。)
- 此问题已超出了目前我知识所了解的范围,并且给的开发周期也不允许我再有过多的时间去调研了。
- 通过以上遇到的问题,我只能先放弃完美的迭代新版本MuPDF的想法( 即更新MuPDF的官方版本,又更新原开发添加在MuPDF解密算法。 因为目前使用的是16年的,已经有3年之差了。),进行MuPDF降版本处理,也就是说只更新原项目MuPDF的SO库解密算法,不更新MuPDF官方版本 。
- 因此我准备将在1.16.1版本上添加的C解密逻辑移植到之前项目所用的MuPDF版本,所幸的是MuPDF这3年期间的大幅度更新并未在读取数据流这块有大变动,从而也方便了我从高版本往低版本的移植。在低版本的MuPDF读取加密PDF测试通过后,我将优化解密算法后的SO库移植到公司项目经测试后,解决了因几百页或更多页数的PDF文件因加密后再次解密读取中频繁导致白屏或闪退的问题。
- 目前我的开发环境NDK是紧跟官方更新版本,那么就需要自身降到r17以下的版本,所以我将r17之前的旧版本NDK都下载下来,以防对应NDK版本对C99、C11的兼容各不同所遇到自身不能解决的问题而“卡壳”。
总结(改变我所能改变的,舍弃我所能舍弃的)
- 项目这次PDF频繁出现白屏与闪退的原因,是因为解密算法读取规则方式不严谨导致。如果是文件资源较小的PDF,则会很少几率出现,若随着PDF资源的变大,解密读取的不规则的问题也就会被放大化。
- 总的来说,难预计、难估期的项目大多数还是因为自身能力欠缺,简明的说就是缺知识。
- 解决任何问题都不能一厢情愿的去莽撞处理,要多考虑到时间、环境、人力、以及可行性等多种因素。也要多会借助外部的力量,毕竟独木不成林。
上一篇: 历史上的 "最佳拍档"。
下一篇: 独木难成林 005
推荐阅读
-
整理更新 MuPDF 和修改相关 C 代码过程中遇到的问题。
-
什么是数据库事物?为什么需要数据库事物,事物有哪些特征?事物的隔离级别是什么?-1.什么是数据库事务? 1.事务是作为一个逻辑单元执行的一系列操作。一个逻辑工作单元必须具备四个属性,即ACID(原子性、一致性、隔离性和持久性)属性,只有这样才能成为事务: 原子性 2.事务必须是一个原子工作单元;它的数据修改要么全部执行,要么全部不执行。 一致性 3.事务完成时,所有数据必须保持一致。在相关数据库中,所有规则都必须适用于事务的修改,以保持所有数据的完整性。事务结束时,所有内部数据结构(如 B 树索引或双向链接表)必须正确无误。 隔离 4.并发事务的修改必须与其他并发事务的修改隔离。一个事务会在另一个并发事务修改之前或之后查看某一状态下的数据,而不会查看中间状态下的数据。这就是所谓的可序列化,因为它允许重新加载起始数据和重放一系列事务,从而使数据最终处于与原始事务执行时相同的状态。 持久性 5.事务完成后,它对系统的影响是永久性的。即使在系统发生故障的情况下,修改也会保留。 2. 为什么需要数据库事物,事物有哪些特征? 事物对数据库的作用是对数据进行一系列操作,要么全部成功,要么全部失败,防止出现中间状态,确保数据库中的数据始终处于正确、和谐的状态。 特征:原子性、一致性、隔离性、持久性,以及其他特征 原子性(Atomicity):所有操作在事务开始后,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出现错误时,会回滚到事务开始前的状态,所有操作就像没有发生一样。也就是说,事务是一个不可分割的整体,就像化学中的原子一样,是物质的基本单位。 一致性(Consistency):在事务开始之前和结束之后,数据库的完整性约束都没有被破坏。例如,如果 A 转钱给 B,A 不可能扣除这笔钱,但 B 却没有收到这笔钱。 隔离:在同一时间内,只允许一个事务请求相同的数据,不同事务之间没有干扰。例如,甲正在从一张银行卡上取款,在甲取款过程结束之前,乙不能向这张卡转账。 持久性(耐用性):事务完成后,事务对数据库的所有更新都将保存到数据库中,无法回滚 3.事务的隔离级别有哪些? 数据库事务有四种隔离级别,从低到高分别是未提交读取(Read uncommitted)、已提交读取(Read committed)、可重复读取(Repeatable read)、可序列化(Serializable)。此外,事务的并发操作中可能会出现脏读、不可重复读、幽灵读等情况。事务并发问题 脏读:事务 A 读取事务 B 更新的数据,然后事务 B 回滚操作,那么事务 A 读取的数据就是脏数据。 不可重复读取:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取期间更新并提交数据,导致事务 A 多次读取同一数据时结果不一致。 幻影读取:系统管理员 A 将数据库中所有学生的具体分数改为 ABCDE 等级,但系统管理员 B 在此时插入了具体分数的记录,当系统管理员 A 更改结束后发现仍有一条记录未被更改,仿佛发生了幻觉,这称为幻影读取。 小结:不可重复读和幻读容易混淆,不可重复读侧重于修改,幻读侧重于增删。解决不可重复读问题只需锁定满足条件的行,解决幻读问题则需要锁定表 MySQL 事务隔离级别
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面