Android Go est le futur milliard d'appareils et une limite de 50 Mo. Conférence Yandex

De nouvelles directions pour le développement d'une plateforme déjà familiÚre - c'est toujours intéressant. D'une part, vous élargissez la clientÚle, d'autre part, vous n'investissez pas dans la création de logiciels à partir de zéro, mais en utilisant les développements existants. Mais si la direction est vraiment nouvelle, avec ses spécificités, alors il ne sera pas possible de gérer avec trÚs peu de sang. Lors de la prochaine réunion de la communauté Mosdroid dans notre bureau, le développeur Arthur Vasilov Arturka a parlé de l'adaptation de l'application Yandex au systÚme Android Go.


En moyenne, si vous n'Ă©crivez pas de calculatrice, de rĂ©veil, etc., alors soit vous ĂȘtes trĂšs cool, bien fait et avez tout bien fait, soit votre application prend 150-170 mĂ©gaoctets.

- Je m'appelle Arthur, je suis développeur Android, je travaille sur l'application Yandex. Aujourd'hui, je vais partager avec vous une histoire sur la façon dont nous nous sommes adaptés à Android Go. Je vais vous dire sur quel type de rùteau nous sommes tombés, ce qui n'a pas fonctionné pour nous et comment tout cela fonctionne, pourquoi il est nécessaire.

Une petite digression sur ce dont il s'agit. Android Go est une version spĂ©ciale d'Android conçue pour les appareils Ă  bas prix. Ils coĂ»tent de 60 Ă  100 dollars et sont donc trĂšs faibles, lents, ralentis. Google a donc dĂ©cidĂ© pour eux de crĂ©er leur propre systĂšme afin qu'ils fonctionnent au moins normalement. Il a Ă©tĂ© annoncĂ© sur Google I / O en 2017, soit un an et plusieurs mois se sont Ă©coulĂ©s. Par consĂ©quent, lorsque le mitap a Ă©tĂ© annoncĂ©, une question logique a Ă©tĂ© soulevĂ©e: "Êtes-vous toujours en vie, ou quoi?" J'ai alors dit que tout va bien, tout va bien, je vais le dire. Et maintenant, en tant que hĂ©ros Internet typique, je rĂ©pondrai a posteriori pourquoi cela s'est produit.


Que s'est-il passĂ© exactement? Google a dĂ©clarĂ©: "Nous crĂ©ons un tel systĂšme." Puis il a dit: "D'accord, nous avons besoin de temps pour finaliser" et tout ça. AprĂšs cela, les fournisseurs ont toujours un certain retard dans l'adaptation de cette version pour eux-mĂȘmes. Et la nouvelle version avec nous est dĂ©jĂ  sortie au plus tĂŽt un an plus tard, il n'y a donc rien de surprenant ici. De plus, ils devaient dĂ©cider de faire une telle chose: ils parlent d'appareils bon marchĂ© et il n'est pas clair s'ils en profiteront ou non. De plus, cet appareil doit ĂȘtre neuf, il faut le faire, apprendre Ă  vendre, comprendre comment il fonctionnera.

Le premier smartphone avec Android Go est apparu il n'y a pas si longtemps. Quelque part en avril, les ventes ont probablement commencĂ©, ou peut-ĂȘtre en mai. C'est Nokia 1, il est vendu partout. Je traĂźne ici. Maintenant, Ă  mon avis, il n'y a que neuf de ces smartphones sur le marchĂ©, mais Ă  la fin de l'annĂ©e, ils promettent plus d'une centaine. Et en principe, aucun des principaux acteurs tels que Huawei, Samsung et d'autres n'ont dit leur mot, alors ils ajouteront autre chose, ils ne pourront pas rester Ă  l'Ă©cart d'un marchĂ© aussi important.

Avant le rapport, je suis allé sur la page Android Go et j'ai vu qu'ils faisaient l'édition Android Pie Go. Mais ils n'y ont rien fait, ils ont simplement réduit à l'avance le nombre d'applications installées et leur poids. Ils ont dit: - "Vous avez maintenant deux fois plus d'espace libre." Et les excuses standard: correction de bugs, amélioration des performances, tout. Mais au moins, ils ont appelé, ce qui signifie qu'ils n'ont pas oublié.

