Păstrați utilizarea memorie a proiectului dvs. de memorie stabilă cu ajutorul unui obiect de asociere

Utilizarea memoriei este un aspect al dezvoltării pe care trebuie să-l iei cu grijă, sau ar putea să-ți încetinești aplicația, să-ți ții o mulțime de memorie sau chiar să prăbușești totul. Acest tutorial vă va ajuta să evitați aceste rezultate potențiale proaste!


Rezultatul final al rezultatelor

Să aruncăm o privire asupra rezultatului final pe care îl vom strădui:

Faceți clic oriunde pe scenă pentru a crea un efect de focuri de artificii și păstrați o privire asupra profilului de memorie din colțul din stânga sus.


Pasul 1: Introducere

Dacă ați profilat vreodată aplicația dvs. utilizând orice instrument de profilare sau ați folosit orice cod sau bibliotecă care vă indică utilizarea curentă a memoriei aplicației dvs., este posibil să fi observat că de multe ori crește frecvența utilizării memoriei și apoi se coboară din nou (dacă nu sunteți Codul dvs. este superb!). Ei bine, deși aceste spikes cauzate de utilizarea memoriei mari arată destul de răcoros, nu este o veste bună fie pentru aplicația dvs., fie pentru utilizatorii dvs. (în consecință). Continuați să citiți pentru a înțelege de ce se întâmplă acest lucru și cum să îl evitați.


Pasul 2: Utilizarea bună și rea

Imaginea de mai jos este un exemplu foarte bun de gestionare a memoriei slabe. E de la un prototip al unui joc. Trebuie să observați două lucruri importante: vârfurile mari ale memoriei și consumul de memorie. Vârful este aproape la 540Mb! Asta înseamnă că acest prototip singur a ajuns la punctul de a folosi 540Mb din memoria RAM a utilizatorului - și asta este ceva cu siguranță că vrei să eviți.

Această problemă începe atunci când începeți să creați o mulțime de instanțe de obiecte în aplicația dvs. Exemplele neutilizate vor continua să utilizeze memoria aplicației până când colectorul de gunoi se va executa, atunci când va fi dealocat - cauzând spikele mari. O situație mai gravă se întâmplă atunci când instanțele pur și simplu nu vor fi dezalocate, cauzând utilizarea memoriei aplicației dvs. să continue să crească până când ceva se prăbușește sau se rupe. Dacă doriți să aflați mai multe despre ultima problemă și cum să o evitați, citiți acest Sfat rapid despre colectarea gunoiului.

În acest tutorial nu vom aborda probleme de colectare a gunoiului. În schimb, vom lucra la construirea de structuri care păstrează eficient obiectele în memorie, făcând astfel utilizarea lor complet stabilă și astfel păstrând colectorul de gunoi de la curățarea memoriei, făcând aplicația mai rapidă. Uitați-vă la utilizarea memoriei aceluiași prototip de mai sus, dar de data aceasta optimizată prin tehnicile prezentate aici:

Toate aceste îmbunătățiri pot fi realizate utilizând utilizarea grupurilor de obiecte. Citiți mai departe pentru a înțelege ce este și cum funcționează.


Pasul 3: Tipuri de bazine

Object pooling este o tehnică în care un număr predefinit de obiecte sunt create atunci când aplicația este inițializată și păstrată în memorie pe toată durata de viață a aplicației. Piscina de obiecte furnizează obiecte atunci când cererea le cere și resetează obiectele înapoi la starea inițială atunci când aplicația este terminată cu ajutorul acestora. Există multe tipuri de bazine de obiecte, dar vom examina doar două dintre ele: bazinele de obiecte statice și dinamice.

Piscul de obiecte statice creează un număr definit de obiecte și păstrează numai acea cantitate de obiecte pe toată durata de viață a aplicației. Dacă un obiect este solicitat, dar piscina a dat deja toate obiectele sale, piscina returnează nul. Atunci când folosiți acest tip de piscină, este necesar să abordați probleme cum ar fi solicitarea unui obiect și obținerea de nimic înapoi.

De asemenea, grupul de obiecte dinamice creează un număr definit de obiecte la inițializare, dar atunci când un obiect este solicitat și fondul este gol, piscina creează automat o altă instanță și returnează acel obiect, mărind dimensiunea bazinului și adăugând noul obiect.

