Teknik Makale

Delphi'de PDF Nesne Grafiğini Tam Bir Kez Serbest Bırakmak

Ayrıştırılmış bir PDF dosyasını yansıtan bir Delphi nesne modeli, genellikle her nesneyi içeren tek bir düz liste ile ona asılı gösterici grafiği olarak biter. Bu listeyi gezip her girdi üzerinde Free çağırırsanız çalışma, bir yıkıcının içinde erişim ihlaliyle yarı yolda ölür — daha önceki bir Free'in farklı bir ebeveyn üzerinden çoktan serbest bıraktığı bir nesnenin üzerinde. Döngüyü List.Clear ile değiştirirsiniz, çökme gider ama süreç bu kez her yeniden yüklemede kabaca bir belge büyümeye devam eder. İki belirtinin de tek nedeni var: liste düzdür, sahiplik ise bir graftır. İşe yarayan yıkım, hangi göstericilerin hedeflerine sahip olduğuna karar verir, o kapanışı bir kez toplar, her sahiplik kenarını keser ve ancak ondan sonra her düğümü — tam olarak bir kez — serbest bırakır

Kayıtlı her nesneyi serbest bırakmak neden çöker?

HotPDF'de düz liste, belgenin nesne kaydından ibarettir ve barındırdığı sınıflar kimin neyi yok edeceği konusunda anlaşamaz. Her yıkıcı kendi başına makuldür. Bunları, aynı sahiplik zincirinin birkaç düzeyinden girdi içeren bir kayıt üzerinde bir araya getirin; girdi başına Free bazı belleği iki kez, bazı belleği ise hiç serbest bırakmaz

HPDFObjs.pas ve HPDFDoc.pas içindeki üç yıkıcı bu durumu kurar. THPDFDictionaryObject.Destroy, Items içeriğini gezer ve bir değeri yalnızca IsIndirect False olduğunda serbest bırakır; dolaylı çocukların kayda ait olduğu varsayımıyla. THPDFArrayObject.Destroy, dolaylı olsun olmasın, tuttuğu her öğeyi serbest bırakır. Nesne akışlarından çıkarılan nesneler için kullanılan sarmalayıcı THPDFIndirectObject ise InternalObject gövdesini serbest bırakır (ISO 32000-1 §7.5.7). Şimdi dolaylı bir sözlüğü, aynı sözlüğü bir gözünde listeleyen bir diziyi ve gövdesi ayrıca kendi başına bir kök olarak kayıtlı bir sarmalayıcıyı tutan bir kayıt düşünün. Önce diziyi serbest bırakın; sözlük, kayıt ona ulaşmadan yok olur. Önce sarmalayıcıyı sonra gövdeyi serbest bırakın; ikinci çağrı, artık kimsenin olmadığı bellek üzerinde yıkıcı çalıştırır. Yalnızca sözlüğü serbest bırakın; atladığı her dolaylı çocuk süreç kapanana dek ayrılmış kalır

Kaydı yeniden sıralamak bunu kurtaramaz. Hangi sırayı seçerseniz seçin, bazı girdi çiftleri bir sahiplik kenarının yanlış tarafına düşer; çünkü bir listenin 12. girdinin 40. girdiye sahip olduğunu söyleme yolu yoktur

HotPDF kaydındaki her girdiyi serbest bırakmanın neden çöktüğü: THPDFDictionaryObject.Destroy dolaylı çocukları atlar, THPDFArrayObject.Destroy tuttuğu her şeyi serbest bırakır ve THPDFIndirectObject.Destroy InternalObject gövdesini serbest bırakır; bu yüzden tek bir düz IndirectObjects listesinde bir sarmalayıcı, bir dizi ve paylaşılan bir sözlük varsa bazı bellek iki kez ölür, bazısı hiç ölmez
Yıkıcılar kimin neye sahip olduğu konusunda anlaşamaz ve kayıt, aynı sahiplik zincirinin birkaç düzeyinden girdi tutar; bu yüzden düz bir listenin hiçbir sıralaması saf bir nesne başına Free çağrısını doğru bir yıkıma çeviremez

PDF nesne grafiğinde sahiplik kenarları ve başvurular

Sahiplik kenarı, hedefini yok etmekle kaynağın yükümlü olduğu bir göstericidir. Geri kalan her şey bir başvurudur ve yıkım yalnızca ilk türü izler. HotPDF'de tam olarak dört sahiplik kenarı türü vardır: bir THPDFDictionaryObject nesnesinin Items alanı, bir THPDFArrayObject nesnesinin Items alanı, bir THPDFIndirectObject arkasındaki InternalObject ve bir THPDFStreamObject nesnesinin iki alanı — Dictionary ile Stream yükü