Quelles sont les limites de ces appareils, en particulier? PremiĂšrement, ils ont 512 mĂ©gaoctets ou 1 gigaoctet de RAM, 8 ou 16 gigaoctets de stockage. Il est clair que dans de telles conditions, ils sont extrĂȘmement inhibĂ©s et toutes les applications normales sur eux fonctionneront approximativement de la mĂȘme maniĂšre. Pour que les applications fonctionnent avec une adĂ©quation minimale, Google a dĂ©clarĂ©: «Introduisons les exigences suivantes.» Ils sont assez logiques, dĂ©coulent de ce qui Ă©tait sur la diapositive prĂ©cĂ©dente.



Tout d'abord, c'est une bonne performance abstraite. Votre application doit fonctionner correctement et rapidement sur un tel appareil. Nous sommes: «Génial. Nous travaillons. "

De plus, il existe dĂ©jĂ  des numĂ©ros spĂ©cifiques auxquels nous devons correspondre. L'espace occupĂ© aprĂšs le dĂ©ballage et l'installation de l'APK ne dĂ©passe pas 40 mĂ©gaoctets. Parfois, c'est un problĂšme car quelqu'un et l'APK pĂšsent tous les 80 mĂ©gaoctets. Ça va faire mal. De plus, cela ne peut pas ĂȘtre correctement pris et mesurĂ©. Autrement dit, vous ne pouvez pas dire: "Je sais que mon APK pĂšse tellement, donc aprĂšs l'installation, l'application prendra tellement." Tout cela dĂ©pend beaucoup du fournisseur, de la version de l'appareil, d'Android, etc. Mais si votre APK prend 10 mĂ©gaoctets, alors, en principe, tout va bien et vous ne dĂ©passerez jamais ce nombre.

Et maintenant, l'exigence la plus amusante et la plus cool: la RAM consommée lors du travail avec l'application ne doit pas dépasser 50 mégaoctets.

Qui sait combien de RAM sa demande prend en moyenne pendant le fonctionnement? Qui s'est jamais interrogĂ© sur cette question? Y a-t-il quelqu'un avec moins de 100 mĂ©gaoctets? Beau. Mais peut-ĂȘtre que vous mentez. En gĂ©nĂ©ral, en moyenne, si vous n'Ă©crivez pas de calculatrice, de rĂ©veil et ainsi de suite, alors soit vous ĂȘtes trĂšs cool, bien fait et vous avez tout bien fait, soit votre application prend 150-170 mĂ©gaoctets. Le mettre dans 50 mĂ©gaoctets est trĂšs difficile. Par consĂ©quent, le reste du temps, nous en parlerons, nous discuterons de la façon de mettre le globe en hibou, etc.


Que comprend notre conversation sur la mĂ©moire? Il est directement nĂ©cessaire de comprendre ce que nous mesurons exactement, puis quelle est la meilleure façon de le mesurer. Aussi pour les appareils Android Go, il y a une spĂ©cificitĂ© qui doit ĂȘtre prise en compte, et nous en parlerons Ă©galement. Et je vais Ă©galement vous dire des choses gĂ©nĂ©rales, des conseils gĂ©nĂ©raux que vous n'auriez peut-ĂȘtre pas devinĂ©s, mais ils peuvent vraiment vous manger beaucoup de mĂ©moire.

En 2018, aprÚs le passage des E / S Google, vous devez commencer l'histoire de la mémoire en faisant référence à ce rapport . Qui l'a regardé?

Super. Les 180 personnes restantes savent quoi faire dans un avenir proche. À mon avis, c'est l'un des meilleurs rapports sur Google I / O. Mec a dit des choses incroyablement cool. Il a tout racontĂ© sur les Ă©tagĂšres, enfin, et avec beaucoup de dĂ©tails profonds. Ceux qui l'ont regardĂ© et qui s'en souviennent bien remarqueront probablement que j'ai copiĂ© certaines choses Ă  partir de lĂ , car c'est impossible autrement, il a tout dit, alors je vais le rĂ©pĂ©ter.

Que mesurons-nous? Il y avait une telle chose appelée PSS (Proportional Set Size). Autrement dit, la RAM dans Android est représentée par environ 40 blocs de kilo-octets, et ces blocs peuvent appartenir entiÚrement à l'application ou fouiller entre les processus et les applications.