În acest tutorial vom construi o aplicație simplă care generează particule atunci când utilizatorul face clic pe ecran. Aceste particule vor avea o durată de viață finită și apoi vor fi îndepărtate de pe ecran și vor fi returnate la piscină. Pentru a face acest lucru, vom crea mai întâi această aplicație fără piscarea obiectelor și vom verifica utilizarea memoriei, apoi vom implementa piscina de obiecte și vom compara utilizarea de memorie cu înainte.


Pasul 4: Cererea inițială

Deschideți FlashDevelop (consultați acest ghid) și creați un nou proiect AS3. Vom folosi un simplu pătrat de culoare mic ca imaginea particulei, care va fi trasată cu cod și se va deplasa în funcție de un unghi aleatoriu. Creați o nouă clasă numită Particle care extinde Sprite. Voi presupune că puteți să vă ocupați de crearea unei particule și să evidențiați aspectele care vor urmări durata de viață a particulelor și scoaterea de pe ecran. Puteți obține codul sursă complet al acestui tutorial în partea de sus a paginii dacă aveți probleme cu crearea particulelor.

 privat var _lifeTime: int; actualizarea funcției publice (timePassed: uint): void // Efectuarea deplasării particulelor x + = Math.cos (_angle) * _speed * timePassed / 1000; y + = Math.sin (_angle) * _speed * timePassed / 1000; // Relaxare mică pentru a face mișcarea să arate destul de rapidă - = 120 * timePassed / 1000; // Grijă de viață și eliminare _lifeTime - = timePassed; dacă (_lifeTime <= 0)  parent.removeChild(this);  

Codul de mai sus este codul responsabil pentru îndepărtarea particulelor de pe ecran. Creăm o variabilă numită _durata de viață pentru a conține numărul de milisecunde pe care particula va apărea pe ecran. Inițializăm valoarea implicită la 1000 de constructori. Actualizați() funcția se numește fiecare cadru și primește cantitatea de milisecunde care a trecut între cadre, astfel încât să scadă valoarea de viață a particulei. Atunci când această valoare ajunge la 0 sau mai puțin, particulă cere automat părintelui să o elimine din ecran. Restul codului are grijă de mișcarea particulelor.

Acum vom face o grămadă de astfel de creare atunci când este detectat un clic al mouse-ului. Accesați Main.as:

 privat var _oldTime: uint; privat var _elapsed: uint; funcția privată init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // punct de intrare stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); _oldTime = getTimer ();  actualizare funcție privatăParticule (e: Eveniment): void _elapsed = getTimer () - _oldTime; _oldTime + = _lapsed; pentru (var i: int = 0; i < numChildren; i++)  if (getChildAt(i) is Particle)  Particle(getChildAt(i)).update(_elapsed);    private function createParticles(e:MouseEvent):void  for (var i:int = 0; i < 10; i++)  addChild(new Particle(stage.mouseX, stage.mouseY));  

Codul pentru actualizarea particulelor ar trebui să vă fie familiar: este rădăcinile unei bucăți simple pe bază de timp, frecvent utilizate în jocuri. Nu uitați declarațiile de import:

 importul flash.events.Event; importul flash.events.MouseEvent; import flash.utils.getTimer;

Puteți să vă testați aplicația și să o profilați folosind profilul profilului încorporat al FlashDevelop. Faceți clic pe o grămadă de ori pe ecran. Iată cum arată modul de utilizare a memoriei:

Am făcut clic până când colectorul de gunoi a început să alerge. Aplicația a creat peste 2000 de particule care au fost colectate. Începe să arate ca utilizarea memoriei acelui prototip? Se pare că, și cu siguranță nu este bine. Pentru a ușura profilarea, vom adăuga utilitatea menționată în primul pas. Iată codul de adăugat în Main.as:

 funcția privată init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // punct de intrare stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); addChild (statistici noi ()); _oldTime = getTimer (); 

Nu uitați să importați net.hires.debug.Stats și este gata de utilizare!


Pasul 5: Definiți un obiect Poolable