Başvurular da aynı özeni hak eder; çünkü birini izlemek, grafik yürüyüşünü sonsuz bir döngüye ya da serbest bırakılmış bellek okumasına çevirir. Bir THPDFLink nesne numarası ve nesil tutar — bu tam olarak ISO 32000-1 §7.3.10'daki dolaylı başvurudur: dosyada başka bir yerde saklanan bir nesnenin adı. O numarayı kayıt üzerinden çözmek, başka bir kenarın zaten sahip olduğu bir düğüme düşer; bu yüzden yıkım linklere hiç başvurmaz. Sözlüklerin ve dizilerin tuttuğu FParent geri göstericisi ters yönde çalışır. Çocuğa zaten ebeveyn sahiptir ve o göstericiyi tırmanmak, yürüyüşün geçtiği bir düğümü yeniden ziyaret etmekten öteye gitmez. İki tür de yıkım boyunca el değmemiş kalır

HotPDF nesne grafiğinde sahiplik kenarları ve başvurular: DictionaryObject Items, ArrayObject Items, IndirectObject InternalObject ve bir StreamObject'ün iki yarısı izlenip koparılırken, bir THPDFLink nesne numarası ve FParent geri göstericisi başka yerde yaşayan nesnelerin adlarıdır, bu yüzden CloseIndirectObjects onları hiç çözmez
Sahiplik kenarı, hedefini kaynağın yok etmesi gereken bir göstericidir; bunun yerine bir başvuruyu izlemek genişlik öncelikli yürüyüşü sonsuz döngüye ya da serbest bırakılmış bellek kullanımına çevirirdi, bu yüzden linkler ve ebeveyn göstericileri el değmemiş bırakılır

Topla, kopar, serbest bırak: üç geçişli yıkım

HotPDF, bir belgenin nesnelerini protected THotPDF.CloseIndirectObjects yöntemi içinde üç geçişte serbest bırakır ve geçişlerin sırası tüm hiledir. Birinci geçiş toplar. İş listesi her kayıt girdisiyle başlar; yürüyüş her düğüm için sahiplik kenarlarının hedeflerini listeye ekler ve çoktan gördüklerini atlar. Görülenler kümesi, doğrusal yoklamalı ham göstericilerden oluşan açık adreslemeli bir dizidir ve yarıya dolduğunda iki katına çıkar; böylece düğüm başına hiçbir ayırma yapılmaz — bu, bir belge yüz binlerce nesne taşıdığında önem kazanır. Akış yükleri ikinci bir listeye gider; çünkü onlar THPDFObject düğümü değil, TStream torunlarıdır

HotPDF'te üç aşamalı CloseIndirectObjects yıkımı: genişlik öncelikli toplama, iş listesini IndirectObjects ile başlatır ve yalnızca sahiplik kenarlarını HPDFFastCacheHashInt64 ile hash'lenen açık adreslemeli bir görülenler kümesi üzerinden izler, ikinci aşama her kenarı MarkAsFreed ve nil atamasıyla koparır ve üçüncü aşama her düğümü ve akış yükünü tam olarak bir kez serbest bırakır
Kenarları herhangi bir yıkıcı çalışmadan önce kesmek, mevcut yıkıcıları yeniden kullanıma güvenli kılan şeydir: her biri o zaman özyineleyecek bir şey bulmaz, böylece paylaşılan çocuklar, döngüler ve sarmalayıcı-gövde takma adları çift serbest bırakma olmadan yıkılır

Aşağıdaki iki listeleme, bileşenin iç uygulamasından sadeleştirilmiş alıntılardır; geçişleri kütüphanenin kendi nesne türleri içinde görebilmeniz için verildi. Uygulama kodu bunların hiçbirini çağırmaz; LoadFromFile, LoadFromStream ve yıkıcı bunları sizin için çalıştırır. Alıntıda adres hash'i bir kaydırma ve maskeye indirgenmiştir ve görülenler kümesi tablosunu dolduğunda iki katına çıkaran adım çıkarılmıştır

// İç yıkımın sadeleştirilmiş alıntısı; çağrılabilir bir API değildir
procedure Collect(Value: TObject; Payload: boolean);
var
  Slot: Integer;