Et la question est: comment comprendre exactement Ă  quelle application relier cette mĂ©moire partagĂ©e? Il existe plusieurs approches, elles sont assez logiques. PSS dit que si la mĂ©moire tente entre N processus, nous supposerons que votre application possĂšde 1 / N de cette mĂ©moire. Et exactement de la mĂȘme maniĂšre, il y a la taille de l'ensemble rĂ©sidentiel et la taille de l'ensemble unique, qui disent que «Aucune de la mĂ©moire partagĂ©e ne m'appartient» et «Tout appartient». En principe, PSS est le plus logique ici.

Comment pouvez-vous mesurer exactement la mémoire consommée? Ici, tout est simple. Soit c'est un profileur dans Android Studio, soit c'est dumpsys. Il existe bien sûr d'autres outils. Ils peuvent vous donner des résultats plus détaillés, quelque chose de plus compliqué, mais le problÚme est que pour comprendre leurs résultats, les utiliser tout cela est trÚs, trÚs difficile. Souvent, vous avez besoin de root ou d'une version personnalisée d'Android. Et en gros vous n'en avez pas besoin, les deux premiers outils suffisent.


Lien depuis la diapositive

Je ne parlerai pas du profileur dans Android Studio, je pense que beaucoup de gens l'ont utilisĂ©. Qui n'a pas utilisĂ©, assurez-vous de pousser. En particulier, j'ai fourni le lien ci-dessous - juste un bon article de la documentation avec vidĂ©o, comment l'utiliser, avec des dĂ©mos. Et, en principe, tout est clair. Il montre oĂč va votre mĂ©moire, le montre en temps rĂ©el. La seule chose Ă  retenir est qu'elle impose nĂ©anmoins certaines erreurs qui dĂ©coulent du fait que nous mesurons constamment cette mĂ©moire. Mais ils sont dans des limites acceptables.


Lien depuis la diapositive

Dumpsys est une console simple qui ne nécessite rien de vous, juste un téléphone connecté et un adb. Et vous pouvez exécuter cette commande: appelez dumpsys meminfo, passez-lui le paquet et il vous renverra quelque chose comme ça. Et si nous sommes intéressés par la consommation de notre application, nous pouvons examiner spécifiquement TOTAL, qui indique que "votre application consomme environ 168 mégaoctets". Beaucoup, mais que faire?


Lien depuis la diapositive

Il vous montre également une ventilation exacte de la quantité de mémoire nécessaire dans cette mémoire consommée. Il y a différentes sections ici, elles sont complexes, nous en parlerons plus loin, mais pour l'instant nous pouvons remarquer l'essentiel - ce sont Java Heap, nos objets Java, et tout est plus compliqué.

Quoi d'autre est important? La mĂ©moire est une chose trĂšs sensible Ă  toutes sortes de tests et Ă  toutes sortes de conditions externes. Autrement dit, tous les tests que vous devez effectuer autant que possible dans les mĂȘmes conditions. Il est clair que cela devrait idĂ©alement ĂȘtre un seul appareil, car la mĂ©moire dĂ©pend de la version d'Android. Il suffit de rappeler les diffĂ©rences entre les quatre et les cinq. Cela dĂ©pend de la taille ou de la rĂ©solution de l'Ă©cran, car plus la taille de l'Ă©cran est grande, plus le contenu s'introduit, plus vous avez besoin de mĂ©moire pour tout dessiner. Plus la rĂ©solution est Ă©levĂ©e, plus votre bitmap occupe de pixels et plus de mĂ©moire est nĂ©cessaire pour les stocker.

Le scĂ©nario d'application affecte Ă©galement les tests. Vous pouvez lire le texte ou faire dĂ©filer la galerie avec un tas d'images. Et il est clair oĂč il y aura plus de mĂ©moire.

Une autre chose importante est la charge sur l'appareil. Autrement dit, vous pouvez tout avoir de la mĂȘme façon, mais dans un cas, votre application est la seule qui fonctionne pour vous, et dans l'autre cas, vous avez un tas d'applications qui font quelque chose en arriĂšre-plan, tĂ©lĂ©chargent quelque chose, le suppriment, travaillent au premier plan , et en mĂȘme temps, votre mĂ©moire est simplement Ă©puisĂ©e, car vous devez la donner Ă  d'autres applications. Par consĂ©quent, idĂ©alement, il est prĂ©fĂ©rable de prendre et de tuer d'abord toutes les autres applications qui fonctionnent, pas la vĂŽtre. Tout ce que vous pouvez atteindre, puis tuer. Juste dans ce cas, et PSS vous remerciera certainement, car il ne sera pas nĂ©cessaire de tĂątonner la mĂ©moire entre les processus.


