質問

2018年04月13日 16時53分
  • VBA(Outlook)のメモリー解放に関して 【解決】

情シスのオープンナレッジ『Syszo』サービス終了のお知らせ

質問

お世話になっております。
タイトル通り、Outlook の VBA(自作)のメモリー解放に関してです。
作成した VBA で、メモリー解放が上手く出来ておらず、メモリーが大幅に費やされています。
ただし、具体的にどの記述が誤っているのか分からず、ご指摘して頂きたいと考えた次第です orz

■ VBA の概要
Outlook の中の、指定したメールを複製する。
複製する際に、別途 Excel で作成しておいたメールアドレス一覧を元に To を設定する。

■ メモリーの解放が上手く出来ていない箇所
下記の Function で、メモリーを食いまくっています(メモリー解放できていない)。
なお、Loop 処理で、下記 Function を 5000回ほど呼び出しています。

Private Function fnReplicateMail(ByVal parastrTempMail As String _
, ByVal para
strMailAddress As String _
, ByVal para_strAtena As String _
) As Integer

Dim objNewItem As Outlook.MailItem
Dim strHtmlBody As String
Dim dblHtmlCnt As Double

fnReplicateMail = -1
On Error GoTo ErrorHandler

Set objNewItem = Outlook.Application.CreateItemFromTemplate(parastrTempMail)
With objNewItem
'内容設定(To/CC/BCC/タイトル/本文/開封確認要求)
.To = para
strMailAddress
.CC = .CC
.BCC = .BCC
.Subject = .Subject
If Me.chkAtena.Value = True Then
If .BodyFormat = olFormatHTML Then
strHtmlBody = UCase(.HTMLBody)
dblHtmlCnt = InStr(strHtmlBody, UCase("")

.HTMLBody = Mid(.HTMLBody, 1, dblHtmlCnt) & parastrAtena & "" & Mid(.HTMLBody, dblHtmlCnt + 1)
Else
.Body = para
strAtena & vbCrLf & vbCrLf & .Body
End If
End If
.ReadReceiptRequested = .ReadReceiptRequested
'送信
.Send
End With

'オブジェクトを明示的にクリアする
Set objNewItem = Nothing

fnReplicateMail = 0
Exit Function
ErrorHandler:
Call MsgBox("エラーが発生しました。情報システム部へ連絡下さい(fnReplicateMail)" & vbCrLf & Err.Description, vbOKOnly)
Set objNewItem = Nothing
End Function

■自分で疑っている事
・オブジェクトの生成時に、うっかり暗黙で記述している箇所がある?
・そのせいで、オブジェクトへの参照が上手く消えていない?
・結果として、この Function を抜けても、オブジェクトへの参照のみが消えて、メモリが開放されてない?

新年度早々ではありますが、どうぞよろしくお願いいたします。

6件の回答があります

回答

Set objNewItem = Outlook.Application.CreateItemFromTemplate(parastrTempMail)

Dim outlookapp As Outlook.Application
Set objNewItem = outlookapp.CreateItemFromTemplate(parastrTempMail)
(中略)
Set outlookapp = Nothing

とかしたら変わりませんかね?
それでダメなら、outlookappをグローバル変数にして割り当て1回しかされないようにするとか。

With句の中で使ってるオブジェクトも暗黙的に参照してるかもしれないですし、いちいち、CreateItemFromTemplateするのやめて、項目を上書いて回す、って手もありかも。

2018年04月09日 09時14分

回答

sysjojoさま

アドバイスありがとうございます!
下記を試してみたのですが、使用メモリが増えていくのは止められませんでした。
ですが、少し見えて来た事もあります。ありがとうございます!!

■ Outlook の Application オブジェクトを明示的に準備

Dim outlookapp As Outlook.Application
Set objNewItem = outlookapp.CreateItemFromTemplate(parastrTempMail)
(中略)
Set outlookapp = Nothing

■ outlookapp をグローバル変数にして、ループに入る前に Set し、ループを抜けた後に Nothing

■ With を使わずに(objNewItem.Send みたいに)記述

■ objNewItem を毎度生成しない

以下、なんとなく見えて来た事と、気になった事。

● Send メソッド実行後、そのままオブジェクトを参照しようとすると「削除されている」とのエラーが発生。
 この事から、そもそも Set Nothing をする前に、オブジェクトへの参照が切れてしまっている??
 (しかし、そんな事あるんですかね?)

● Form 上のボタンをクリックすると、一連の処理が流れるようになっているのですが、
 Click イベントを抜けると(Form に制御が戻ると)、メモリが多少解放される

● outlookapp に Set Nothing する前に、Quit メソッドを実行してみると、
 Form や VBAのエディタ がバッサリ終了するが、Outlook は起動したままの状態。

引き続き、色々調べつつ試してみようと思います。
もしまた何かあれば、アドバイス頂けると幸いです。

回答

● Send メソッド実行後、そのままオブジェクトを参照しようとすると「削除されている」とのエラーが発生。
 この事から、そもそも Set Nothing をする前に、オブジェクトへの参照が切れてしまっている??
 (しかし、そんな事あるんですかね?)

は、生成されたMailItemがOutlookのメール送信フォームに準ずるもの、ということであればSendで参照が切れる気持ちはわかりますけど(送信フォームで送信ボタン押したら、送信フォーム自体が閉じられるのと同じ意味で。)、MSDNでそういった記述は見つけきれなかったです。

Outlook.Applicationを使ってるのはOutlookの送信履歴に残(したか)ったりするんですかね?
メール送信するだけなら、CDO.Messageって手もありますけど。

2018年04月09日 15時30分

回答

Outlook.Applicationを使ってるのはOutlookの送信履歴に残(したか)ったりするんですかね?

失礼しました。
OutlookのVBAでしたね。

Dim outlookapp As Outlook.Application

これを

Dim outlookapp As this.Application

にしてもQuitしたとき閉じないですかね?
(Excel VBA等から)Outlook.ApplicationをCreateObjectしていれば、QuitでOutlookごと落ちる気がしますが、起動しているOutlookの中でOutlook.Applicationと宣言した時に自分自身になっているかよくわからなかったので、thisで明示的に自分であることを示せば、落ちるかも?と。

あと、

● outlookapp に Set Nothing する前に、Quit メソッドを実行してみると、
 Form や VBAのエディタ がバッサリ終了するが、Outlook は起動したままの状態。

Set hogehoge = Nothingした瞬間メモリ開放されるわけではないのと、hogehogeが別のところで参照されていると、Set hogehoge = Nothingしても効かないので、使い捨てできないオブジェクトでメモリリーク解消させるのはめんどくさいかも。

Form 上のボタンをクリックすると、一連の処理が流れるようになっているのですが、
 Click イベントを抜けると(Form に制御が戻ると)、メモリが多少解放される

でメモリが解放されているのは、Set hogehoge = Nothingが効いたオブジェクト分空いてるんでしょうね。

2018年04月09日 16時08分

回答

sysjojo さま

再度コメントありがとうございます!

メール送信するだけなら、CDO.Messageって手もありますけど。

メモリ大量消費がこのまま解消しなければ、その方法に切り替えようかとも思っていました。
(サーバーからのアラート代わりに使ってるのですが、便利ですよね!)

ただ、今回の VBA は、ユーザー部門からの頼まれモノなので、
出来る限り Outlook の中で完結させたいな、と。
別の仕組みで動かすと、そこの説明から始めないといけないのと、
諸々の保守の工数を考えると、恐ろしくて…!!

で、ようやく原因が特定出来ました。
やはり「Sendメソッド」が原因でした。
それを回避する為に、

.Send

を、以下のように変更しました。
(一連の流れの中では、メールを作成するのみ…に変更しました)

'送信トレイ
Dim objOutboxFolder As Outlook.Folder
Set objOutboxFolder = Application.GetNamespace("MAPI").GetDefaultFolder(4)
'メール保存
.Save
'保存したメールを送信トレイに移動させる
.Move objOutboxFolder

その上で、別途、下記のマクロで送信を実施。

'選択したメールを送信
Public Sub SendMail()

 Dim objMailItem As Outlook.MailItem

 For Each objMailItem In ActiveExplorer.Selection
  objMailItem.Send
 Next

 Set objMailItem = Nothing

End Sub

これで、メモリリークを回避する事が出来ました!

なお、下記は、色々調べて分かった事です。

・Outlook の VBA の中での「Set objOutlookApp = CreateObject("Outlook.Application")」は不要
 Application オブジェクトをそのまま使用してOK

・With ~ End With の方がメモリの消費を抑えられる

今回、私1人で考えているだけでは、きっと答えに辿り着く前に嫌気がさしてたと思います。
sysjojo さまが色々アドバイスをして下さったおかげで、解決まで辿り着けたと思っています。
本当にありがとうございました。

回答

なにかのきっかけにでもなってればよかったです。

2018年04月16日 09時09分

あなたもコメントしましょう!