使用 Delphi 创作可跨 Android 和 iOS 运行的多语种应用程序
使用 Delphi 的 FireMonkey 框架,开发跨平台的程序,可以做到一个源码,编译成 Windows, Mac OS, Android, iOS,甚至是 Linux 上面运行的程序。
简单说,就是可以开发 Android 和 iOS 的 APP,同时代码不修改也可以运行在 Windows 上和 Mac OS 上面。
如果你发布的 APP 需要给各国用户使用,需要考虑多语言的支持。我之前有博客文章写过如何实现多语言支持:
假期有时间写代码:FireMonkey 之多语言(TLang)
问题简述
数据库的中文内容,在手机系统语言非中文时,显示乱码。此现象出现在 Android 和 iOS 上面。Windows 上面没有此问题。
一些使用多语言框架需要注意的细节
1. 设计期,一个 TLabel 或者其它控件,只要它有 AutoTranslate 属性,将这个属性设置为 True,拿 TLabel 作为例子,在设计期设置好它的 Text 属性为特定的字符串,比如【温度】,然后在语言前述文章里面提到的语言文件里面有【温度=Temperature】这样的翻译内容,则程序运行时,切换了语言,则 TLabel 的 Text 显示的文字内,自动变化为 Temperature 这个翻译后的文字。
2. 运行期:假设运行期,在一个 Button1.OnClick 事件里面,有代码:
Label1.Text := '温度';
则它仍然显示汉字的温度,不会自动变成英文,即便当前的语言设置是英文。
这里有两种办法解决这个问题。
2.1. 办法一:赋值语句,调用翻译函数,将翻译后的字符串赋值给 Label1,代码:
Label1.Text := Translate('温度');
这个办法,前述文章提到过。
2.2. 办法二:触发自动翻译,代码:
Label1.Text := '温度';
TStyleManager.UpdateScenes; //---- 执行这行代码,Label1.Text 会自动翻译。前提是设置 Label1.AutoTranslate 属性为 True;
//---- TStyleManager.UpdateScenes; 这个方法需要 uses Fmx.Styles 单元。
一个更大的问题:数据库内容的多语言显示
在 FireMonkey 框架底下,我们可以使用 SQLite 数据库作为 APP 程序的本地数据库,将其内容显示呈现在界面上。
假设数据库里面有个表,里面有一些设计期就写入的内容,在用户使用 APP 时,这些内容会显示在 APP 界面上。同时我们希望用户使用不同语言的时候,同样的内容,要显示为对应的语言。
数据库的内容要显示到界面,这里可以采用 Delphi 提供的 LiveBindings 方法,将界面上的 ListView 或者 Label 等控件绑定到数据库指定的字段上。
数据库游标滚动后显示的内容
当数据库的游标滚动,在使用 LiveBindings 技术的情况下,Label 显示的内容自动跟随显示当前 DataSet 的游标所在记录的对应字段的内容,这时候,显示的是数据库里面的原始的字段内容字符串,并不是我们希望的当前用户设置的语言对应的文字。这里其实就是相当于运行期在为 Label1.Text 赋值。此时,在数据库游标滚动后,需要增加一行代码:TStyleManager.UpdateScenes; 让多语言框架触发自动翻译,然后用户看到的就是对数据库的内容翻译后的内容。
数据库内容在不同语言的系统里面显示乱码,自动翻译失败
在 FireMonkey 里面,数据库控件,我喜欢使用 FireDAC 那套。
如果字符串字段的类型是 TStringField,里面的内容是中文值,则:
1. 在 Windows 底下,无论系统语言是中文还是英文,都没有问题;
2. 在 Android 和 iOS 底下,系统语言是中文没有问题;系统语言是英文,则显示出来的文字是乱码,当然也就会翻译失败。因为我们的翻译内容是【温度=Temperature】,是乱码,而不是【温度】,使得自动翻译找不到对应的 Key 值,在找不到语言文件里面的对应的 Key 值得情况下,自动翻译将给出没翻译的原始内容,而这个内容是乱码。因此,显示乱码。
解决这个问题:字段类型为 TWideStringField,则上述问题解决。
假设本地数据库采用 SQLite,要怎样做才能让 TFdQuery 这个 DataSet 的字段类型是 TWideStringFied ?
在 SQLite 数据库里面,设计的时候,存储字符串的字段,如果类型是 VarChar 则对应该字段的 Delphi 的 DataSet 里面的字段类型是 TStringField;
如果 SQLite 的字段类型是 NVarChar 字段,则 Delphi 里面对应的字段类型是 TWideStringField;
因此,如果数据库的内容想要在多种系统语言下面正确显示,简单说,要在英文为系统语言的手机上,正确显示数据库内容而不是显示乱码,使用 SQLite 作为手机端本地数据库来使用的话,SQLite 的字段类型应该是 NVarChar 而不是 VarChar。
做到上述几点,数据库内容是中文,也能正确将中文显示在英文系统语言的手机上。
但是,如果数据库内容是中文,在英文系统语言的手机上想要显示为翻译后的英文,那么,就需要在运行期,在数据库滚动后(DataSet.Next 或类似的操作),调用一次 TStyleManager.UpdateScenes;
当然,重复一下:想要自动翻译,前提是使用 LoadLangFromStrings 函数事先加载对应的翻译词条。
总结:
1. 使用 LoadLangFromStrings 加载对应语言的 Key=Value 的翻译词条;
2. SQLite 的字段使用 NVarChar 而不是 VarChar 类型,对应的 Delphi 的 DataSet 里面的字段类型是 TWideStringField 而不是 TStringField;
3. 运行期改变的界面元素的内容后,比如使用 LiveBindings 绑定 TLabel.Text 到数据库的 DataSet 的某个字段,当 DataSet 滚动后,调用一次 TStyleManager.UpdateScenes; 触发自动翻译。
推荐阅读
-
使用 Delphi 创作可跨 Android 和 iOS 运行的多语种应用程序
-
Grid++Report 锐浪报表开发常见问题解答集锦-报表设计 问:怎样在设计时打印预览报表? 答:为了及时查看报表的设计效果,Grid++Report 报表设计应用程序提供了四种查看视图:普通视图、页面视图、预览视图与查询视图。通过窗口下边的 Tab 按钮可以在四种视图中任意切换。在预览视图中查看报表的打印预览效果,在查询视图中查看报表的查询显示效果。如果在报表的记录集提供了数据源连接串与查询 SQL,在进入预览视图与查询视图时会利用数据源连接串与查询 SQL 从数据源中自动取数,否则 Grid++Report 将自动生成模拟数据进行模拟打印预览与查询显示。注意:在预览视图与查询视图中看到的报表运行结果有可能与在你程序中的最终运行结果有差异,因为在报表的生成过程中我们可以在程序中对报表的生成行为进行一定的控制。 问:怎样用 Grid++Report 设计交叉表? 答:Grid++Report 没有提供专门实现交叉表的功能,其它的报表构件提供的交叉表功能一般也比较死板和功能有限。利用 Grid++Report 的编程接口可以做出灵活多变,功能丰富的交叉表。示例程序 CrossTab 就是一个实现交叉表的例子程序,认真领会此例子程序,你就可以做出自己想要各种交叉表,并能提取一些共用代码,便于重复使用。 问:怎样设置整个报表的缺省字体? 答:设置报表主对象的字体属性,也就是设置了整个报表的缺省字体。如果改变报表主对象的字体属性,则没有专门的设置字体属性的子对象的字体属性也跟随改变。同样每个报表节与明细网格也有字体属性,他们的字体属性也就是其拥有的子对象的缺省字体。 问:怎样在打印时限制一页的输出行数? 答:设定明细网格的内容行的‘每页行数(RowsPerPage)’属性即可。另外要注意‘调节行高(AdjustRowHeight)’属性值:为真时根据页面的输出高度自动调整行的高度,使整个页面的输出区域充满。为假时按设计时的高度输出行。 问:怎样显示中文大写金额? 答:将对象的“格式(Format)”属性设为 “$$” 及可,可以设置格式的对象有:字段(IGRField)、参数(IGRParameter)、系统变量(IGRSystemVarBox)与综合文字框(IGRMemoBox),其中综合文字框是在报表式上设格式。 问:能否实现自定义纸张与票据打印? 答:Grid++Report 完全支持自定义纸张的打印,只要在报表设定时在页面设置中选定自定义纸张,并指定准确的纸张尺寸。当然要在最终输出时得道合适的打印结果,输出打印机必须支持自定义纸张打印。Windows2000/XP/2003 操作系统上可以在打印机上定义自定义纸张,也可以采用这种方式实现自定义纸张打印。 问:怎样实现 0 值不打印? 答:直接设置格式串就可以,在“数字格式”设置对话框中选定“0 不显示”,就会得到合适的格式串。也可以通过直接录入格式串来指定 0 不显示,但格式串必须符合 Grid++Report 的规定格式。另一种实现办法是在报表获取明细记录数据时,在 BeforePostRecord 事件中将值为零的字段设为空,调用字段的 Clear 方法将字段置为空。 问:怎样实现多栏报表? 答:在明细网格上设‘页栏数(PageColumnCount)’属性值大于 1 即可。通过 Grid++Report 的“页栏输出顺序”还可以指定多栏报表的输出顺序是“先从上到下”还是“先从左到右”。 问:如何实现票据套打? 答:Grid++Report 为实现票据套打做了很多专门的安排:报表设计器提供了页面设计模式,按照设定的纸张尺寸显示设计面板,如果将空白票据的扫描图设为设计背景图,在定位报表内容的输出位置会非常方便。报表部件可以设定打印类别,非套打输出的内容在套打打印模式下就不会输出。 问:Grid++Report 有没有横向分页功能? 答:回答是肯定的,在列的总宽度超过打印页面的输出宽度时,Grid++Report 可以另起新页输出剩余的列,如果左边存在锁定列,锁定列可以在后面的新页中重复输出,这样可以保证关键数据列在每一页都有输出。仔细体会 Grid++Report 提供的多种打印适应策略,选用最合适的方式。Grid++Report 的多种打印适应策略为开发动态报表提供了很好的支持。 问:怎样实现报表本页小计功能? 答:定义一个报表分组,将本分组定义为页分组,在本分组的分组头与分组尾上定义统计。页分组就是在每页产生一个分组项,在每页的上端与下端都会分别显示页分组的分组头与分组尾,页分组不用定义分组依据字段。 报表运行 问:怎样与数据库建立连接? 答:如果在设计报表时指定了数据集的数据源连接串与查询 SQL 语句,Grid++Report 采用拉模式直接从数据源取得报表数据,Grid++Report 利用 OLE DB 从数据源取数,OLE DB 提供了广泛的数据源操作能力。如果 Grid++Report 的数据来源采用推模式,即 Grid++Report 不直接与数据库建立连接,各种编程语言/平台都提供了很好的数据库连接方式,并且易于操作,应用程序在报表主对象(IGridppReport)的 FetchRecord 事件中将数据传入,例子程序提供了各种编程语言填入数据的通用方法,对C++Builder 和 Delphi 还进行了专门的包装,直接关联 TDataSet 对象也可以将 TDataSet 对象中的数据传给报表。 问:打印时能否对打印纸张进行自适应?支持表格的折行打印吗? 答:Grid++Report 在打印时采用多种适应策略,通过设置明细网格(IGRDetailGrid)的‘打印策略(PrintAdaptMethod)’属性指定打印策略。(1)丢弃:按设计时列的宽度输出,超出范围的内容不显示。(2)绕行:按设计时列的宽度输出,如果在当前行不能完整输出,则另起新行进行输出。(3)缩放适应:对所有列的输出宽度进行按比例地缩放,使总宽度等于页面的输出宽度。(4)缩小适应:如果列的总宽度小于页面的输出宽度,对所有列的输出宽度进行按比例地缩小,使总宽度等于页面的输出宽度。(5)横向分页:超范围的列在新页中输出。(6)横向分页并重复锁定列。 问:如何改变缺省打印预览窗口的窗口标题? 答:改变报表主对象的‘标题(Title)’属性即可。 问:利用集合对象的编程接口取子对象的接口引用,但不是自己期望的结果。 答:Grid++Report中所有集合对象的下标索引都是从 1 开始,另按对象的名称查找对象的接口引用时,名称字符是不区分大小写的。 问:怎样在运行时控制报表中各个对象的可见性?即怎样在运行时显示或隐藏对象? 答:在报表主对象(GridppReport)的 SectionFormat 事件中设定相应报表子对象的可见(Visible)属性即可。 问:报表主对象重新载入数据,设计器中为什么没有反映新载入的数据? 答:应调用 IGRDesigner 的 Reload 方法。 问:怎样实现不进入打印预览界面,直接将报表打印出来?
-
【Netty】「萌新入门」(七)ByteBuf 的性能优化-堆内存的分配和释放都是由 Java 虚拟机自动管理的,这意味着它们可以快速地被分配和释放,但是也会产生一些开销。 直接内存需要手动分配和释放,因为它由操作系统管理,这使得分配和释放的速度更快,但是也需要更多的系统资源。 另外,直接内存可以映射到本地文件中,这对于需要频繁读写文件的应用程序非常有用。 此外,直接内存还可以避免在使用 NIO 进行网络传输时发生数据拷贝的情况。在使用传统的 I/O 时,数据必须先从文件或网络中读取到堆内存中,然后再从堆内存中复制到直接缓冲区中,最后再通过 SocketChannel 发送到网络中。而使用直接缓冲区时,数据可以直接从文件或网络中读取到直接缓冲区中,并且可以直接从直接缓冲区中发送到网络中,避免了不必要的数据拷贝和内存分配。 通过 ByteBufAllocator.DEFAULT.directBuffer 方法来创建基于直接内存的 ByteBuf: ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); 通过 ByteBufAllocator.DEFAULT.heapBuffer 方法来创建基于堆内存的 ByteBuf: ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); 注意: 直接内存是一种特殊的内存分配方式,可以通过在堆外申请内存来避免 JVM 堆内存的限制,从而提高读写性能和降低 GC 压力。但是,直接内存的创建和销毁代价昂贵,因此需要慎重使用。 此外,由于直接内存不受 JVM 垃圾回收的管理,我们需要主动释放这部分内存,否则会造成内存泄漏。通常情况下,可以使用 ByteBuffer.clear 方法来释放直接内存中的数据,或者使用 ByteBuffer.cleaner 方法来手动释放直接内存空间。 测试代码: public static void testCreateByteBuf { ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16); System.out.println(buf.getClass); ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); System.out.println(heapBuf.getClass); ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); System.out.println(directBuf.getClass); } 运行结果: class io.netty.buffer.PooledUnsafeDirectByteBuf class io.netty.buffer.PooledUnsafeHeapByteBuf class io.netty.buffer.PooledUnsafeDirectByteBuf 池化技术 在 Netty 中,池化技术指的是通过对象池来重用已经创建的对象,从而避免了频繁地创建和销毁对象,这种技术可以提高系统的性能和可伸缩性。 通过设置 VM options,来决定池化功能是否开启: -Dio.netty.allocator.type={unpooled|pooled} 在 Netty 4.1 版本以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现; 这里我们使用非池化功能进行测试,依旧使用的是上面的测试代码 testCreateByteBuf,运行结果如下所示: class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf 可以看到,ByteBuf 类由 PooledUnsafeDirectByteBuf 变成了 UnpooledUnsafeDirectByteBuf; 在没有池化的情况下,每次使用都需要创建新的 ByteBuf 实例,这个操作会涉及到内存的分配和初始化,如果是直接内存则代价更为昂贵,而且频繁的内存分配也可能导致内存碎片问题,增加 GC 压力。 使用池化技术可以避免频繁内存分配带来的开销,并且重用池中的 ByteBuf 实例,减少了内存占用和内存碎片问题。另外,池化技术还可以采用类似 jemalloc 的内存分配算法,进一步提升分配效率。 在高并发环境下,池化技术的优点更加明显,因为内存的分配和释放都是比较耗时的操作,频繁的内存分配和释放会导致系统性能下降,甚至可能出现内存溢出的风险。使用池化技术可以将内存分配和释放的操作集中到预先分配的池中,从而有效地降低系统的内存开销和风险。 内存释放 当在 Netty 中使用 ByteBuf 来处理数据时,需要特别注意内存回收问题。 Netty 提供了不同类型的 ByteBuf 实现,包括堆内存(JVM 内存)实现 UnpooledHeapByteBuf 和堆外内存(直接内存)实现 UnpooledDirectByteBuf,以及池化技术实现的 PooledByteBuf 及其子类。 UnpooledHeapByteBuf:通过 Java 的垃圾回收机制来自动回收内存; UnpooledDirectByteBuf:由于 JVM 的垃圾回收机制无法管理这些内存,因此需要手动调用 release 方法来释放内存; PooledByteBuf:使用了池化机制,需要更复杂的规则来回收内存; 由于池化技术的特殊性质,释放 PooledByteBuf 对象所使用的内存并不是立即被回收的,而是被放入一个内存池中,待下次分配内存时再次使用。因此,释放 PooledByteBuf 对象的内存可能会延迟到后续的某个时间点。为了避免内存泄漏和占用过多内存,我们需要根据实际情况来设置池化技术的相关参数,以便及时回收内存; Netty 采用了引用计数法来控制 ByteBuf 对象的内存回收,在博文 「源码解析」ByteBuf 的引用计数机制 中将会通过解读源码的形式对 ByteBuf 的引用计数法进行深入理解; 每个 ByteBuf 对象被创建时,都会初始化为1,表示该对象的初始计数为1。 在使用 ByteBuf 对象过程中,如果当前 handler 已经使用完该对象,需要通过调用 release 方法将计数减1,当计数为0时,底层内存会被回收,该对象也就被销毁了。此时即使 ByteBuf 对象还在,其各个方法均无法正常使用。 但是,如果当前 handler 还需要继续使用该对象,可以通过调用 retain 方法将计数加1,这样即使其他 handler 已经调用了 release 方法,该对象的内存仍然不会被回收。这种机制可以有效地避免了内存泄漏和意外访问已经释放的内存的情况。 一般来说,应该尽可能地保证 retain 和 release 方法成对出现,以确保计数正确。