Vous pouvez, par exemple, prendre et voir les informations actuelles sur la mémoire libre, sur la mémoire occupée. Il vous apportera quelque chose comme un signe qui dit: «Ici, j'ai tellement de mémoire libre, tellement de mémoire en cache, tellement de mémoire occupée.» Et si vous avez 200 à 250 mégaoctets de mémoire libre pour votre bien-aimé là-bas, alors c'est bien, trÚs probablement, alors rien n'affectera vos tests.

Peut-ĂȘtre que quelqu'un a maintenant la question "Pourquoi ai-je besoin de tout cela?" C'est un tel casse-tĂȘte dans lequel je dirai en outre la motivation de tout cela.

PremiĂšrement, mĂȘme si vous ne faites rien sous Android Go maintenant et pensez qu'il est mort, il pourrait bien se dĂ©velopper, venir Ă  vous, et Ă  un moment donnĂ©, vous devrez faire face Ă  tout cela.

La deuxiĂšme chose que je considĂšre trĂšs importante est que vous pouvez simplement faire des tests de rĂ©gression Ă  partir de la mĂ©moire. Autrement dit, vous pouvez simplement Ă©crire un script qui exĂ©cutera l'application, fera des vidages, prendra de telles mesures et observera comment ces indicateurs changent entre les versions. Un tel script peut ĂȘtre Ă©crit en quelques heures, l'infrastructure peut ĂȘtre configurĂ©e plus longtemps, mais il me semble que c'est une bonne chose.

Si nous parlons des spĂ©cificitĂ©s d'Android Go - notre troisiĂšme point dans la lutte contre la mĂ©moire - alors il y a de bonnes nouvelles. Tout d'abord, personne ne vous oblige vraiment Ă  respecter ces restrictions. Autrement dit, vous pouvez utiliser l'application telle quelle, la mettre sur l'appareil avec Android Go, et tout va bien. Le problĂšme est que l'utilisateur est trĂšs susceptible de vous retirer car vous prenez beaucoup de place. Votre application peut Ă©galement s'exĂ©cuter lentement et avoir beaucoup de mĂ©moire, oui. Mais jusqu'Ă  prĂ©sent, personne ne l'a interdit, car sinon il n'y aurait eu aucune application autre que les applications Google sur Android Go. Mais si cette chose se dĂ©veloppe, beaucoup s'adapteront Ă  de telles conditions, puis, au final, votre application peut simplement ĂȘtre rĂ©duite dans l'Ă©mission Android Go, ou dire comment l'utilisateur installe l'application sur son smartphone Android Go, ils peuvent lui montrer Alert : "Mec, l'application ne fonctionne pas bien avec Android Go. Peut-ĂȘtre que vous ne le mettrez pas? "

Il y a un autre point - vous pouvez exclure manuellement les appareils Android Go de Google Play, c'est-Ă -dire que l'application ne peut pas ĂȘtre installĂ©e sur les appareils Android Go. Il est apparu il n'y a pas si longtemps.

Et Google est également assez intelligent, et les 50 mégaoctets qui sonnaient dans le titre du rapport ne sont pas un nombre fixe, cela dépend de la résolution de l'appareil, de la taille de l'écran et, en outre, du type d'application. Par exemple, les jeux sont alloués plus, à mon avis, 115 mégaoctets. En principe, cela est compréhensible.


Lien depuis la diapositive

Et si nous parlons directement de ce test? Il y a un autre point, qui nous prĂ©occupe beaucoup en particulier: travailler avec des prĂ©rĂ©glages. Lorsque les fournisseurs fabriquent un nouveau tĂ©lĂ©phone, ils y mettent souvent un ensemble d'applications prĂ©installĂ©es. En particulier, nous y sommes trĂšs impliquĂ©s, et le problĂšme est qu'ils y exĂ©cutent des choses comme Compatibility Test Suite. Ce sont des tests google, ils les exĂ©cutent. Et lĂ , si votre application ne correspond pas Ă  ces 50 mĂ©gaoctets, alors tout est mauvais, et votre application ne peut pas ĂȘtre prĂ©installĂ©e sur un tel appareil.