begin
  if Value = nil then Exit;
  // (gerçek rutin, tablo yarıya dolduğunda iki katına çıkarıp yeniden hash'ler)
  Slot := Integer(NativeUInt(Value) shr 4) and (Length(Seen) - 1);
  while Seen[Slot] <> nil do
  begin
    if Seen[Slot] = Pointer(Value) then Exit;   // zaten toplandı
    Slot := (Slot + 1) and (Length(Seen) - 1);
  end;
  Seen[Slot] := Pointer(Value);
  Inc(SeenCount);
  if Payload then Streams.Add(Value) else Nodes.Add(Value);
end;

// Birinci aşama: kayıtla başlat, sonra yalnızca sahiplik kenarlarını izle
for I := 0 to IndirectObjects.Count - 1 do
  Collect(TObject(IndirectObjects[I]), False);
I := 0;
while I < Nodes.Count do
begin
  Obj := THPDFObject(Nodes[I]);
  if Obj is THPDFIndirectObject then
    Collect(THPDFIndirectObject(Obj).InternalObject, False)
  else if Obj is THPDFStreamObject then
  begin
    Collect(THPDFStreamObject(Obj).Dictionary, False);
    Collect(THPDFStreamObject(Obj).Stream, True);
  end
  else if Obj is THPDFDictionaryObject then
    for J := 0 to THPDFDictionaryObject(Obj).Items.Count - 1 do
      Collect(PHPDFDictionaryItem(THPDFDictionaryObject(Obj).Items[J])^.Value, False)
  else if Obj is THPDFArrayObject then
    for J := 0 to THPDFArrayObject(Obj).Items.Count - 1 do
      Collect(TObject(THPDFArrayObject(Obj).Items[J]), False);
  // THPDFLink değerleri ve FParent göstericileri hiç izlenmez
  Inc(I);
end;

İkinci geçiş keser. Tek bir yıkıcı çalışmadan önce her sahiplik kenarı nil yapılır. Sarmalayıcı MarkAsFreed alır; bu, InternalObject'i temizler ve yıkıcısının ilk baktığı IsFreed bayrağını set eder. Bir akış nesnesinin Dictionary ve Stream alanlarına nil atanır, her sözlük öğesinin Value'su temizlenir ve her dizi gözünün üzerine nil yazılır. Üçüncü geçiş kaydı temizler, Nodes içindeki her düğümü ve ardından Streams içindeki her yükü serbest bırakır

// İç yıkımın sadeleştirilmiş alıntısı, devamı; çağrılabilir bir API değildir
// İkinci aşama: hiçbir şeyi serbest bırakmadan önce her sahiplik kenarını kopar
for I := 0 to Nodes.Count - 1 do
begin
  Obj := THPDFObject(Nodes[I]);
  if Obj is THPDFIndirectObject then
    THPDFIndirectObject(Obj).MarkAsFreed
  else if Obj is THPDFStreamObject then
  begin
    THPDFStreamObject(Obj).Dictionary := nil;
    THPDFStreamObject(Obj).Stream := nil;
  end
  else if Obj is THPDFDictionaryObject then
    for J := 0 to THPDFDictionaryObject(Obj).Items.Count - 1 do
      PHPDFDictionaryItem(THPDFDictionaryObject(Obj).Items[J])^.Value := nil
  else if Obj is THPDFArrayObject then
    for J := 0 to THPDFArrayObject(Obj).Items.Count - 1 do
      THPDFArrayObject(Obj).Items[J] := nil;
end;

// Üçüncü aşama: her benzersiz düğüm ve yük tam olarak bir kez serbest bırakılır
IndirectObjects.Clear;
for I := 0 to Nodes.Count - 1 do TObject(Nodes[I]).Free;
for I := 0 to Streams.Count - 1 do TObject(Streams[I]).Free;
FreeAndNil(IndirectObjects);

Kesme, mevcut yıkıcıların olduğu gibi kalmasını sağlayan şeydir. Kenar kalmadığı için her yıkıcı özyineleyecek bir şey bulamaz ve yalnızca kendini yok eder. Aynı sonuca giden diğer yol, her yıkıcıya kaydı öğretmektir; böylece sözlük, dizi ve sarmalayıcı, çocuklarını başka bir patikanın serbest bırakıp bırakmayacağını sorar. Bu, grafik bilgisini dört sınıfa ve sonradan eklenen her sınıfa yayar; herhangi bir yerdeki tek yanlış cevap çift serbest bırakmayı geri getirir. Bütün sahiplik modelini tek bir yıkım rutininde tutmak daha güvenli dizilimdir

