RO34 - I Cannot ROmember
作者
Brian Moelk (www.brainendeavor.com)
RemObjects
提示
:
我们相信本文是正确的
,
但我们不做任何保证
.
在此感谢
Henrick
写的文章
,
很高兴在此发表
.
介绍
RemObjects
是功能强大可扩展的远程框架
;
但是当考虑远程对象的
allocation(内存
分配
)/deallocation(内存释放)/serialization(
序列化
)
问题时让人摸不到边际
.
本文将讨论
RO
内核澄清这些问题
.
Delphi
开发者可以很幸运的使用
RemObjects/DataAbstract
创建
n
层服务
.
使用
RO,
我们通过在
Service Builder
工具重创建服务定义库以导向的架构设计我们服务
.
其中之一就是我们可以自定义结构体类型作为输入输出参数
. RO
使用其定义创建代码所以可以序列化他们并在网络上传输
.
这能够轻松做到
,RO
最大程度的提升了我们的
n
层应用
.
不幸的是还有一些细节需要我们自己管理
;
如
.
清除我们自己分配的内存
.
同
Delphi. RemObjects
尽力做到像本地调用一样实现远程调用
,
然而还是有些不同
,
稍候会讨论
.
这里有一条首要的规则
:
总在客户端释放内存不能在服务端释放
.
为了清晰
,
我们讨论的代码都是我们写过的
;RO
在服务端会为我们自动释放对象
,
所以不会引起内存泄漏
.
已经有关于这个问答的
FAQ
在线问答了
.
但是我想在这里讨论更多的细节
,
因为有很多细节使我们必须熟悉理解的规则
,
我们还有特别小心的使用
in/out (
或
var)
参数
.
OK,
第一件事情是说明所有从
TROComplexType
继承来的
RO
结构体
. TROComplexType
可以将对象数据序列化在网上传输
;
例如
RO
可以序列化从
TROComplexType
继承的对象
.
Use The Source, always use The Source.
查看代码发生了什么最好的方法是调用服务方法并跟踪
RO
结构体或
TROComplexType
对象的内存申请和释放
.
我已经建立了一个简单的项目
project
帮助我们查看这个过程
.
这里是我们要测试执行的服务端接口和方法
:
{ MyStruct }
MyStruct = class(TROComplexType)
private
fStructId: Integer;
fStructData: String;
public
procedure
Assign(iSource: TPersistent); override;
published
property
StructId:Integer read fStructId write fStructId;
property StructData:String read fStructData write fStructData;
end;
[...]
{ ITestStructsService }
ITestStructsService = interface
[
'{7FD55CCE-01E8-4F4A-B64B-02380B31A8FC}'
]
procedure ProcessStruct(const aStruct: MyStruct);
procedure OutStruct(out aStruct: MyStruct);
function GetStruct: MyStruct;
procedure VarStruct(var aStruct: MyStruct);
end;
ITestStructsService
接口结合了所有要使用的参数类型
: in, out,
返回值和
in/out.
为了简单我们只考虑
SOAP
消息
, TROSOAPMessage,
当然对
TROBinMessage
也应用同样的规则
.
虽然我们只考虑结构体
,
但是执行规则同样适用于从
TROComplexType
继承来的
RO
数组
.
通过客户端代码跟踪
...
Ok,
第一个要查看的方式是
ProcessStruct
方法
.
我们再客户端直接创建结构体填充数据并作为参数调用后释放
:
procedure
TClientForm.ButtonProcessStructClick(Sender: TObject);
var
aStruct: MyStruct;
begin
aStruct := MyStruct.Create;
try
aStruct.StructId :=
0
;
aStruct.StructData :=
'Client ProcessStruct'
;
MemoOutput.Lines.Add(
'invoke ProcessStruct'
);
OutputStruct(aStruct);
(RORemoteService as ITestStructsService).ProcessStruct(aStruct);
OutputStruct(aStruct);
finally
FreeAndNil(aStruct);
end;
end
;
客户端代码与我们在
Delphi
中调用需要常量参数的方法一样
.
客户端负责管理传输作为参数的对象生存期
.
RO
已经做了所有的繁琐工作
;
我们看看实际发生了什么
. ROmemberLibrary_Intf.pas
文件中我们可以看到
TTestStructsService_Proxy
代理类为每个接口方法序列化结构体以便在网络传输
.
代理类代码中我们对调用
__Message.Read
和
__Message.Write
的部分感兴趣
.
我们可以看到消息的序列号和反序列化
.
它们负责为调用格式化消息
.
我们看看
TROMessage.Read
和
TROMessage.Write:
procedure
TROMessage.Read(const aName: string;
aTypeInfo: PTypeInfo;
var Ptr; Attributes: TParamAttributes);
begin
Serializer.Read(aName, aTypeInfo, Ptr);
if Assigned(fOnReadMessageParameter) then
fOnReadMessageParameter(Self, aName,
aTypeInfo, pointer(Ptr),
Attributes);
end
;
procedure
TROMessage.Write(const aName: string;
aTypeInfo: PTypeInfo;
const Ptr; Attributes: TParamAttributes);
begin
if
Assigned(fOnWriteMessageParameter) then
fOnWriteMessageParameter(Self, aName,
aTypeInfo, pointer(Ptr),
Attributes);
Serializer.Write(aName, aTypeInfo, Ptr);
end
;
这里使用
TROMessage
的
Serializer
属性
.
方法
:
function
CreateSerializer : TROSerializer; virtual; abstract;
生成消息相对于的
TROSerializer.TROSerializer
实例化后在
TROMessage
构造时将其赋给
TROMessage.fSerializer
成员
.
在
TROSOAPMessage
中生成
TROXMLSerializer.
function
TROSOAPMessage.CreateSerializer : TROSerializer;
begin
result := TROXMLSerializer.Create(pointer(fMessageNode));
end
;
Ok,
现在我们已经看到
TROMessage
和
TROSerializer
直接的关系了
,
现在再考虑特定的
ProcessStruct
案例
. TROSerializer.Write
约定
MyStruct
的
TypeInfo
信息指定了他的
tkClass
并以此来调用
TROSerializer.WriteObject.
WriteObject
是一个复杂的方法这里不再详述
,
但是它使用了
RTTI
读取
MyStruct
中的值并写出来
.
注意这里没有创建和释放代码
,TROXMLSerializer.BeginWriteObject
中也没有创建和释放代码
.
所以客户端简单的序列化了我们传入的常量结构体参数
.
我们负责分配并做相应的释放
.
在服务端代码中跟踪
...
服务端发生了什么
?
服务端通过
ROmemberLibrary_Invk.pas
文件中的
TTestStructsService_Invoker.Invoke_ProcessStruct
方法处理客户端请求
.
procedure
TTestStructsService_Invoker.Invoke_ProcessStruct
(const __Instance:IInterface;
const __Message:IROMessage;
const __Transport:IROTransport;
out __oResponseOptions:TROResponseOptions);
{ procedure ProcessStruct(const aStruct: MyStruct); }
var
aStruct: ROmemberLibrary_Intf.MyStruct;
__lObjectDisposer: TROObjectDisposer;
begin
aStruct := nil;
try
__Message.Read(
'aStruct'
,
TypeInfo(ROmemberLibrary_Intf.MyStruct),
aStruct, []);
(__Instance as ITestStructsService).ProcessStruct(aStruct);
__Message.Initialize(__Transport,
'ROmemberLibrary'
,
'TestStructsService'
,
'ProcessStructResponse'
);
__Message.Finalize;
__oResponseOptions := [roNoResponse];
finally
__lObjectDisposer := TROObjectDisposer.Create(__Instance);
try
__lObjectDisposer.Add(aStruct);
finally
__lObjectDisposer.Free();
end;
end;
end
;
我们可以看到结构体从流中读出
,
因此读取也使用了序列化器
.
我们可以在
try..finally
块中发现
TROObjectDisposer;
这个逻辑接口释放了结构体引用的对象
.
猜测是正确的
.
这意味着什么
?
这说明
__Message.Read
序列化器实例化
MyStruct.
序列化器的
Read
方法测定
MyStruct
实例是一个对象类型
,
所以调用了
ReadObject.
随后调用
BeginReadObject.
在
TROXMLSerializer.BeginReadObject
中执行如下构造函数
:
procedure
TROXMLSerializer.BeginReadObject
[...]
anObject := TROComplexTypeClass(aClass).Create;
[...]
end
;
这里实例被构造出来
,
所以我们需要
TROObjectDisposer. RO
架构实例化了这个结构体让后又会为你在服务端释放它
.
所以我们看这个方法就已经知道
RO
实例化结构体的所有信息
,
使用这个实例后又为我们自动释放
.
序列化器是关键
.
当从流中读取了结构体后序列化器创建一个实例
;
如果要向流中写入就使用已存在的实例
.
这有很大的意义
.
OutStruct
和
GetStruct
下一个方法我们看看
OutStruct.
我们已经认识了
RO
的序列化器
,
下面我们看看生成的代码
TTestStructsService_Proxy.OutStruct,
它明确了我们的职责
:
procedure
TTestStructsService_Proxy.OutStruct(out aStruct: MyStruct);
var
__request, __response : TMemoryStream;
begin
aStruct := nil;
__request := TMemoryStream.Create;
__response := TMemoryStream.Create;
try
__Message.Initialize(__TransportChannel,
'ROmemberLibrary'
, __InterfaceName,
'OutStruct'
);
__Message.Finalize;
__Message.WriteToStream(__request);
__TransportChannel.Dispatch(__request, __response);
__Message.ReadFromStream(__response);
__Message.Read(
'aStruct'
, TypeInfo(ROmemberLibrary_Intf.MyStruct),
aStruct, []);
finally
__request.Free;
__response.Free;
end
end
;
这里序列化器要求读结构体
;
这意味着它将要创建一个实例
.
注意它不会释放这个实例
,
所以这个客户端的责任
.
GetStruct
很像
OutStruct
方法
,
但是使用
aStruct
的方式不同
,
它用于返回
.
如果我们查看生成的
TTestStructsService_Proxy.GetStruct
代码可以发现
.
留心结构体做
var
参数
!!!
最后看
VarStruct
方法
.
这是非常重要的部分
,
我们看生成的代码
TTestStructsService_Proxy.VarStruct:
procedure
TTestStructsService_Proxy.VarStruct(var aStruct: MyStruct);
var
__request, __response : TMemoryStream;
begin
__request := TMemoryStream.Create;
__response := TMemoryStream.Create;
try
__Message.Initialize(__TransportChannel,
'ROmemberLibrary'
,
__InterfaceName,
'VarStruct'
);
__Message.Write(
'aStruct'
, TypeInfo(ROmemberLibrary_Intf.MyStruct),
aStruct, []);
__Message.Finalize;
__Message.WriteToStream(__request);
__TransportChannel.Dispatch(__request, __response);
__Message.ReadFromStream(__response);
__Message.Read(
'aStruct'
, TypeInfo(ROmemberLibrary_Intf.MyStruct),
aStruct, []);
finally
__request.Free;
__response.Free;
end
end
;
代码使用
uses __Message.Write
和
__Message.Read,
这以为着它使用
aStruct
参数传入的实例
,
同时也会创建一个新的实例
aStruct
.
客户端如何写代码就很重要了
.
正确的书写客户端代码保证没有内存泄露的方式如下
:
procedure
TClientForm.ButtonVarStructClick(Sender: TObject);
var
aInStruct, aOutStruct: MyStruct;
begin
aInStruct := MyStruct.Create;
try
aInStruct.StructId :=
2
;
aInStruct.StructData :=
'Client VarStruct'
;
MemoOutput.Lines.Add(
'invoke VarStruct'
);
OutputStruct(aInStruct);
aOutStruct := aInStruct;
(RORemoteService as ITestStructsService).VarStruct(aOutStruct);
OutputStruct(aOutStruct);
finally
FreeAndNil(aOutStruct);
FreeAndNil(aInStruct);
end;
end
;
我们必须释放
aInStruct
和
aOutStruct
.
为给代理传入对象我们必须实例化
aInStruct.
代理将之写入流中并做调用
.
代理代码然后实例化一个新的
MyStruct,
重写
var
参数的引用
.
这意味着客户端必要跟踪什么传入了代理
,
什么又从代理中传出
,
并将两者都释放掉
.
为什么
RO
做这些呢
?
想象一下如果
RO
为我们释放其中的一个
.
问题是我们可能还要在其他的地方使用这些对象的实例
.
最后
,
远程调用与我将在下一小节中介绍的局部调用有明显的不同
.
在我看来
,
理想的解决方案是使用接口应用计数器来管理生存期
.
所以我们使用
IROComplexType
替换
TROComplexType,
我们所有的结构体都基于接口
.
问题是
RO
为了支持没有
RTTI
接口支持的
delphi5.
这样无法使用
RTTI
序列化它们
;
然而我们可以通过其它方法绕过
RTTI.
如果我们在服务端查看调用
VarStruct
的代码我们可以看到如下内容
:
procedure
TTestStructsService_Invoker.Invoke_VarStruct
(const __Instance:IInterface;
const __Message:IROMessage;
const __Transport:IROTransport;
out __oResponseOptions:TROResponseOptions);
{ procedure VarStruct(var aStruct: MyStruct); }
var
aStruct: ROmemberLibrary_Intf.MyStruct;
__in_aStruct: ROmemberLibrary_Intf.MyStruct;
__lObjectDisposer: TROObjectDisposer;
begin
aStruct := nil;
__in_aStruct := nil;
try
__Message.Read(
'aStruct'
, TypeInfo(ROmemberLibrary_Intf.MyStruct),
aStruct, []);
__in_aStruct := aStruct;
(__Instance as ITestStructsService).VarStruct(aStruct);
__Message.Initialize(__Transport,
'ROmemberLibrary'
,
'TestStructsService'
,
'VarStructResponse'
);
__Message.Write(
'aStruct'
, TypeInfo(ROmemberLibrary_Intf.MyStruct),
aStruct, []);
__Message.Finalize;
finally
__lObjectDisposer := TROObjectDisposer.Create(__Instance);
try
__lObjectDisposer.Add(__in_aStruct);
__lObjectDisposer.Add(aStruct);
finally
__lObjectDisposer.Free();
end;
end;
end
;
我们可以看到如何通过
TROObjectDisposer
释放
__in_aStruct
和
the aStruct
实例
.
这正是我们要做客户端保证没有内存泄露的做法
.
抛开
Delphi
话题展示本地和远程调用的区别
...
要真正的理解相对于本地调用的
RO
调用以及为什么必须释放两个结构体实例
,
我们需要看看一些使用
Var
参数的本地调用
.
如果你对我们在
RO
的
Var
参数调用时为什么必须写那些代码的解释很满意
,
你可以越过本小节
.
如果你想去对比远程和本地调用请继续阅读
.
为什么你要在
RO
中使用
in/out
和
var
参数呢
?
这是我们向服务端传递结构体和从服务端得到更新修改的结构体的唯一方式
.
本地调用有更多弹性达到这个目标
,
我们将看到每一种选择
.
这对于在
RO
中不能传递非常量结构体参数的情况没有什么参考价值
.
如果要在本地实现
ITestStructsService.VarStruct
的功能
,
我将考虑使用结构体替代类
(
记住
RO
中的结构体是从
TROComplexType
类继承下来的
).
这是因为在本地调用时结构体与
RO
结构体提供了同样等级的功能
.
因而我们使用如下方法测试
:
type
PMyRecord = ^TMyRecord;
TMyRecord = record
RecId: integer;
RecData: shortstring;
end;
[...]
procedure
ChangeTheRecord(var aRec: TMyRecord);
begin
aRec.RecId := aRec.RecId +
1
;
aRec.RecData := aRec.RecData +
' Changed'
;
end
;
[...]
procedure
TClientForm.ButtonLocalStackRecordClick(Sender: TObject);
var
LocalRecord: TMyRecord;
begin
LocalRecord.RecId :=
1
;
LocalRecord.RecData :=
'Foo'
;
OutputRecord(
'before stack record'
, LocalRecord);
ChangeTheRecord(LocalRecord);
//value of LocalRecord is: Id = 2 and Data = 'Foo Changed'
OutputRecord(
'after stack record'
, LocalRecord);
OutputSeparator;
end
;
在本例中
,
由于使用了分配在客户端方法
(
如
TClientForm.ButtonLocalStackRecordClick)
栈上的本地记录结构体
,Delphi
为我们处理所有的内存分配和释放
.
这里
ChangeTheRecord
方法的参数必须使用
var
否则过程就不能更新结构体
.
但是我们可以通过不同的方式使用结构体
;
加入我们在堆上分配结构体
:
procedure
ChangeTheHeapRecord(aRec: PMyRecord);
begin
aRec.RecId := aRec.RecId +
1
;
aRec.RecData := aRec.RecData +
' Changed'
;
end
;
procedure
ChangeTheHeapRecordVar(var aRec: PMyRecord);
begin
aRec.RecId := aRec.RecId +
1
;
aRec.RecData := aRec.RecData +
' Changed'
;
end
;
[...]
procedure
TClientForm.ButtonLocalHeapRecordClick(Sender: TObject);
var
HeapRecord: PMyRecord;
begin
GetMem(HeapRecord, SizeOf(TMyRecord));
try
HeapRecord.RecId :=
1
;
HeapRecord.RecData :=
'Foo'
;
OutputRecord(
'before heap record'
, HeapRecord^);
//all three do the same thing...so use any one of them..
//ChangeTheRecord(HeapRecord^);
ChangeTheHeapRecord(HeapRecord);
//ChangeTheHeapRecordVar(HeapRecord);
//value of HeapRecord is: Id = 2 and Data = 'Foo Changed'
OutputRecord(
'after heap record'
, HeapRecord^);
finally
FreeMem(HeapRecord);
end;
OutputSeparator;
end
;
三个方法都正常执行
: ChangeTheRecord, ChangeTheHeapRecord
和
ChangeTheHeapRecordVar .
我们传递堆上的引用就可以改变值
(
不管是否在参数前放置
var
标识
), ChangeTheHeapRecord
和
ChangeTheHeapRecordVar
实现了同等功能
.
或者我们可以给
ChangeTheRecord
传递忽略
var
标识的结构参数
,
而执行效果却和基于栈使用
var
时的相同
.
唯一不同的地方就是我们必须要在堆上分配和释放内存
.
但是注意我们只做一次分配和释放操作
.
更完整的
,
我们看在本地传递
MyStruct
对象的代码是什么样的
:
procedure
ChangeTheStruct(aStruct: MyStruct);
begin
aStruct.StructId := aStruct.StructId +
1
;
aStruct.StructData := aStruct.StructData +
' Changed'
;
end
;
procedure
ChangeTheStructVar(var aStruct: MyStruct);
begin
if
(Assigned(aStruct) = False) then
begin
aStruct := MyStruct.Create;
end;
aStruct.StructId := aStruct.StructId +
1
;
aStruct.StructData := aStruct.StructData +
' Changed'
;
end
;
[...]
procedure
TClientForm.ButtonLocalObjectClick(Sender: TObject);
var
aStruct: MyStruct;
begin
aStruct := MyStruct.Create;
try
aStruct.StructId :=
1
;
aStruct.StructData :=
'Foo'
;
OutputStruct(
'invoke Local Object'
, aStruct);
ChangeTheStruct(aStruct);
OutputStruct(
'after Local Object'
, aStruct);
finally
FreeAndNil(aStruct);
end;
OutputSeparator;
end
;
procedure
TClientForm.ButtonLocalObjectVarClick(Sender: TObject);
var
aStruct: MyStruct;
begin
aStruct := nil;
try
//OR replace above two lines with:
//aStruct := MyStruct.Create;
//try
// aStruct.StructId := 1;
// aStruct.StructData := 'Foo';
OutputStruct(
'before Local Object var'
, aStruct);
ChangeTheStructVar(aStruct);
OutputStruct(
'after Local Object var'
, aStruct);
finally
FreeAndNil(aStruct);
end;
OutputSeparator;
end
;
又一次清楚的看到我们只做一次分配和释放结构体的操作
.
因为我们在本地调用并传递的是对象的引用
,
方法改变参数将直接修改对象本身
,
因此要达到我们目的
var
标识不是必须的
.
在对结构体做
var
标识的情况下我们可以传递
nil
或一个分配内存的实例
.
更多时候在方法中使用
var
对象引用参数的意图是作为
"Source"
方法
.
这时如果将
nil
值传递给方法
,
将为调用者分配一个新实例
.
同
ChangeTheStructVar .
但是注意及时是这种情况我们以只释放一次实例
.
当然我们分配和释放对象的方法和原因完全依赖于我们调用的方法
/
过程
.
这里特别约定了我们将在方法中直接改变调用者提供的结构
/
对象
.
也可以将修改结果返回调用者
.
这里有多种约定
, ChangeTheStructVar
与
"source"
方法不同
.RO
的
var
参数的约定也有不同意义
.
它可以使用如下代码模仿本地调用
:
procedure
ChangeTheStructVarLikeRO(var aStruct: MyStruct);
var
InStruct: MyStruct;
begin
InStruct := aStruct;
//this simulates the __Message.Read construction on the client
//after the server method is invoked
aStruct := MyStruct.Create;
//this simulates the __Message.Read deserialization
aStruct.Assign(InStruct);
aStruct.StructId := aStruct.StructId +
1
;
aStruct.StructData := aStruct.StructData +
' Changed'
;
end
;
[...]
procedure
TClientForm.ButtonLocalObjectLikeROClick(Sender: TObject);
var
aInStruct, aOutStruct: MyStruct;
begin
aInStruct := MyStruct.Create;
try
aInStruct.StructId :=
1
;
aInStruct.StructData :=
'Foo'
;
OutputStruct(
'before Local Object var like RO'
, aInStruct);
aOutStruct := aInStruct;
ChangeTheStructVarLikeRO(aOutStruct);
OutputStruct(
'after Local Object var like RO (aInStruct)'
,
aInStruct);
OutputStruct(
'after Local Object var like RO (aOutStruct)'
,
aOutStruct);
finally
FreeAndNil(aInStruct);
FreeAndNil(aOutStruct);
end;
OutputSeparator;
end
;
正如你在代码中看到的
,
结构比必须在
TClientForm.ButtonLocalObjectLikeROClick
方法中释放两次
.
这时因为
ChangeTheStructVarLikeRO
执行的约定很像
RO
执行
var
参数的远程调用约定
.
结论
RO
是令人惊奇的架构
,
但是使用它是必须要知道一些告诫
.
为了不会写出内存泄漏的代码
,
理解其中隐含的内容是很重要的
.
我希望本文可以起到引导作用
,
并且学到我写作时学到的东西
.