Malheureusement, les tests que Google fait ne sont pas open source, je ne peux pas leur dire, ils sont sous le NDA, mais de bons développeurs de Google ont écrit un tel article sur Android Go, et là, en principe, il y a des recommandations qui sont assez bonnes

Autrement dit, tout est banal. Nous lançons l'application. Nous attendons 5 secondes pour que tout se charge. Nous faisons dumpsys, écrivons la valeur TOTALE, exécutons un tas de fois, nous obtenons le résultat. Tout est trÚs simple et banal.


La seule chose est qu'ils n'ont pas pris en compte une si petite fonctionnalitĂ© dans leur article, ou, peut-ĂȘtre, n'en ont pas parlĂ© - il existe une chose telle que travailler dans un autre processus, et ils le font souvent afin de lutter contre la consommation de mĂ©moire.

Qui pense que c'est bon pour Android Go? Et qui pense que c'est mauvais? Bravement.


Le problÚme est que, oui, c'est mauvais, car au final, la mémoire de votre application consommée est comptée dans tous les processus. Si nous faisons un tel processus vide sans rien et prenons des vidages de ce processus particulier, nous verrons qu'il faut 7 mégaoctets. 5-8 mégaoctets - c'est une telle surcharge de la création du processus. Par conséquent, lorsque nous nous battons pour que chaque mégaoctet comprenne tout en 50, une telle chose nous est trÚs mal donnée. En particulier, supposons que Yandex possÚde la bibliothÚque Yandex.Metrica la plus populaire, elle fonctionne également dans un processus différent, ce qui peut également nous faire mal.


Par consĂ©quent, si certaines bibliothĂšques externes viennent Ă  vous, par exemple, vous pouvez simplement dire: «Mec, veuillez travailler dans le processus principal. Je suis d'accord que cela peut ĂȘtre plus lent, mais cela ne consommera pas de mĂ©moire inutile. » C'est donc aussi un point subtil.



Si nous parlons de consommation de mĂ©moire, passons Ă  cette plaque, qui est lĂ . Nous prenons un projet vide, et exĂ©cutons ce dumpsys, dĂ©marrons cette entreprise, et nous voyons que 23 mĂ©gaoctets de 50 sont dĂ©jĂ  pris. Vide "Bonjour, monde!" sans rien, il suffit d'activer avec le texte. Ça devient assez triste. Il devient encore plus triste de constater que nous pouvons influencer directement un paramĂštre tel que Java Heap, c'est-Ă -dire que ce sont directement nos objets Java que nous pouvons suivre explicitement, que nous pouvons supprimer, rĂ©duire et au moins interagir d'une maniĂšre ou d'une autre.

Et il est difficile d'interagir normalement avec tout cela, car il s'agit de tout code cadre et directement à partir de Java, vous n'avez tout simplement pas d'outils normaux pour comprendre comment tout cela est utilisé. Mais la bonne nouvelle est que nous pouvons influencer tout cela indirectement, alors parlons de ce qui existe.


Qu'est-ce que Java Heap est comprĂ©hensible. Ce qui est Native Heap est Ă©galement assez logique Ă  assumer. Ce sont les mĂȘmes allocations, seulement positives. Ils proviennent du framework et de vos bibliothĂšques natives, des fichiers .so, etc.

Le code est directement lié au stockage de code. C'est la taille de votre .dex, c'est votre .so, ce sont les ressources, les fichiers mmap et tout ça. Autrement dit, moins il y a de code, mieux c'est. La vérité la plus simple qui fonctionne en général sur tout.

Stack est une pile de threads Java / C ++. C'est-à-dire que chaque thread a sa propre pile d'appels, donc chaque thread crée une zone de mémoire spécifique pour stocker tout cela.

Les graphiques sont un rendu d'interface utilisateur. Il y a des bitmaps partiellement stockés qui sont dessinés et ce qui y est lié.

Autre privé, SystÚme - c'est quelque chose que nous ne pouvons pas influencer du tout, et, en fait, tout le reste qui n'est pas trÚs catégorisable.



