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:

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

0 komentarzy