FMX开发android和iOS越来越稳定完善,期待delphi能够有更多新人接力。
下面说说在FMX开发中APP经常莫名其妙闪退的一些原因:
1)线程访问UI: 优先排查最常见的线程访问UI控件没有加同步保护,下面是相应的建议;
为了避免界面UI因为一些耗时较长的调用(网络访问,阻塞请求等)导致APP提示无响应,建议在各种用户交互操作中使用以下代码,此代码几乎是各种操作通用的调用方法:
procedure TfmDemo.Button1Click(Sender: TObject);
begin
TThread.CreateAnonymousThread(procedure begin
//Do something here...
//update UI
TThread.Synchronize(nil, procedure begin
Button1.Text := 'done';
end);
end).Start;
end;
2)内存泄漏:检查变量的创建和释放,访问等相关代码,排除空指针,无效变量,无效类实例等调用。
这里没什么好说的,枯燥无聊的排查,让人沉浸其中且烦闷的过程,不过也有以下方法:
- A.可以将程序先输出为Windows平台,设置 System.ReportMemoryLeaksOnShutdown := True; 来开启内存泄漏检测(当然也有其他内存泄漏检测组件包,大同小异),实际中需详细排查,此方法排查跨平台代码。
- B.在可调试条件下,直接调试并在出错的地方看CallStack是最直观的,可一层一层剥开看到出错的地方(程序员做的最多就是且应该是这种操作了)
- C.输出运行日志,比较基础的操作就是利用Log.D或者其他自主实现的记录日志方法,在程序各个函数的入口出口处记录参数和结果(在VCL中经常使用OutputDebugString的人会比较习惯)
- D.单元测试:相信这个方法90%以上的人做不到,当然也包括本人,不过大公司规范严格的会有此类要求,接触过的人会比较习惯
3)运行时函数的非法参数:系统时间日期格式问题
这个问题多半出现在将时间字符串转成TDateTime过程中,该动作在取数据库时间,在显示日期时间等功能中经常使用,
假如使用了StrToDateTime,EncodeDate,EncodeTime等RTL函数,因为一些习惯问题,很多人认为时间字符串格式默认就是“YYYY-MM-DD hh:mm:ss”,但是很多时候Windows系统Android系统,甚至iOS系统都不是这个格式,具体有系统设置区域语言控制,这时候调用上面的RTL函数会出现异常错误,而在手机上此类错误会导致直接闪退。
解决办法就是先设置TFormatSettings中与时间格式相关的字段,可参考如下:
function StrToDateTime(sDateTime: string; const Default: TDateTime): TDateTime; var aFormatSettings: TFormatSettings; begin aFormatSettings := TFormatSettings.Create; aFormatSettings.TimeSeparator := ':'; aFormatSettings.ShortTimeFormat := 'hh:mm:ss'; aFormatSettings.LongTimeFormat := 'hh:mm:ss'; if sDateTime.Contains('/') then begin aFormatSettings.ShortDateFormat := 'YYYY/MM/DD'; aFormatSettings.LongDateFormat := 'YYYY/MM/DD'; aFormatSettings.DateSeparator := '/'; end else begin aFormatSettings.ShortDateFormat := 'YYYY-MM-DD'; aFormatSettings.LongDateFormat := 'YYYY-MM-DD'; aFormatSettings.DateSeparator := '-'; end; Result := SysUtils.StrToDateTimeDef(sDateTime, Default, aFormatSettings); end;
4)编写习惯错误:新版本的变量释放用法,当然这里只是说明一下在传统VCL转到FMX后经常碰到问题时的大概率原因。
在VCL开发中有些人会经常使用destroy来释放创建的类示例(Create和Destroy或Free一一对应),
这也是因为在VCL中的Free内部其实也是调用Destroy,导致很多人释放时直接调用Destroy。
而Delphi FMX在开发Android APP时,经常出现APP崩溃闪退也有很大可能性是因Destroy导致的。
因为在FMX,全局变量和类变量采用ARC引用计数来释放,而计数的增加很多时候是隐藏在代码背后自动添加的,属于FMX框架运行时所需的计数量。
对于Delphi语法来说 Create 应该和 Free(或FreeAndNil)一一对应来使用。
但在FMX中,经常有人反馈占用内存越来越大,因为Free并不能直接释放内存,因此Delphi又提供了DisposeOf来释放(并不是强制释放,仍与计数有关),
我们在开发中如确定实例已经不再被需要了,可以调用DisposeOf来释放,正常情况都可以直接释放。
简单调用如下
procedure TfmDemo.DoProcess();
var
aObj: TDemoObj;
begin
aObj:=TDemoObj.Create;
try
//.....
finally
aObj.DisposeOf;
// 建议标准做法: aObj.Free; aObj := nil;
// FreeAndNil目前由于调用时会增加新的引用计数,所以不推荐,若以后Delphi有针对该方法优化则可直接调用
end;
end;
为什么在VCL调用Destroy可以正常,而在FMX中却容易出错
注:当然并不一定会出错,在非类方法的纯函数(面向过程)中仍可正常调用(只是实际中测试正常,无法保证随着以后的升级更新后仍然正常)
通过Delphi提供的TObject源码可知,若直接调用Destroy,则会导致类示例的Destroy方法被执行并直接释放内存,也就相当于“内容”被擦除了,但程序中某些地方仍会使用实例变量,且变量的引用计数仍在,所以当FMX框架内部在引用计数为0时的自动释放变量所指的实例过程中,会再次擦除不存在的实例“内容”,相当于访问不存在的非法内存,就会导致崩溃,且调试中大部分无法直接定义代码位置,若没有该经验,最终觉得毫无头绪。。。
就像上面说的,全局变量或类变量会有引用计数的影响,而且需要注意的是,匿名函数在编译时也会被当作匿名类处理,其局部变量也会转换为类变量。
当我们编译匿名函数(最常用的是匿名线程中的各种调用)时,若有未使用变量的警告,提示内容一般是:
[DCC Hint] Unit1.pas(56): H2164 Variable 'aSSS' is declared but never used in '(null).[0]'
这里的 (null).[0] 就是编译器自动创建的匿名函数类名称。
补充:
根据EMB的说法,未来将会取消ARC,所以目前代码的编写习惯,建议是保持Create和Free 一一对应即可,没必要纠结内存增长,而归根结底内存增长其实是由于编码不规范导致的,不能认为是编译器或者FMX框架的BUG,变量的交叉关联引用都会导致ARC无法正常释放,所以改善编码质量,局部变量及时释放;全局变量或类变量避免重复创建,合理释放即可;
个人认为保持代码的编译兼容性最为重要,对于移交代码,跨平台编译,多环境编译非常有用。
5)补充一个非常冷门的原因,代码优化BUG:常出现在APP使用DEBUG编译时运行正常,但使用RELEASE编译运行则崩溃
这种情况下,首先确认你的水平处于中高以上时,对自己的代码已经非常确定无错误,但实际中编译出来仍然会出现崩溃,可尝试关闭代码优化,DCC的编译器BUG是存在的,特别是对代码优化的处理上,每个版本的更新都会被报告或多或少几个编译器BUG。 所以关闭代码优化,可能可以解决崩溃问题!
需要注意的是,使用DEBUG和使用RELEASE编译使用的dcu不同,甚至代码中可能使用{$IFDEF DEBUG}做编译开关导致处理代码不同,因此需要优先排查此部分代码是否存在BUG。