Si nous parlons de bibliothÚques natives, alors, disons que vous pouvez prendre une application vide ... vous pouvez prendre l'application habituelle que nous avons et en prendre des dumpsys, voir que cela prend 146 mégaoctets. Et si vous allez couper quelque chose ... en particulier, j'ai pris et scié les deux plus grandes bibliothÚques natives, dont nous avons un total de 15 mégaoctets, et prenez dumpsys aprÚs cela, vous pouvez facilement voir que nous avons perdu la consommation du tas natif et du code. Autrement dit, avec un geste si simple, nous nous sommes économisés environ 35 mégaoctets. Pas assez mal.



Streams. Faisons le test le plus simple. Il y a une application vide, et il y a la mĂȘme application vide, oĂč nous prenons et faisons une boucle qui fera le nouveau Thread (), dormira dedans pendant cinq secondes et dĂ©marrera ce thread. Vous pouvez voir que dans ce cas, la pile est considĂ©rablement reconstituĂ©e. Autrement dit, nous pouvons influencer tout cela, mais indirectement, en rĂ©duisant le nombre de threads dans le code Java. Autrement dit, grĂące Ă  des objets Java, y compris influencer tous ces emplacements qui se trouvent dans d'autres Ă©lĂ©ments.



Si vous continuez Ă  parler des threads, vous pouvez facilement voir ce que vous avez pour les threads, ce qu'ils font dans l'application. Il existe diffĂ©rents outils. Vous pouvez utiliser le mĂȘme systrace, vous pouvez simplement utiliser la console pour trouver l'id de votre processus et saisir ps par quels threads se trouvent dans le systĂšme.

Si vous vous dĂ©brouillez bien et utilisez toutes sortes de ThreadFactory pour nommer vos threads, alors vous pouvez comprendre ce que vous devriez avoir, ce qui ne devrait pas l'ĂȘtre, et ainsi rĂ©duire le tout, car avoir 100 threads dans l'application n'a pas de sens.

Que peut-on faire d'autre? Il existe un ensemble bien connu de conseils sur l'accordĂ©on: surveillez les fuites, utilisez des pools, ne crĂ©ez pas d'objets lorsque vous n'en avez pas besoin, et toutes ces bĂȘtises, tout cela s'applique. Super.

Il existe une bonne corrĂ©lation entre la quantitĂ© de mĂ©moire utilisĂ©e par votre application et sa taille. Autrement dit, plus votre application est grande, plus elle consommera de mĂ©moire, donc, d'une maniĂšre simple, rĂ©duisez le poids de l'APK, rĂ©duisez le poids de .dex, rĂ©duisez .so, supprimez tout ce qui peut ĂȘtre supprimĂ©.

. , , , . , ? — — , . , , . . — .


dumpsys, , , . , , View, WebView, Assets, .


, , , , , SharedPreference SQLite - , , , .



, , Assets. , , – . 1,5 . -, , . .


— WebView. , WebView, , dumpsys, 100 , . , , - , , 300 — . , WebView , . -.



Google . . WebView, Google, - . WebView. D'accord.



Google Go, Android Go-. , - layout , . , , , Chrome Tabs, Chrome . .

Android Go, ? , Android Go, .


Il y a plusieurs façons. -, , , Android Go . , , . , , , - build , - , , - . APK, , — uses-feature «low-memory», APK.

, , Android Go. Google. , , Go- .

, ? , , - , : «, - . ». . , , , , , - , , - . , , , , Google, Google , . , , , Android Go , , .

, , , -. .



, Google I/O: , - . , , , , , - , . , « , . ». . , . , .

? , . — , , 98% . , , , . . - Android Go-. , YouTube Go . YouTube , , , . . , Android Go , , , — . — Facebook Lite, Twitter Lite, . Lite, - . -, , . Facebook, . , , . , — , Android Go.

? Stack Overflow, 1000 . . , , , . , , — dumpsys.

, Android , 512 , Android 4.4. API , .

, . , . — , .

, : « ». : «». . — , . - 170 . . - . ? , , , , - . - — . 105 . .


. build- -. densitySplit, , resConfig, . , , — . ProGuard . , ProGuard , , , , , stacktraces.

. , , Android Go- .

- 64 . : «, ». 14 , 4 , 60 . , . . , . — , , , , . , , , Android Go, . 10 , APK , , . , , .

, — . , . , , - , , . . , , . .

Source: https://habr.com/ru/post/fr423493/


All Articles