Aplicația pe care am construit-o în pasul 4 a fost destul de simplă. Ea conținea doar un efect simplu de particule, dar a creat o mulțime de probleme în memorie. În acest pas, vom începe să lucrăm la un grup de obiecte pentru a rezolva această problemă.

Primul nostru pas către o soluție bună este să ne gândim la modul în care obiectele pot fi reunite fără probleme. Într-un grup de obiecte, trebuie să ne asigurăm întotdeauna că obiectul creat este gata pentru utilizare și că obiectul returnat este complet "izolat" de restul aplicației (adică nu deține referințe la alte lucruri). Pentru a forța fiecare obiect cumulat să facă acest lucru, vom crea unul interfață. Această interfață va defini două funcții importante pe care trebuie să le aibă obiectul: reînnoi() și distruge(). În acest fel, putem să numim întotdeauna aceste metode fără să ne îngrijorăm dacă le are sau nu obiectul (pentru că va avea). Acest lucru înseamnă, de asemenea, că fiecare obiect pe care dorim să îl piscină va trebui să implementeze această interfață. Deci, aici este:

 pachet interfață publică IPoolable function get destroyed (): Boolean; reînnoirea funcției (): void; distruge funcția (): void; 

Din moment ce particulele noastre vor fi poolabile, trebuie să le punem în aplicare IPoolable. Practic, mutăm tot codul de la constructorii lor la reînnoi() funcția și elimina orice referințe externe la obiect în distruge() funcţie. Iată cum ar trebui să arate:

 / * INTERFACE IPoolable * / funcția publică se distruge (): Boolean return _destroyed;  funcția public reînnoi (): void if (! _destroyed) return;  _destroyed = false; grafic.beginFill (uint (Math.random () * 0xFFFFFF), 0,5 + (Math.random () * 0,5)); graficele graficului (-1.5, -1.5, 3, 3); graphics.endFill (); _angle = Math.random () * Math.PI * 2; _speed = 150; // Pixeli pe secundă _lifeTime = 1000; // milisecunde funcția publică distruge (): void if (_destroyed) return;  _destroyed = true; graphics.clear (); 

De asemenea, constructorul nu ar mai trebui să mai solicite argumente. Dacă doriți să transmiteți orice informație obiectului, va trebui să o faceți prin funcții acum. Datorită modului în care reînnoi() funcționează acum, trebuie, de asemenea, să stabilim _distrus la Adevărat în constructor astfel încât funcția să poată fi rulată.

Cu asta, ne-am adaptat particulă clasa să se comporte ca o IPoolable. În acest fel, bazinul de obiecte va putea crea o piscină de particule.


Pasul 6: Pornirea grupului de obiecte

Este timpul să creați o piscină flexibilă de obiecte care să pună în comun orice obiect dorit. Acest pool va acționa puțin ca o fabrică: în loc să folosească nou pentru a crea obiecte pe care le puteți folosi, vom apela o metodă în bazinul care ne întoarce un obiect.

În scopul simplității, piscina de obiecte va fi un Singleton. În acest fel, îl putem accesa oriunde în codul nostru. Începeți prin a crea o nouă clasă numită "ObjectPool" și adăugând codul pentru a deveni un Singleton:

 pachet public class ObjectPool privat static var _instance: ObjectPool; privat static var _allowInstantiation: Boolean; funcția statică publică obține instanță (): ObjectPool if (! _instance) _allowInstantiation = true; _instance = ObjectPool nou (); _allowInstantiation = false;  întoarcere _instance;  funcția publică ObjectPool () if (! _allowInstantiation) arunca o nouă eroare ("Încercarea de a instanți un Singleton!"); 

Variabila _allowInstantiation este nucleul acestei implementări Singleton: este privat, deci numai clasa proprie poate modifica și singurul loc în care ar trebui să fie modificat este înainte de a crea prima instanță a acesteia.

Acum trebuie să decidem cum să ținem piscinele în interiorul acestei clase. Deoarece va fi globală (adică poate pescui orice obiect din aplicația dvs.), trebuie să găsim mai întâi o modalitate de a avea întotdeauna un nume unic pentru fiecare grup. Cum să faci asta? Există multe moduri, dar cel mai bun lucru pe care l-am găsit până acum este de a folosi numele propriilor clase ale obiectelor ca nume de piscină. În acest fel, am putea avea o piscină "Particule", o piscină "Enemy" și așa mai departe ... dar există o altă problemă. Numele de clase trebuie să fie doar unice în pachetele lor, de exemplu, o clasă "BaseObject" din pachetul "inamici" și o clasă "BaseObject" din pachetul "structuri" ar fi permisă. Asta ar cauza probleme în piscină.