Zor durumları bu düzenekten geçirin; hiçbiri özel bir dal gerektirmez. İki akış nesnesinin paylaştığı bir sözlük bir kez toplanır, ikisinden de koparılır ve bir kez serbest bırakılır. Bir dizinin kendi ebeveyn sözlüğünü listelediği döngü, görülenler kümesi ikinci ziyareti reddettiği için sonlanır. İki kök olarak kaydedilmiş bir sarmalayıcı ile gövdesi kümede iki ayrı göstericidir; ikisi de serbest bırakılır ve sarmalayıcının yıkıcısı gövdeye dokunmaz, çünkü MarkAsFreed o kenarı çoktan kaldırmıştır. İki akış nesnesine yük olarak atanmış tek bir TMemoryStream, Streams içinde tam olarak bir kez bulunur

Grafik yıkılmadan önce neyin durması gerekiyor?

Graftan nesne ödünç alan her şey önce durmak zorundadır. HotPDF, PrefetchLoadedPages ile sayfaları bir işçi iş parçacığında oluşturabilir ve belgenin nesnelerinden derlenen bitmap'leri ve görüntüleme listelerini önbelleğe alır; grafikten uzun yaşayan bir işçi ya da önbellek girdisi serbest bırakılmış bellek okurdu. Bu yüzden yıkım CancelLoadedPagePrefetch ile açılır ve kayda dokunmadan önce oluşturulmuş sayfa önbelleğini geçersiz kılar. LoadFromFile, LoadFromStream ve THotPDF yıkıcısının hepsi buradan geçer; böylece bir belgeyi değiştirin ya da bileşeni serbest bırakın, sıra aynıdır ve tek bir THotPDF'i belgeler arasında yeniden kullanma kuralları buna yaslanır

Bildirim sözleşmesi de en az o kadar önemlidir. InvalidateRenderedPageCache, PageIndex = -1 ile OnLoadedDocumentModified tetikleyen yöntemdir ve bir dosyayı yeniden yükleyen görüntüleyici, eski belgenin iç temizliği için bir düzenleme bildirimi almamalıdır. Yıkım içinde işleyici saklanır, o çağrının çevresinde nil yapılır ve bir finally bloğunda geri yüklenir. Yeniden yükleme yine de bir PageIndex = -1 çağrısı üretir; bu çağrıyı, yeni belge için önbelleği sıfırlarken yüklemenin kendisi tetikler ve görüntüleyicinin yeniden çizim için beklediği çağrı budur. Yıkıcıda grafik kapanana dek önbellekler çoktan elden çıkarılmıştır, bu yüzden yıkım orada geçersiz kılmayı tamamen atlar

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  FPdf := THotPDF.Create(Self);
  FPdf.OnLoadedDocumentModified := PdfModified;
end;

procedure TViewerForm.PdfModified(Sender: TObject; PageIndex: Integer);
begin
  if PageIndex = -1 then
    RepaintAllPages           // kendi görüntüleyici yönteminiz
  else
    RepaintPage(PageIndex);   // kendi görüntüleyici yönteminiz
end;

procedure TViewerForm.OpenDocument(const FileName: string);
begin
  // LoadFromFile, çalışan bir ön yüklemeyi iptal eder ve önceki
  // belgenin nesnelerini ayrıştırmadan önce bırakır; PdfModified tek bir -1 çağrısı görür
  FPdf.LoadFromFile(FileName);
  FPdf.PrefetchLoadedPages('1-10', 150);
end;

OpenDocument içinde ayrıca CancelLoadedPagePrefetch çağırmaya gerek yoktur; çünkü yükleme onu kendisi yapar. Yine de ön yükleme çalışıyor olabilirken yüklenmiş belgeyi değiştirmek üzereyseniz kendiniz çağırmak doğru hamledir; HPDFDoc.pas içindeki bildirim yorumu tam olarak bunu ister. Daha büyük ölçekte işçi havuzuyla oluşturma için paralel oluşturma hattı yazısı, arka plan işinin nasıl kısıtlandığını anlatır

Bir sızıntıyı ayırıcı alıkoymasından nasıl ayırt edersiniz?

Tekrarlanan yüklemeler boyunca bellek yöneticisinin canlı ayırma sayacını izleyin ve süreç çalışma kümesini yok sayın. Delphi bellek yöneticisi, serbest bırakılan belleği yeniden kullanım için havuzda tutar; bu yüzden bir belgeyi kapattıktan sonra yüksek ayak izinde sabit kalan bir süreç illa sızdırmış değildir. Canlı büyük blok sayısı her yeniden yüklemede tırmanan süreç sızdırmıştır. Aşağıdaki döngü aynı dosyayı tek bir örnekte yirmi kez yükler ve her geçişten sonra büyük blok rakamlarını yazar

procedure ReportLargeBlocks(const Tag: string);
var
  State: TMemoryManagerState;
