Android रनटाइम (एआरटी) टीम ने कंपाइल किए गए कोड या पीक मेमोरी रिग्रेशन से समझौता किए बिना, कंपाइल करने में लगने वाले समय को 18% कम कर दिया है. यह सुधार, साल 2025 में शुरू की गई हमारी उस पहल का हिस्सा था जिसका मकसद कंपाइल के समय को कम करना था. हालांकि, इसके लिए मेमोरी के इस्तेमाल या कंपाइल किए गए कोड की क्वालिटी से समझौता नहीं किया गया.
एआरटी के लिए, कंपाइल के समय को ऑप्टिमाइज़ करना ज़रूरी है. उदाहरण के लिए, जस्ट-इन-टाइम (जेआईटी) कंपाइलेशन के दौरान, यह ऐप्लिकेशन की परफ़ॉर्मेंस और डिवाइस की परफ़ॉर्मेंस पर सीधे तौर पर असर डालता है. तेज़ी से कंपाइल होने की वजह से, ऑप्टिमाइज़ेशन शुरू होने में कम समय लगता है. इससे उपयोगकर्ताओं को बेहतर और ज़्यादा रिस्पॉन्सिव अनुभव मिलता है. इसके अलावा, जेआईटी और अहेड-ऑफ़-टाइम (एओटी), दोनों के लिए कंपाइल के समय में सुधार होने से, कंपाइलेशन की प्रोसेस के दौरान कम संसाधनों का इस्तेमाल होता है. इससे बैटरी लाइफ़ और डिवाइस के तापमान को कंट्रोल करने में मदद मिलती है. खास तौर पर, कम सुविधाओं वाले डिवाइसों पर.
कंपाइल के समय को कम करने से जुड़े कुछ सुधार, जून 2025 में Android के रिलीज़ के साथ लॉन्च किए गए हैं. बाकी सुधार, साल के आखिर में Android के रिलीज़ के साथ उपलब्ध होंगे. इसके अलावा, Android 12 और इसके बाद के वर्शन इस्तेमाल करने वाले सभी लोगों को मेनलाइन अपडेट के ज़रिए ये सुधार मिलेंगे.
ऑप्टिमाइज़ करने वाले कंपाइलर को ऑप्टिमाइज़ करना
कंपाइलर को ऑप्टिमाइज़ करना हमेशा एक मुश्किल काम होता है. तेज़ स्पीड पाने के लिए, आपको कुछ न कुछ समझौता करना पड़ता है. हमने खुद के लिए एक बहुत ही साफ़ और मुश्किल लक्ष्य तय किया: कंपाइलर को तेज़ बनाना, लेकिन ऐसा करते समय मेमोरी रिग्रेशन को शामिल न करना. साथ ही, इससे जनरेट होने वाले कोड की क्वालिटी को कम न करना. अगर कंपाइलर तेज़ी से काम करता है, लेकिन ऐप्लिकेशन धीरे-धीरे चलते हैं, तो इसका मतलब है कि हम सफल नहीं हुए.
हमारा मकसद था कि हम अपना समय और मेहनत लगाकर, इस समस्या की गहराई से जांच करें और इन सख्त शर्तों को पूरा करने के लिए बेहतर समाधान ढूंढें. आइए, इस बारे में ज़्यादा जानें कि हम सुधार के लिए ज़रूरी पहलुओं को कैसे ढूंढते हैं. साथ ही, अलग-अलग समस्याओं के सही समाधान कैसे खोजते हैं.
ऑप्टिमाइज़ेशन के लिए सही और संभावित सुझाव ढूंढना
किसी मेट्रिक को ऑप्टिमाइज़ करने से पहले, आपको उसे मेज़र करना होगा. ऐसा न करने पर, आपको कभी भी यह पता नहीं चल पाएगा कि आपने इसमें सुधार किया है या नहीं. खुशकिस्मती से, कंपाइल का समय काफ़ी हद तक एक जैसा रहता है. हालांकि, इसके लिए आपको कुछ सावधानियां बरतनी होंगी. जैसे, बदलाव से पहले और बाद में मेज़र करने के लिए एक ही डिवाइस का इस्तेमाल करना और यह पक्का करना कि आपके डिवाइस का तापमान ज़्यादा न हो. इसके अलावा, हमारे पास डिटरमिनिस्टिक मेज़रमेंट भी हैं. जैसे, कंपाइलर के आंकड़े. इनसे हमें यह समझने में मदद मिलती है कि बैकग्राउंड में क्या हो रहा है.
इन सुधारों के लिए, हम अपने डेवलपमेंट के समय का इस्तेमाल कर रहे थे. इसलिए, हम जितनी जल्दी हो सके, उतनी जल्दी बदलाव करना चाहते थे. इसका मतलब है कि हमने समाधानों के प्रोटोटाइप बनाने के लिए, कुछ चुनिंदा ऐप्लिकेशन चुने. इनमें पहले पक्ष के ऐप्लिकेशन, तीसरे पक्ष के ऐप्लिकेशन, और Android ऑपरेटिंग सिस्टम शामिल था. इसके बाद, हमने मैन्युअल और ऑटोमेटेड टेस्टिंग के ज़रिए बड़े पैमाने पर यह पुष्टि की कि आखिरी बार लागू किया गया बदलाव सही था.
चुने गए APKs के सेट के साथ, हम मैन्युअल तरीके से कंपाइल करने की प्रोसेस को स्थानीय तौर पर ट्रिगर करेंगे. साथ ही, कंपाइलेशन की प्रोफ़ाइल हासिल करेंगे और pprof का इस्तेमाल करके यह देखेंगे कि हमारा समय कहां खर्च हो रहा है.
pprof में प्रोफ़ाइल के फ़्लेम ग्राफ़ का उदाहरण
pprof टूल बहुत असरदार है. इसकी मदद से, डेटा को स्लाइस, फ़िल्टर, और क्रम से लगाया जा सकता है. उदाहरण के लिए, यह देखा जा सकता है कि कंपाइलर के किन फ़ेज़ या तरीकों में सबसे ज़्यादा समय लग रहा है. हम pprof के बारे में ज़्यादा जानकारी नहीं देंगे. बस इतना जान लें कि अगर बार बड़ा है, तो इसका मतलब है कि कंपाइलेशन में ज़्यादा समय लगा.
इनमें से एक व्यू “बॉटम अप” है. इसमें यह देखा जा सकता है कि कौनसे तरीके सबसे ज़्यादा समय ले रहे हैं. नीचे दी गई इमेज में, Kill नाम का एक तरीका देखा जा सकता है. यह कंपाइल के समय का 1% से ज़्यादा हिस्सा लेता है. ब्लॉग पोस्ट में, कुछ अन्य बेहतरीन तरीकों के बारे में भी बताया जाएगा.
प्रोफ़ाइल का बॉटम अप व्यू
हमारे ऑप्टिमाइज़िंग कंपाइलर में, ग्लोबल वैल्यू नंबरिंग (जीवीएन) नाम का एक फ़ेज़ होता है. आपको यह चिंता करने की ज़रूरत नहीं है कि यह पूरी तरह से क्या करता है. हालांकि, काम की बात यह है कि इसमें `Kill` नाम का एक तरीका है, जो फ़िल्टर के हिसाब से कुछ नोड मिटा देगा. इसमें समय लगता है, क्योंकि इसे सभी नोड को एक-एक करके देखना और जांचना पड़ता है. हमने देखा है कि कुछ मामलों में हमें पहले से पता होता है कि जांच का नतीजा गलत होगा. भले ही, उस समय हमारे पास कितने भी नोड मौजूद हों. इन मामलों में, हम इटरेट करने की प्रोसेस को पूरी तरह से स्किप कर सकते हैं. इससे यह 1.023% से घटकर ~0.3% हो जाता है और जीवीएन का रनटाइम ~15% बेहतर हो जाता है.
काम के ऑप्टिमाइज़ेशन लागू करना
हमने इस बारे में बात की कि समय को कैसे मेज़र किया जाए और यह कैसे पता लगाया जाए कि समय कहां खर्च हो रहा है. हालांकि, यह सिर्फ़ शुरुआत है. अगला चरण, कंपाइल करने में लगने वाले समय को ऑप्टिमाइज़ करना है.
आम तौर पर, ऊपर दिए गए `Kill` जैसे मामले में, हम यह देखते हैं कि नोड को कैसे दोहराया जाए और उसे तेज़ी से कैसे किया जाए. उदाहरण के लिए, चीज़ों को एक साथ करना या एल्गोरिदम को बेहतर बनाना. हमने शुरुआत में यही तरीका अपनाया था. हालांकि, जब हमें कोई समाधान नहीं मिला, तो हमने सोचा कि “एक मिनट रुकें…” और देखा कि कुछ मामलों में, समाधान यह था कि हमें कुछ भी दोहराना नहीं था! इस तरह के ऑप्टिमाइज़ेशन करते समय, छोटी-छोटी चीज़ों पर ध्यान देने की वजह से, बड़ी चीज़ों पर ध्यान नहीं जा पाता.
अन्य मामलों में, हमने कुछ अलग-अलग तकनीकों का इस्तेमाल किया है. जैसे:
- ह्यूरिस्टिक का इस्तेमाल करके यह तय करना कि कोई ऑप्टिमाइज़ेशन, काम के नतीजे देने में नाकाम रहेगा या नहीं. अगर ऐसा होता है, तो उसे छोड़ा जा सकता है
- कंप्यूट किए गए डेटा को कैश मेमोरी में सेव करने के लिए, अतिरिक्त डेटा स्ट्रक्चर का इस्तेमाल करना
- मौजूदा डेटा स्ट्रक्चर में बदलाव करना, ताकि स्पीड बूस्ट की सुविधा मिल सके
- कुछ मामलों में साइकल से बचने के लिए, नतीजों को लेज़ी तरीके से कंप्यूट किया जा रहा है
- सही एब्स्ट्रैक्शन का इस्तेमाल करें - ग़ैर-ज़रूरी सुविधाओं से कोड की रफ़्तार धीमी हो सकती है
- अक्सर इस्तेमाल किए जाने वाले पॉइंटर को कई लोड के ज़रिए चेज़ करने से बचें
हमें कैसे पता चलेगा कि ऑप्टिमाइज़ेशन से फ़ायदा होगा या नहीं?
सबसे अच्छी बात यह है कि आपको ऐसा करने की ज़रूरत नहीं है. कभी-कभी ऐसा होता है कि किसी हिस्से को कंपाइल करने में ज़्यादा समय लगता है. इसके बाद, उसे बेहतर बनाने के लिए डेवलपमेंट में समय देने के बाद भी, आपको कोई समाधान नहीं मिलता. ऐसा हो सकता है कि कुछ करने की ज़रूरत न हो, इसे लागू करने में बहुत ज़्यादा समय लगे, इससे कोई दूसरी मेट्रिक काफ़ी कम हो जाए, कोड बेस की जटिलता बढ़ जाए वगैरह. इस ब्लॉग पोस्ट में आपको ऑप्टिमाइज़ेशन के जितने भी सफल उदाहरण दिख रहे हैं उनके अलावा, ऐसे अनगिनत उदाहरण भी हैं जो सफल नहीं हो पाए.
अगर आप भी ऐसी ही स्थिति में हैं, तो कम से कम काम करके मेट्रिक को बेहतर बनाने की कोशिश करें. इसका मतलब है कि:
- पहले से इकट्ठा की गई मेट्रिक या सिर्फ़ अनुमान के आधार पर
- तेज़ी से और रफ़ प्रोटोटाइप बनाकर अनुमान लगाना
- कोई समाधान लागू करें.
अपने समाधान की कमियों का अनुमान लगाना न भूलें. उदाहरण के लिए, अगर आपको अतिरिक्त डेटा स्ट्रक्चर पर भरोसा करना है, तो आपको कितनी मेमोरी इस्तेमाल करनी है?
समस्या को गहराई से समझना
देर न करते हुए, चलिए हमने जो बदलाव किए हैं उनमें से कुछ के बारे में बात करते हैं.
हमने FindReferenceInfoOf नाम के तरीके को ऑप्टिमाइज़ करने के लिए बदलाव किया. इस तरीके से, किसी एंट्री को ढूंढने के लिए वेक्टर की लीनियर सर्च की जा रही थी. हमने उस डेटा स्ट्रक्चर को अपडेट किया, ताकि उसे निर्देश के आईडी के हिसाब से इंडेक्स किया जा सके. इससे FindReferenceInfoOf, O(n) के बजाय O(1) हो जाएगा. साथ ही, हमने वेक्टर को पहले से ही तय कर दिया है, ताकि साइज़ बदलने की ज़रूरत न पड़े. हमने मेमोरी को थोड़ा बढ़ाया, क्योंकि हमें एक अतिरिक्त फ़ील्ड जोड़ना था. इससे यह पता चलता था कि हमने वेक्टर में कितनी एंट्री डाली हैं. हालांकि, यह एक छोटा सा समझौता था, क्योंकि पीक मेमोरी नहीं बढ़ी. इससे हमारे LoadStoreAnalysis फ़ेज़ की स्पीड 34 से 66% तक बढ़ गई. इससे कंपाइल करने के समय में ~0.5 से 1.8% तक की कमी आई.
हमारे पास HashSet का एक कस्टम वर्शन है, जिसका इस्तेमाल हम कई जगहों पर करते हैं. इस डेटा स्ट्रक्चर को बनाने में काफ़ी समय लग रहा था. हमें इसकी वजह पता चल गई है. कई साल पहले, इस डेटा स्ट्रक्चर का इस्तेमाल सिर्फ़ कुछ जगहों पर किया जाता था. ये ऐसी जगहें थीं जहां बहुत बड़े HashSet का इस्तेमाल किया जाता था. साथ ही, इसे ऑप्टिमाइज़ करने के लिए इसमें बदलाव किया गया था. हालांकि, आजकल इसका इस्तेमाल उल्टी दिशा में किया जाता है. इसमें सिर्फ़ कुछ एंट्री होती हैं और यह कम समय के लिए होता है. इसका मतलब है कि हम इस बड़े HashSet को बनाकर साइकल बर्बाद कर रहे थे. हालांकि, हमने इसे खारिज करने से पहले सिर्फ़ कुछ एंट्री के लिए इस्तेमाल किया. इस बदलाव से, हमने कंपाइल के समय को करीब 1.3 से 2% तक कम किया. एक और फ़ायदा यह हुआ कि मेमोरी का इस्तेमाल करीब 0.5 से 1% तक कम हो गया, क्योंकि हम पहले की तरह बड़े डेटा स्ट्रक्चर का इस्तेमाल नहीं कर रहे थे.
हमने कंपाइल के समय को ~0.5-1% तक कम किया है. इसके लिए, हमने लैम्ब्डा को रेफ़रंस के तौर पर डेटा स्ट्रक्चर पास किया, ताकि उन्हें कॉपी करने से बचा जा सके. यह एक ऐसी गड़बड़ी थी जो ओरिजनल समीक्षा में छूट गई थी और सालों तक हमारे कोड बेस में मौजूद रही. pprof में प्रोफ़ाइलों को देखने के बाद, हमें पता चला कि ये तरीके कई डेटा स्ट्रक्चर बना रहे थे और उन्हें मिटा रहे थे. इस वजह से, हमने उनकी जांच की और उन्हें ऑप्टिमाइज़ किया.
हमने कंपाइल किए गए आउटपुट को लिखने के फ़ेज़ को तेज़ किया. इसके लिए, हमने कंप्यूट की गई वैल्यू को कैश मेमोरी में सेव किया. इससे कंपाइल करने के कुल समय में करीब 1.3 से 2.8% की कमी आई. हालांकि, अतिरिक्त बुककीपिंग की वजह से मेमोरी का इस्तेमाल काफ़ी बढ़ गया. इस वजह से, ऑटोमेटेड टेस्टिंग ने हमें मेमोरी रिग्रेशन के बारे में सूचना दी. इसके बाद, हमने उसी कोड को फिर से देखा और एक नया वर्शन लागू किया. इससे न सिर्फ़ मेमोरी रिग्रेशन की समस्या हल हुई, बल्कि कंपाइल के समय में भी ~0.5-1.8% की कमी आई! इस दूसरे बदलाव में, हमें इस चरण के काम करने के तरीके को फिर से तैयार करना पड़ा और उसे नए सिरे से समझना पड़ा. ऐसा इसलिए, ताकि दो डेटा स्ट्रक्चर में से किसी एक से छुटकारा पाया जा सके.
हमारे ऑप्टिमाइज़िंग कंपाइलर में एक फ़ेज़ होता है, जो बेहतर परफ़ॉर्मेंस पाने के लिए फ़ंक्शन कॉल को इनलाइन करता है. इनलाइन करने के लिए कौनसे तरीके चुनने हैं, यह तय करने के लिए हम कोई भी कंप्यूटेशन करने से पहले, दोनों ह्यूरिस्टिक का इस्तेमाल करते हैं. साथ ही, काम पूरा करने के बाद, लेकिन इनलाइन करने की प्रोसेस को फ़ाइनल करने से ठीक पहले, आखिरी जांच करते हैं. अगर इनमें से किसी को भी लगता है कि इनलाइन करने से कोई फ़ायदा नहीं होगा (उदाहरण के लिए, बहुत सारे नए निर्देश जोड़े जाएंगे), तो हम मेथड कॉल को इनलाइन नहीं करते.
हमने “फ़ाइनल चेक” कैटगरी से दो चेक को “ह्यूरिस्टिक” कैटगरी में ट्रांसफ़र किया है. इससे यह अनुमान लगाया जा सकता है कि इनलाइनिंग सफल होगी या नहीं. इसके लिए, हमें ज़्यादा समय लेने वाली कंप्यूटेशन करने की ज़रूरत नहीं पड़ती. यह एक अनुमान है, इसलिए यह पूरी तरह से सटीक नहीं है. हालांकि, हमने पुष्टि की है कि हमारे नए ह्यूरिस्टिक, पहले से इनलाइन किए गए 99.9% कोड को कवर करते हैं. इससे परफ़ॉर्मेंस पर कोई असर नहीं पड़ता. इनमें से एक ह्यूरिस्टिक, ज़रूरी DEX रजिस्टर के बारे में था. इससे करीब 0.2 से 1.3% तक सुधार हुआ. दूसरा ह्यूरिस्टिक, निर्देशों की संख्या के बारे में था. इससे करीब 2% तक सुधार हुआ.
हमने BitVector को कस्टम तरीके से लागू किया है. इसका इस्तेमाल हम कई जगहों पर करते हैं. हमने कुछ तय साइज़ के बिट वेक्टर के लिए, साइज़ बदलने लायक BitVector क्लास को आसान BitVectorView से बदल दिया है. इससे कुछ इनडायरेक्शन और रन-टाइम रेंज की जांच की ज़रूरत नहीं पड़ती. साथ ही, बिट वेक्टर ऑब्जेक्ट बनाने की प्रोसेस तेज़ हो जाती है.
इसके अलावा, BitVectorView क्लास को स्टोरेज के टाइप के आधार पर टेंप्लेट किया गया था. इसमें uint32_t को पुराने BitVector के तौर पर हमेशा इस्तेमाल करने के बजाय, ऐसा किया गया था. इससे 64-बिट वाले प्लैटफ़ॉर्म पर, Union() जैसे कुछ ऑपरेशन एक साथ दोगुनी बिट प्रोसेस कर सकते हैं. Android OS को कंपाइल करते समय, जिन फ़ंक्शन पर असर पड़ा था उनके सैंपल में कुल मिलाकर 1% से ज़्यादा की कमी आई. इसके लिए, कई बदलाव किए गए [1, 2, 3, 4, 5, 6]
अगर हम सभी ऑप्टिमाइज़ेशन के बारे में विस्तार से बात करें, तो हमें पूरा दिन लग जाएगा! अगर आपको कुछ और ऑप्टिमाइज़ेशन के बारे में जानना है, तो यहां दिए गए कुछ अन्य बदलावों को देखें:
- कंपाइल करने में लगने वाले समय को करीब 0.6 से 1.6% तक कम करने के लिए, बुककीपिंग जोड़ें.
- अगर हो सके, तो साइकल से बचने के लिए, डेटा को लेज़ी तरीके से कंप्यूट करें.
- अपने कोड को रीफ़ैक्टर करें, ताकि प्रीकंप्यूटिंग के काम को स्किप किया जा सके. ऐसा तब करें, जब इसका इस्तेमाल न किया जा रहा हो.
- जब ऐलोकेटर को अन्य जगहों से आसानी से हासिल किया जा सकता हो, तब एक-दूसरे पर निर्भर लोड चेन से बचें.
- बेवजह के काम से बचने के लिए, जांच करने का एक और मामला.
- रजिस्टर ऐलोकेटर में रजिस्टर टाइप (कोर/एफ़पी) पर, बार-बार ब्रांचिंग करने से बचें.
- पक्का करें कि कुछ कलेक्शन, कंपाइल के समय शुरू किए गए हों. इसके लिए, clang पर भरोसा न करें.
- कुछ लूप हटाएं. रेंज लूप का इस्तेमाल करें, ताकि Clang बेहतर तरीके से ऑप्टिमाइज़ कर सके. ऐसा इसलिए, क्योंकि लूप के खराब असर की वजह से, कंटेनर के इंटरनल पॉइंटर को फिर से लोड करने की ज़रूरत नहीं पड़ती. हर इनपुट के लिए, इनलाइन किए गए `InputAt(.)` के ज़रिए लूप में वर्चुअल फ़ंक्शन `HInstruction::GetInputRecords()` को कॉल करने से बचें.
- विज़िटर पैटर्न के लिए Accept() फ़ंक्शन का इस्तेमाल करने से बचें. इसके लिए, कंपाइलर ऑप्टिमाइज़ेशन का इस्तेमाल करें.
नतीजा
एआरटी के कंपाइल के समय को कम करने के लिए, हमने लगातार काम किया है. इससे हमें काफ़ी अच्छे नतीजे मिले हैं. अब Android डिवाइसों पर ऐप्लिकेशन ज़्यादा तेज़ी से और बेहतर तरीके से काम करते हैं. साथ ही, बैटरी लाइफ़ और डिवाइस के तापमान को मैनेज करने में भी मदद मिलती है. ऑप्टिमाइज़ेशन की पहचान करके और उन्हें लागू करके, हमने यह दिखाया है कि मेमोरी के इस्तेमाल या कोड की क्वालिटी से समझौता किए बिना, कंपाइल के समय में काफ़ी सुधार किया जा सकता है.
इस सफ़र में, हमने pprof जैसे टूल की मदद से प्रोफ़ाइलिंग की. साथ ही, हमने बार-बार कोशिश करने और कभी-कभी कम फ़ायदेमंद तरीकों को छोड़ने की इच्छा दिखाई. एआरटी टीम की कोशिशों से, कंपाइल करने में लगने वाले समय में काफ़ी कमी आई है. साथ ही, आने वाले समय में होने वाले सुधारों के लिए भी एक मज़बूत बुनियाद तैयार हुई है.
ये सभी सुधार, 2025 के आखिर में उपलब्ध कराए जाने वाले Android अपडेट में शामिल हैं. साथ ही, ये Android 12 और इसके बाद के वर्शन के लिए, मेनलाइन अपडेट के ज़रिए उपलब्ध हैं. हमें उम्मीद है कि ऑप्टिमाइज़ेशन की प्रोसेस के बारे में दी गई इस ज़्यादा जानकारी से, आपको कंपाइलर इंजीनियरिंग की जटिलताओं और फ़ायदों के बारे में अहम जानकारी मिली होगी!
-
प्रॉडक्ट से जुड़ी खबरेंआज Android डेवलपर समिट में, हम कई टूल और एपीआई की उपलब्धता का एलान कर रहे हैं. इन्हें रोज़मर्रा की समस्याओं को दूर करने और आपको बेहतरीन Android ऐप्लिकेशन बनाने के लिए ज़्यादा सुविधाएं देने के लिए डिज़ाइन किया गया है.
Matthew McCullough • पढ़ने में लगने वाला समय: 4 मिनट -
प्रॉडक्ट से जुड़ी खबरेंAndroid Studio Rabbit 2 अब स्टेबल है और प्रोडक्शन में इस्तेमाल के लिए तैयार है. इस रिलीज़ में, ऐप्लिकेशन डेवलपमेंट को बेहतर बनाने के लिए मुख्य सुविधाएं शामिल हैं. जैसे, Bring Your Own Agent, XR एमुलेटर की नई सुविधाएं, LeakCanary का अपडेट, एजेंट स्किल मैनेजर का नया टूल, एआई की मदद से कोड की समीक्षा करने की सुविधा वगैरह.
Matthew Warner • तीन मिनट में पढ़ा जा सकता है -
प्रॉडक्ट से जुड़ी खबरेंAndroid के आने वाले वर्शन को टारगेट करने वाले ऐप्लिकेशन, WRITE_CONTACTS अनुमति का इस्तेमाल करके संपर्कों में सीधे तौर पर बदलाव नहीं कर पाएंगे.
Roxanna Aliabadi Walker • इसे पढ़ने में छह मिनट लगेंगे
Android डेवलपमेंट से जुड़ी ताज़ा अहम जानकारी, हर हफ़्ते अपने इनबॉक्स में पाएं.