Ideea de a folosi nume de clasă ca identificatori pentru bazine este încă mare și aici este locul flash.utils.getQualifiedClassName () vine să ne ajute. Practic această funcție generează un șir cu numele de clasă completă, inclusiv orice pachete. Deci, acum putem folosi numele de clasă calificat al fiecărui obiect ca identificator pentru bazinele lor respective! Aceasta este ceea ce vom adăuga în pasul următor.


Pasul 7: Crearea de piscine

Acum că avem o modalitate de a identifica piscinele, este timpul să adăugăm codul care le creează. Piscina noastră de obiecte ar trebui să fie suficient de flexibilă pentru a susține bazinele statice și dinamice (am vorbit despre acestea în pasul 3, amintiți?). Trebuie, de asemenea, să putem stoca dimensiunea fiecărui bazin și câte obiecte active există în fiecare. O bună soluție pentru aceasta este de a crea o clasă privată cu toate aceste informații și de a stoca toate bazinele într-un Obiect:

 pachet public class ObjectPool privat static var _instance: ObjectPool; privat static var _allowInstantiation: Boolean; privat var _pools: Obiect; funcția statică publică obține instanță (): ObjectPool if (! _instance) _allowInstantiation = true; _instance = ObjectPool nou (); _allowInstantiation = false;  întoarcere _instance;  funcția publică ObjectPool () if (! _allowInstantiation) arunca o nouă eroare ("Încercarea de a instanți un Singleton!");  _pools = ;  clasa PoolInfo public var items: Vector.; public var itemClass: Clasă; public var size: uint; public var activ: uint; public var esteDynamic: Boolean; funcția publică PoolInfo (itemClass: Class, size: uint, isDynamic: Boolean = true) this.itemClass = itemClass; elemente = Vector nou.(dimensiune,! esteDynamic); this.size = dimensiune; this.isDynamic = isDynamic; active = 0; inițializarea ();  funcția privată initialize (): void pentru (var i: int = 0; i < size; i++)  items[i] = new itemClass();   

Codul de mai sus creează clasa privată, care va conține toate informațiile despre o piscină. De asemenea, am creat _pools obiecte pentru a ține toate bazele de obiecte. Mai jos vom crea funcția care înregistrează o piscină în clasă:

 funcția publică RegisterPool (objectClass: Class, size: uint = 1, isDynamic: Boolean = true): void if (! 0)) arunca o nouă eroare ("Nu se poate pune în comun ceva care nu implementează IPOlabil!"); întoarcere;  var qualifiedName: String = getQualifiedClassName (objectClass); dacă (! _pools [qualifiedName]) _pools [qualifiedName] = PoolInfo nou (objectClass, size, isDynamic); 

Acest cod pare un pic mai complicat, dar nu intra in panica. Totul este explicat aici. Primul dacă declarația pare cu adevărat ciudată. Poate că nu ați mai văzut aceste funcții înainte, deci iată ce face:

  • Funcția describeType () creează un XML care conține toate informațiile despre obiectul pe care l-am trecut.
  • În cazul unei clase, totul despre el este conținut în cadrul fabrică etichetă.
  • În interiorul acestuia, XML descrie toate interfețele pe care le implementează clasa cu implementsInterface etichetă.
  • Facem o căutare rapidă pentru a vedea dacă IPoolable interfața este printre ele. Dacă este așa, atunci știm că putem adăuga acea clasă la piscină, pentru că vom reuși să o prezentăm cu succes Obiectez.

Codul după această verificare creează doar o intrare înăuntru _pools dacă nu există deja. După aceea, PoolInfo constructorul solicită inițializa () funcția în cadrul acelei clase, creând efectiv piscina cu dimensiunea dorită. Acum este gata să fie folosit!


Pasul 8: Obținerea unui obiect

