今天和Delphi高手刘麻子讨论了一下移动平台上生存期自管理容易导致的一些问题.有几点体会
在移动平台上Delphi的对象是生存期自管理的,这和早期版本和现有的PC版本有很大不同.
大部分时间我们原来的代码不会有问题.我们原来的手动释放代码在移动平台也不会有问题,因为TObject.Free方法在移动平台上就是个摆设.
procedure TObject . Free;
begin
// under ARC, this method isn't actually called since the compiler translates
// the call to be a mere nil assignment to the instance variable, which then calls _InstClear
{$IFNDEF AUTOREFCOUNT}
if Self <> nil then
Destroy;
{ $ENDIF }
end ;
|
真正调用到对象的Destroy的只有编译器自动产生的代码.
一般有两种情况.
1. :=也就是赋值,会调用旧的变量内容的__ObjRelease减少引用计数,会调用新变量内容的__ObjAddRef增加引用计数.
2.当变量销毁时,如局部变量离开函数恢复栈的时候,如成员变量在对象的销毁时候.会调用变量内容的__ObjRelease减少引用计数.
当引用计数减到0的时候会调用到Destroy或FreeInstance真正的释放对象.
另外还有一个特殊的Attribute名字叫WeakAttribute.如果变量被[weak]修饰,那么编译器在处理这个变量的时候不会调用该变量内容的__ObjAddRef和__ObjRelease.
一切看起来都挺顺利的.但是我们的旧代码移植到移动平台上还是会有对象在我们不希望它释放的时候被释放了,导致地址出错等莫名其妙的问题.
为什么呢,因为Delphi是一种支持指针的语言.但是指针和靠引用计数生存期自管理简直是水火不相容.
比如我们把对象赋值给指针保管,然后旧的对象变量销毁的时候对象有可能就被释放.我们再用指针操作该对象的时候地址已经是无效的了.
比如我们使用Classes单元的TList而不是泛型的TList<T>来保管对象,这种代码在旧的Delphi代码中非常常见.
Classes单元中的TList是个指针的容器,就非常容易导致上面说的问题.
怎么办呢?
有一个无赖的办法.
对象在赋值给指针之前手动调用一下__ObjAddRef方法,给他的引用计数加一.这样编译器产生的生存期自管理代码就永远不会把该对象的引用计数减到零来释放了.
然后在指针变量销毁前,或者赋值别的内容前手工调用一下__ObjRelease.
比如给上面说的TList增加对象前,为防止对象被释放要调用一下对象的__ObjAddRef,响应的,在恰当的时候要调用一下__ObjRelease.
本来以为这个方法有点无赖了,发现易博龙本身RTL中也是这样的做的.嘿嘿.
class function TEncoding . GetANSI: TEncoding;
var
LEncoding: TEncoding;
begin
if FANSIEncoding = nil then
begin
LEncoding := TMBCSEncoding . Create(GetACP, 0 , 0 );
if AtomicCmpExchange( Pointer (FANSIEncoding), Pointer (LEncoding), nil ) <> nil then
LEncoding . Free;
{$IFDEF AUTOREFCOUNT}
FANSIEncoding . __ObjAddRef; //<------瞧,易博龙和我们一样"无赖"
{$ENDIF AUTOREFCOUNT}
end ;
Result := FANSIEncoding;
end ;
class destructor TEncoding . Destroy;
begin
FreeEncodings; //销毁代码
end ;
class procedure TEncoding . FreeEncodings;
begin
FreeAndNil(FANSIEncoding);
FreeAndNil(FASCIIEncoding);
FreeAndNil(FUTF7Encoding);
FreeAndNil(FUTF8Encoding);
FreeAndNil(FUnicodeEncoding);
FreeAndNil(FBigEndianUnicodeEncoding);
end ;
procedure FreeAndNil( var Obj);
{$IF not Defined(AUTOREFCOUNT)}
var
Temp: TObject;
begin
Temp := TObject(Obj);
Pointer (Obj) := nil ;
Temp . Free;
end ;
{ $ELSE }
begin
TObject(Obj) := nil ; //这个等价于TObject(Obj).__ObjRelease; Obj:=nil;
end ;
{ $ENDIF }
|
最后说一句,手动调用__ObjAddRef/__ObjRelease是没办法的时候做的.说句祝福:希望大家在移植旧代码到移动平台的时候都不要遇到对象生存期自管理和旧代码的兼容问题,不要被迫使用这个办法.