Optymalizacje w Delphi (przyklad - część I)
stg
Niniejszy artykuł został wprawdzie napisany w oparciu o Delphi 5, jednak część tutaj opisanych technik można z powodzeniem zastosować i w innych wersjach Delphi.
W przypadku gdy pisana aplikacja nie jest wybitnie złożona, można w znaczący sposób zmniejszyć jej rozmiar nie używając VCL. Zacznijmy jednak od początku. Napiszemy prosty program, który ma za zadanie wyswietlać co sekundę liczbę oraz całkowity rozmiar plików znajdujących się w koszu. W tym celu tworzymy nowy projekt, a następnie wrzucamy na forme dwa komponenty TLabel, jeden TButton oraz TTimer. Oto kod programu:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls;
type
PSHQueryRBInfo = ^TSHQueryRBInfo;
TSHQueryRBInfo = packed record
cbSize: DWORD;
i64Size: Int64;
i64NumItems: Int64;
end;
type
TForm1 = class(TForm)
Label1: TLabel;
Label2: TLabel;
Button1: TButton;
Timer1: TTimer;
procedure Timer1Timer(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
DllVersion: Integer;
SHQueryRBInfo: TSHQueryRBInfo;
implementation
{$R *.DFM}
function SHQueryRecycleBin(szRootPath: PChar; SHQueryRBInfo: PSHQueryRBInfo): HResult; stdcall; external 'shell32.dll' Name 'SHQueryRecycleBinA';
function GetDllVersion(FileName: String): Integer;
var
InfoSize, Wnd: DWORD;
VerBuf: Pointer;
FI: PVSFixedFileInfo;
VerSize: DWORD;
begin
Result := 0;
InfoSize := GetFileVersionInfoSize(PChar(FileName), Wnd);
if InfoSize <> 0 then
begin
GetMem(VerBuf, InfoSize);
try
if GetFileVersionInfo(PChar(FileName), Wnd, InfoSize, VerBuf) then
if VerQueryValue(VerBuf, '\', Pointer(FI), VerSize) then
Result := FI.dwFileVersionMS;
finally
FreeMem(VerBuf);
end;
end;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
DllVersion := GetDllVersion(PChar('shell32.dll'));
if DllVersion >= $00040048 then
begin
FillChar(SHQueryRBInfo, SizeOf(TSHQueryRBInfo), #0);
SHQueryRBInfo.cbSize := SizeOf(TSHQueryRBInfo);
SHQueryRecycleBin(nil, @SHQueryRBInfo);
Label1.Caption := 'Ca³kowity rozmiar plików w koszu: ' + IntToStr(SHQueryRBInfo.i64Size) + ' bajtów';
Label2.Caption := 'Liczba plików w koszu: ' + IntToStr(SHQueryRBInfo.i64NumItems);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Timer1.Enabled := False;
Close;
end;
end.
Po skompilowaniu program zajmuje 298 kB. Sporo. Spróbujmy nieco inaczej. Na stronie http://kolmck.net/ znajduje się paczka KOL&MCK którą pobieramy i instalujemy. Przy tworzeniu nowego projektu postępujemy zgodnie ze wskazówkami znajdującymi się w pobranej paczce. Oprócz wspomnianego pliku ściągamy również zamienniki System, SysUtils oraz Classes (zakładka System), które wrzucamy np. do katalogu z KOL&MCK. W Delphi w polu Conditional Defines (zakladka Directories/Conditionals) do parametru KOL_MCK
dodajemy SMALLEST_CODE
. Na formie umieszczamy: 2x TKOLLabel oraz po jednym TKOLButton i TKOLTimer. Część odpowiadająca za funkcjonowanie programu wygląda analogicznie jak w przypadku VCL:
{ KOL MCK } // Do not remove this line!
{$DEFINE KOL_MCK}
unit Unit1;
interface
{$IFDEF KOL_MCK}
uses Windows, Messages, KOL {$IFNDEF KOL_MCK}, mirror, Classes, Controls, mckCtrls, mckObjs, Graphics {$ENDIF (place your units here->)};
{$ELSE}
{$I uses.inc}
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;
{$ENDIF}
type
PSHQueryRBInfo = ^TSHQueryRBInfo;
TSHQueryRBInfo = packed record
cbSize: DWORD;
i64Size: I64;
i64NumItems: I64;
end;
type
{$IFDEF KOL_MCK}
{$I MCKfakeClasses.inc}
{$IFDEF KOLCLASSES} {$I TForm1class.inc} {$ELSE OBJECTS} PForm1 = ^TForm1; {$ENDIF CLASSES/OBJECTS}
{$IFDEF KOLCLASSES}{$I TForm1.inc}{$ELSE} TForm1 = object(TObj) {$ENDIF}
Form: PControl;
{$ELSE not_KOL_MCK}
TForm1 = class(TForm)
{$ENDIF KOL_MCK}
KOLProject1: TKOLProject;
KOLForm1: TKOLForm;
Timer1: TKOLTimer;
Button1: TKOLButton;
Label1: TKOLLabel;
Label2: TKOLLabel;
procedure Timer1Timer(Sender: PObj);
procedure Button1Click(Sender: PObj);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1 {$IFDEF KOL_MCK} : PForm1 {$ELSE} : TForm1 {$ENDIF} ;
DllVersion: Integer;
SHQueryRBInfo: TSHQueryRBInfo;
{$IFDEF KOL_MCK}
procedure NewForm1( var Result: PForm1; AParent: PControl );
{$ENDIF}
implementation
{$IFNDEF KOL_MCK} {$R *.DFM} {$ENDIF}
{$IFDEF KOL_MCK}
{$I Unit1_1.inc}
{$ENDIF}
function SHQueryRecycleBin(szRootPath: PChar; SHQueryRBInfo: PSHQueryRBInfo): HResult; stdcall; external 'shell32.dll' Name 'SHQueryRecycleBinA';
function GetDllVersion(FileName: String): Integer;
var
InfoSize, Wnd: DWORD;
VerBuf: Pointer;
FI: PVSFixedFileInfo;
VerSize: DWORD;
begin
Result := 0;
InfoSize := GetFileVersionInfoSize(PChar(FileName), Wnd);
if InfoSize <> 0 then
begin
GetMem(VerBuf, InfoSize);
try
if GetFileVersionInfo(PChar(FileName), Wnd, InfoSize, VerBuf) then
if VerQueryValue(VerBuf, '\', Pointer(FI), VerSize) then
Result := FI.dwFileVersionMS;
finally
FreeMem(VerBuf);
end;
end;
end;
procedure TForm1.Timer1Timer(Sender: PObj);
begin
DllVersion := GetDllVersion(PChar('shell32.dll'));
if DllVersion >= $00040048 then
begin
FillChar(SHQueryRBInfo, SizeOf(TSHQueryRBInfo), #0);
SHQueryRBInfo.cbSize := SizeOf(TSHQueryRBInfo);
SHQueryRecycleBin(nil, @SHQueryRBInfo);
Label1.Caption := 'Ca³kowity rozmiar plików w koszu: ' + Num2Bytes(Int64_2Double(SHQueryRBInfo.i64Size));
Label2.Caption := 'Liczba plików w koszu: ' + Int64_2Str(SHQueryRBInfo.i64NumItems);
end;
end;
procedure TForm1.Button1Click(Sender: PObj);
begin
Timer1.Enabled := False;
PostQuitMessage(0);
end;
end.
Program możemy również napisać nie uzywając MCK (graficznego interfejsu dla KOL). Źródło programu dla 'czystego' KOL:
program KOL_Trash;
uses Windows, KOL;
type
PSHQueryRBInfo = ^TSHQueryRBInfo;
TSHQueryRBInfo = packed record
cbSize: DWORD;
i64Size: I64;
i64NumItems: I64;
end;
var
DllVersion: Integer;
SHQueryRBInfo: TSHQueryRBInfo;
W, B, L1, L2 : PControl;
T : PTimer;
function SHQueryRecycleBin(szRootPath: PChar; SHQueryRBInfo: PSHQueryRBInfo): HResult; stdcall; external 'shell32.dll' Name 'SHQueryRecycleBinA';
function GetDllVersion(FileName: String): Integer;
var
InfoSize, Wnd: DWORD;
VerBuf: Pointer;
FI: PVSFixedFileInfo;
VerSize: DWORD;
begin
Result := 0;
InfoSize := GetFileVersionInfoSize(PChar(FileName), Wnd);
if InfoSize <> 0 then
begin
GetMem(VerBuf, InfoSize);
try
if GetFileVersionInfo(PChar(FileName), Wnd, InfoSize, VerBuf) then
if VerQueryValue(VerBuf, '\', Pointer(FI), VerSize) then
Result := FI.dwFileVersionMS;
finally
FreeMem(VerBuf);
end;
end;
end;
procedure TimerTick(Dummy: Pointer; Sender: PTimer);
begin
DllVersion := GetDllVersion(PChar('shell32.dll'));
if DllVersion >= $00040048 then
begin
FillChar(SHQueryRBInfo, SizeOf(TSHQueryRBInfo), #0);
SHQueryRBInfo.cbSize := SizeOf(TSHQueryRBInfo);
SHQueryRecycleBin(nil, @SHQueryRBInfo);
L1.Caption := 'Ca³kowity rozmiar plików w koszu: ' + Num2Bytes(Int64_2Double(SHQueryRBInfo.i64Size));
L2.Caption := 'Liczba plików w koszu: ' + Int64_2Str(SHQueryRBInfo.i64NumItems);
end;
end;
procedure CloseClick(Dummy: Pointer; Sender: PControl);
begin
T.Enabled := False;
PostQuitMessage(0);
end;
begin
W := NewForm(Applet, 'Kosz').SetClientSize(350,250).CenterOnParent;
L1 := NewLabel(W, '').SetPosition(40,40).AutoSize(True);
L2 := NewLabel(W, '').SetPosition(40,80).AutoSize(True);
B := NewButton(W, 'Zamknij').SetPosition(100,180).SetSize(133, 33);
B.OnClick := TOnEvent(MakeMethod(nil, @CloseClick));
T := NewTimer(1000);
T.OnTimer := TOnEvent(MakeMethod(nil, @TimerTick));
T.Enabled := True;
Run(W);
end.
Oprócz standardowego kodu dodana została funkcja Num2Bytes, która zwraca nam rozmiar plików kosza w przejrzystym formacie. Kompilujemy. Tym razem program zajmuje 18,5 kB. Jest dobrze, ale może być jeszcze lepiej. W tym momencie z pomocą przychodzi WinAPI:
program API_Trash;
uses Windows, Messages;
type
I64 = record Lo, Hi: DWORD;
end;
type
PSHQueryRBInfo = ^TSHQueryRBInfo;
TSHQueryRBInfo = packed record
cbSize: DWORD;
i64Size: I64;
i64NumItems: I64;
end;
var
Wnd: TWndClass;
Msg: TMsg;
L1, L2: HWND;
function SHQueryRecycleBin(szRootPath: PChar; SHQueryRBInfo: PSHQueryRBInfo): HResult; stdcall; external 'shell32.dll' Name 'SHQueryRecycleBinA';
function StrFormatByteSize64(dw: I64; szBuf: PChar; uiBufSize: UINT): PChar; stdcall; external 'shlwapi.dll' name 'StrFormatByteSize64A';
function GetDllVersion(FileName: string): Integer;
var
InfoSize, Wnd: DWORD;
VerBuf: Pointer;
FI: PVSFixedFileInfo;
VerSize: DWORD;
begin
Result := 0;
InfoSize := GetFileVersionInfoSize(PChar(FileName), Wnd);
if InfoSize <> 0 then
begin
GetMem(VerBuf, InfoSize);
try
if GetFileVersionInfo(PChar(FileName), Wnd, InfoSize, VerBuf) then
if VerQueryValue(VerBuf, '\', Pointer(FI), VerSize) then
Result := FI.dwFileVersionMS;
finally
FreeMem(VerBuf);
end;
end;
end;
function WndProc(Wnd: HWND; uMsg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT; stdcall;
var
Buffer: array[0..255] of Char;
DllVersion: integer;
SHQueryRBInfo: TSHQueryRBInfo;
begin
Result := 0;
case uMsg of
WM_CREATE:
begin
CreateWindow('BUTTON', 'Zamknij', WS_CHILD or WS_VISIBLE, 100, 180, 133, 33, Wnd, 14, hInstance, nil);
L1 := CreateWindow('STATIC', '', WS_CHILD or WS_VISIBLE, 40, 40, 300, 25, Wnd, 0, hInstance, nil);
L2 := CreateWindow('STATIC', '', WS_CHILD or WS_VISIBLE, 40, 80, 300, 25, Wnd, 0, hInstance, nil);
DllVersion := GetDllVersion(PChar('shell32.dll'));
if DllVersion >= $00040048 then SetTimer(Wnd, 1, 1000, nil);
end;
WM_TIMER:
begin
FillChar(SHQueryRBInfo, SizeOf(TSHQueryRBInfo), #0);
SHQueryRBInfo.cbSize := SizeOf(TSHQueryRBInfo);
SHQueryRecycleBin(nil, @SHQueryRBInfo);
StrFormatByteSize64(SHQueryRBInfo.i64Size, Buffer, 255);
SetWindowText(L1, PChar('Ca³kowity rozmiar plików w koszu: ' + Buffer));
wvsprintf(Buffer, '%lu', @SHQueryRBInfo.i64NumItems);
SetWindowText(L2, PChar('Liczba plików w koszu: ' + Buffer));
end;
WM_COMMAND: if wPar = 14 then
begin
KillTimer(Wnd,1);
PostQuitMessage(0);
end;
WM_DESTROY: PostQuitMessage(0);
else Result := DefWindowProc(Wnd, uMsg, wPar, lPar);
end;
end;
begin
with Wnd do
begin
lpfnWndProc := @WndProc;
hInstance := hInstance;
lpszClassName := 'XPU';
hbrBackground := COLOR_WINDOW;
hIcon := LoadIcon(0, IDI_APPLICATION);
hCursor := LoadCursor(0, IDC_ARROW);
end;
RegisterClass(Wnd);
CreateWindow('XPU', 'Kosz', WS_VISIBLE or WS_TILEDWINDOW, (GetSystemMetrics(SM_CXSCREEN) div 2)-350, (GetSystemMetrics(SM_CYSCREEN) div 2)-250, 350, 250, 0, 0, hInstance, NIL);
while GetMessage(msg, 0, 0, 0) do
begin
TranslateMessage(msg);
DispatchMessage(msg);
end;
end.
Zaczynaliśmy od początkowych 298 kB, a być może kończymy 'zabawe' na 8 kB. 'Być może', ponieważ prawdziwi maniacy optymalizacji na pewno znajdą więcej możliwości dalszego zmniejszania rozmiaru aplikacji (choćby przyglądajac się bardziej szczegołowo funkcji GetDllVersion). Jeśli chodzi o kompresję gotowego pliku EXE pamiętaj, iż takie znane 'firmy' jak UPX czy ASPack nie zawsze osiągaja najlepsze wyniki. Do zmniejszania rozmiaru tak małych aplikacji najbardziej się nadają 'scenowe pakery', wykorzystywane między innymi do kompresji 64 kB, 4 kB czy nawet 1 kB demek. Na podstawie poniższych wartości można porównac wyniki wybranych kompresorów:
- ASPack: 10,752 bajtów
- fsg: 3,817 bajtów
- Mew: 3,542 bajtów
- PESpin: 26,112 bajtów
- UPX: 5,632 bajtów
- WinUpack: 3,764 bajtów
Najskuteczniejszy dla naszej aplikacji okazał się być Mew - jedynie 3,45 kB. Co ciekawe, niektóre kompresory w przypadku małych EXE zupełnie sobie nie radzą, np. ASPack zwiększył końcowy rozmiar o 3 kB, a PESpin 'skompresował' naszą aplikację aż do 26 kB.
Wszystkie kody żródłowe programu wraz z gotowymi plikami EXE: opty_przyklady.zip