În ultima etapă am reușit să creăm funcția care înregistrează o piscină de obiecte, dar acum trebuie să obținem un obiect pentru ao folosi. Este foarte simplu: obținem un obiect dacă piscina nu este goală și o returnează. Dacă piscina este goală, verificăm dacă este dinamică; dacă este așa, vom mări dimensiunea sa, apoi vom crea un obiect nou și vom returna. Dacă nu, vom reveni la zero. (De asemenea, puteți alege să aruncați o eroare, dar este mai bine să reveniți la nul și să faceți codul dvs. de lucru în jurul acestei situații atunci când se întâmplă.)

Iată-l getObj () funcţie:

 funcția publică getObj (objectClass: Class): IPoolable var qualifiedName: String = getQualifiedClassName (objectClass); if (! _pools [qualifiedName]) arunca o eroare nouă ("Nu se poate obține un obiect dintr-un grup care nu a fost înregistrat!"); întoarcere;  var returnObj: IPoolable; dacă PoolInfo (_pools [qualifiedName]) este activă == PoolInfo (_pools [qualifiedName])) if (PoolInfo (_pools [qualifiedName]) esteDynamic) returnObj = new objectClass; PoolInfo (_pools [qualifiedName]) dimensiunea ++.; PoolInfo (_pools [qualifiedName]) items.push (returnObj.);  altceva return null;  altceva returnObj = PoolInfo (_pools [qualifiedName]) elemente [PoolInfo (_pools [qualifiedName]). returnObj.renew ();  PoolInfo (_pools [qualifiedName]) activ ++; retur returObj; 

În funcție, mai întâi verificăm existența fondului. Presupunând că această condiție este îndeplinită, verificăm dacă piscina este goală: dacă este, dar este dinamică, vom crea un obiect nou și vom adăuga în bazin. Dacă piscina nu este dinamică, vom opri codul acolo și vom reveni la zero. Dacă piscina are încă un obiect, obținem obiectul cel mai apropiat de începutul bazinului și sunăm reînnoi() pe el. Acest lucru este important: motivul pe care îl numim reînnoi() pe un obiect care era deja în piscină este de a garanta că acest obiect va fi dat într-o stare "utilizabilă".

Probabil vă întrebați: de ce nu folosiți, de asemenea, acea verificare rece cu describeType () în această funcție? Ei bine, răspunsul este simplu: describeType () creează un XML fiecare timpul îl numim, deci este foarte important să evităm crearea de obiecte care folosesc o mulțime de memorie și pe care nu o putem controla. În afară de aceasta, este suficient să verificați doar dacă piscina există într-adevăr: dacă clasa trecută nu este implementată IPoolable, asta înseamnă că nici măcar nu am putea crea o piscină pentru asta. Dacă nu există o piscină pentru ea, atunci cu siguranță ne prindem acest caz în noi dacă declarație la începutul funcției.

Acum ne putem modifica Principal clasa și de a folosi piscina obiect! Verifică:

 funcția privată init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // punct de intrare stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); _oldTime = getTimer (); ObjectPool.instance.registerPool (particule, 200, true);  funcția privată createParticles (e: MouseEvent): void var tempParticle: Particle; pentru (var i: int = 0; i < 10; i++)  tempParticle = ObjectPool.instance.getObj(Particle) as Particle; tempParticle.x = e.stageX; tempParticle.y = e.stageY; addChild(tempParticle);  

Hit compilați și profitați de utilizarea memoriei! Iată ce am primit:

E cam cool, nu-i așa??


Pasul 9: Revenirea obiectelor la piscină

Am implementat cu succes un bazin de obiecte care ne oferă obiecte. Asta e uimitor! Dar nu sa terminat încă. Încă mai primim obiecte, dar nu le întoarcem niciodată când nu mai avem nevoie de ele. Este timpul să adăugați o funcție pentru a returna obiectele din interior ObjectPool.as:

 funcția publică returnObj (obj: IPoolable): void var qualifiedName: String = getQualifiedClassName (obj); if (! _pools [qualifiedName]) arunca o eroare nouă ("Nu se poate întoarce un obiect dintr-un grup care nu a fost înregistrat!"); întoarcere;  var objIndex: int = PoolInfo (_pools [qualifiedName]). elemente.indexOf (obj); dacă (objIndex> = 0) if (! PoolInfo (_pools [qualifiedName]) esteDynamic) PoolInfo (_pools [qualifiedName]) items.fixed = false;  PoolInfo (_pools [qualifiedName]). Items.splice (objIndex, 1); obj.destroy (); PoolInfo (_pools [qualifiedName]) items.push (obj.); dacă (! PoolInfo (_pools [qualifiedName]) esteDynamic) PoolInfo (_pools [qualifiedName]) items.fixed = true;  PoolInfo (_pools [qualifiedName]). 

Să trecem prin această funcție: primul lucru este să verificăm dacă există o piscină a obiectului care a fost trecut. Ești obișnuit cu acel cod - singura diferență este că acum folosim un obiect în loc de o clasă pentru a obține numele calificat, dar asta nu schimbă rezultatul).