begin
  GetMemoryManagerState(State);
  Writeln(Tag, ': ', State.AllocatedLargeBlockCount, ' large blocks, ',
    State.TotalAllocatedLargeBlockSize, ' bytes');
end;

var
  Pdf: THotPDF;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    for I := 1 to 20 do
    begin
      Pdf.LoadFromFile('sample.pdf');
      ReportLargeBlocks('pass ' + IntToStr(I));
    end;
  finally
    Pdf.Free;
  end;
  ReportLargeBlocks('after Free');
end.

Çıktıyı bir biçim olarak okuyun. İlk geçişten sonra yerleşen rakamlar alıkoyma ya da sıradan belge başına durumdur. Her geçişte aynı adımla yükselen rakamlar sızıntıdır ve bir PDF için adım genellikle daha büyük akış yüklerinin — sayfa içeriği ile gömülü görsellerin — boyutudur; büyük blok rakamlarına düşecek kadar büyük olan ayırmalar bunlardır. Adımın gürültüye karşı kolay görülmesi için on iki ya da daha fazla sayfalı bir dosya kullanın. Sayfa sayısıyla orantılı büyümeyi ayırıcı alıkoyması açıklayamaz. Bir belge açıkken belleği hangi nesnelerin tuttuğunu bulmak için tutulan baytlarla nesne bağımlılık grafiği sorunun o yanını yanıtlar

Bu deseni kendi Delphi nesne modelinizde yeniden kullanmak

Buradaki hiçbir şey PDF'e özgü değildir. Yıkıcıların çocuklara tutarsız biçimde sahip olduğu, bir çocuğa birden fazla ebeveynden ulaşılabildiği ya da geri göstericilerin ileri göstericilerin yanında durduğu her Delphi nesne modeli, nesne başına Free altında çöker ya da sızdırır. Düzeltme her seferinde aynı biçimdedir: her gösterici alanını sahiplik ya da başvuru olarak sınıflandır, sahiplik kenarlarının kapanışını yeniden ziyaretlere dayanıklı bir gösterici kümesi üzerinden topla, her kenarı kes, düz listeyi yok et. İnsanların atladığı adım kesme adımıdır. Atlarsanız her yıkıcı, diğer tüm yıkıcıları bilmek zorunda kalır

Desen, kopyalamadan önce not edilmeye değer sınırlarla gelir. Gösterici kümesi kimlik olarak nesne adresini kullanır; bu ancak toplama sırasında hiçbir yıkıcının çalışmaması yüzünden sağlamdır, böylece yürüyüşün ortasında hiçbir adres serbest bırakılıp taze bir ayırmaya verilemez. Yürüyüş yalnızca yazıldığı kenar türlerini görür; bir çocuğa başka bir alan üzerinden sahip olan yeni bir sınıf, hem toplama hem koparma geçişine eklenmek zorundadır. Ve linkler izlenmek yerine çözüldüğü için, yalnızca bir linkle başvurulan bir nesneye ulaşılabilmesi için kayıtta olması gerekir. HotPDF ayrıştırıcısı okuduğu her nesneyi kaydeder; kendi kodunuzda elle kurulmuş bir grafik de aynı kurala uymalıdır

Bunu bayt saymak yerine yıkıcı çağrılarını sayarak test edin. Sinsice grafiği elle kurun: iki akışın paylaştığı bir sözlük, hem o sözlüğü hem kendi ebeveynini tutan bir dizi, iki akışa da atanmış tek bir yük, iki kez kaydedilmiş bir kök ve sayısal gövdesi de kayıtlı bir sarmalayıcı. Yıkın ve her benzersiz nesne için tam olarak bir yıkım bekleyin — bir yük, iki akış, iki sözlük, bir dizi, bir sarmalayıcı, bir sayı. Kendi sınıflarınızda her yıkıcıda artırılan sınıf düzeyinde bir sayaç bunun için yeterlidir (kendinizin eklediği bir ölçüm kancasıdır); listeyi yalnızca temizleyen bir yıkım ise sıfır bildirir

HotPDF'de bütün bunlar LoadFromFile, LoadFromStream ve yıkıcının arkasındadır; uygulama kodu yalnızca sonucu görür: bir belgeyi kapatmak ya da yeniden yüklemek belleğini geri verir. HotPDF, tam kaynak koduyla dağıtılan Delphi ve C++Builder için yerel bir VCL PDF kütüphanesidir; burada anlatılan yıkım, HPDFDoc.pas içinde satır satır okunabilir. API referansı ve deneme derlemesi Delphi için HotPDF PDF bileşeni sayfasındadır