Apoi, vom obține indicele elementului din piscină. Dacă nu este în piscină, noi doar ignorăm. După verificarea faptului că obiectul se află în piscină, trebuie să rupem piscina la locul unde se află obiectul și să reintroduceți obiectul la sfârșitul acestuia. Și de ce? Deoarece numărăm obiectele folosite de la începutul piscinei, trebuie să reorganizăm piscina pentru a face ca toate obiectele returnate și neutilizate să fie la sfârșitul acesteia. Și asta facem în această funcție.

Pentru bazinele de obiecte statice, creăm un Vector obiect care are o lungime fixă. Din cauza asta, nu putem lipitură() și Apăsați() obiecte înapoi. Soluționarea acestui lucru este de a schimba situația fix proprietatea acestor persoane Vectors fals, scoateți obiectul și adăugați-l la sfârșit, apoi schimbați proprietatea înapoi Adevărat. De asemenea, trebuie să scădem numărul de obiecte active. După asta, am terminat să întoarcem obiectul.

Acum, când am creat codul pentru a returna un obiect, putem face ca particulele noastre să revină la piscină odată ce ajung la sfârșitul vieții. Interior Particle.as:

 actualizarea funcției publice (timePassed: uint): void // Efectuarea deplasării particulelor x + = Math.cos (_angle) * _speed * timePassed / 1000; y + = Math.sin (_angle) * _speed * timePassed / 1000; // Relaxare mică pentru a face mișcarea să arate destul de rapidă - = 120 * timePassed / 1000; // Grijă de viață și eliminare _lifeTime - = timePassed; dacă (_lifeTime <= 0)  parent.removeChild(this); ObjectPool.instance.returnObj(this);  

Observați că am adăugat un apel la ObjectPool.instance.returnObj () acolo. Asta face ca obiectul să se întoarcă la piscină. Acum putem testa și profilam aplicația noastră:

Și acolo mergem! Memorie stabilă chiar și atunci când au fost făcute sute de clicuri!


Concluzie

Acum știți cum să creați și să utilizați un fond de obiecte pentru a menține stabilitatea memoriei aplicației. Clasa pe care am construit-o construită poate fi folosită oriunde și este foarte simplu să vă adaptați codul: la începutul aplicației dvs., creați grupuri de obiecte pentru fiecare tip de obiect pe care doriți să îl piscinați și ori de câte ori există nou (adică crearea unei instanțe), înlocuiți-o cu un apel către funcția care devine un obiect pentru dvs. Nu uitați să implementați metodele care utilizează interfața IPoolable necesită!

Menținerea stabilității memoriei este foarte importantă. Vă salvează o mulțime de probleme mai târziu în proiectul dvs. atunci când totul începe să se destrame, cu instanțe nereciclate care răspund în continuare la ascultătorii de evenimente, obiecte care umple memoria pe care o aveți la dispoziție pentru utilizare și cu colectorul de gunoi care rulează și încetinește totul în jos. O recomandare bună este să folosiți întotdeauna utilizarea de obiecte de acum înainte și veți observa că viața dvs. va fi mult mai ușoară.

De asemenea, observați că, deși acest tutorial a fost destinat pentru Flash, conceptele dezvoltate în acesta sunt globale: îl puteți folosi în aplicații AIR, aplicații mobile și oriunde se potrivește. Vă mulțumim pentru lectură!

Cod