18% तेज़ी से कंपाइल करने की सुविधा, क्वालिटी से कोई समझौता नहीं
पढ़ने में 8 मिनट लगेंगे
Android Runtime (ART) की टीम ने कंपाइल करने में लगने वाले समय को 18% तक कम कर दिया है. हालांकि, कंपाइल किए गए कोड या मेमोरी के इस्तेमाल में कोई कमी नहीं आई है. यह सुधार, कंपाइल करने में लगने वाले समय को कम करने की हमारी 2025 की पहल का हिस्सा था. इसके लिए, मेमोरी के इस्तेमाल या कंपाइल किए गए कोड की क्वालिटी से समझौता नहीं किया गया.
ART के लिए, कंपाइल करने में लगने वाले समय की स्पीड को ऑप्टिमाइज़ करना ज़रूरी है. उदाहरण के लिए, जस्ट-इन-टाइम (JIT) कंपाइल करने पर, यह सीधे तौर पर ऐप्लिकेशन की परफ़ॉर्मेंस और डिवाइस की कुल परफ़ॉर्मेंस पर असर डालता है. तेज़ी से कंपाइल करने पर, ऑप्टिमाइज़ेशन शुरू होने में लगने वाला समय कम हो जाता है. इससे, लोगों को बेहतर और ज़्यादा रिस्पॉन्सिव अनुभव मिलता है. इसके अलावा, JIT और अहेड-ऑफ़-टाइम (AOT) दोनों के लिए, कंपाइल करने में लगने वाले समय की स्पीड में सुधार करने से, कंपाइलेशन की प्रोसेस के दौरान संसाधनों का इस्तेमाल कम होता है. इससे बैटरी लाइफ़ और डिवाइस के तापमान को बेहतर बनाने में मदद मिलती है. खास तौर पर, कम सुविधाओं वाले डिवाइसों पर.
कंपाइल करने में लगने वाले समय की स्पीड में किए गए कुछ सुधार, Android की जून 2025 की रिलीज़ में लॉन्च किए गए हैं. बाकी सुधार, Android की साल के आखिर में होने वाली रिलीज़ में उपलब्ध होंगे. इसके अलावा, Android के वर्शन 12 और उसके बाद के वर्शन इस्तेमाल करने वाले सभी लोगों को, मुख्य अपडेट के ज़रिए ये सुधार मिल सकते हैं.
ऑप्टिमाइज़िंग कंपाइलर को ऑप्टिमाइज़ करना
कंपाइलर को ऑप्टिमाइज़ करना हमेशा एक मुश्किल काम होता है. आपको स्पीड मुफ़्त में नहीं मिल सकती. इसके लिए, आपको कुछ छोड़ना होगा. हमने अपने लिए एक बहुत साफ़ और मुश्किल लक्ष्य तय किया है: कंपाइलर को तेज़ बनाना, लेकिन ऐसा मेमोरी के इस्तेमाल में कमी किए बिना और सबसे ज़रूरी बात यह है कि कंपाइलर से जनरेट होने वाले कोड की क्वालिटी को कम किए बिना करना. अगर कंपाइलर तेज़ है, लेकिन ऐप्लिकेशन धीरे चलते हैं, तो हम अपने लक्ष्य को हासिल नहीं कर पाए हैं.
हमने इन ज़रूरी शर्तों को पूरा करने के लिए, अपने डेवलपमेंट के समय को खर्च करने का फ़ैसला किया. इससे हमें गहराई से जांच करने और ऐसे बेहतर समाधान ढूंढने में मदद मिली जो इन ज़रूरी शर्तों को पूरा करते हैं. आइए, इस बारे में ज़्यादा जानें कि हम सुधार के लिए जगहें ढूंढने और अलग-अलग समस्याओं के सही समाधान ढूंढने के लिए कैसे काम करते हैं.
ऑप्टिमाइज़ेशन के लिए काम के संभावित तरीके ढूंढना
किसी मेट्रिक को ऑप्टिमाइज़ करने से पहले, आपको उसे मेज़र करना होगा. ऐसा न करने पर, आपको कभी भी यह पक्का नहीं हो पाएगा कि आपने उसे बेहतर बनाया है या नहीं. हमारे लिए अच्छी बात यह है कि कंपाइल करने में लगने वाले समय की स्पीड में काफ़ी स्थिरता होती है. हालांकि, इसके लिए आपको कुछ सावधानियां बरतनी होंगी. जैसे, बदलाव से पहले और बाद में मेज़रमेंट के लिए एक ही डिवाइस का इस्तेमाल करना और यह पक्का करना कि आपका डिवाइस ज़्यादा गर्म न हो. इसके अलावा, हमारे पास कंपाइलर के आंकड़ों जैसे तय मेज़रमेंट भी होते हैं. इनसे हमें यह समझने में मदद मिलती है कि बैकग्राउंड में क्या हो रहा है.
इन सुधारों के लिए, हम अपने डेवलपमेंट के समय को खर्च कर रहे थे. इसलिए, हम चाहते थे कि हम जितनी तेज़ी से हो सके, उतनी तेज़ी से काम करें. इसका मतलब है कि हमने प्रोटोटाइप समाधानों के लिए, कुछ प्रतिनिधि ऐप्लिकेशन (पहले पक्ष के ऐप्लिकेशन, तीसरे पक्ष के ऐप्लिकेशन, और Android ऑपरेटिंग सिस्टम का मिक्स) लिए. इसके बाद, हमने मैन्युअल और ऑटोमेटेड, दोनों तरह से बड़े पैमाने पर टेस्टिंग करके यह पुष्टि की कि फ़ाइनल लागू करने की प्रोसेस काम की है.
हमने हाथ से चुने गए एपीके के सेट के साथ, स्थानीय तौर पर मैन्युअल कंपाइल को ट्रिगर किया. इसके बाद, कंपाइलेशन की प्रोफ़ाइल हासिल की और 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 का एक कस्टम लागू करने का तरीका है, जिसका इस्तेमाल हम कई जगहों पर करते हैं. इस डेटा स्ट्रक्चर को बनाने में काफ़ी समय लग रहा था. हमें इसकी वजह पता चली. कई साल पहले, इस डेटा स्ट्रक्चर का इस्तेमाल सिर्फ़ कुछ जगहों पर किया जाता था. इन जगहों पर बहुत बड़े HashSets का इस्तेमाल किया जाता था. इसे ऑप्टिमाइज़ करने के लिए, इसमें बदलाव किया गया था. हालांकि, आजकल इसका इस्तेमाल सिर्फ़ कुछ एंट्री और कम समय के लिए किया जाता था. इसका मतलब है कि हम इस बड़े HashSet को बनाकर साइकल बर्बाद कर रहे थे. हालांकि, हमने इसे कुछ एंट्री के लिए ही इस्तेमाल किया और फिर छोड़ दिया. इस बदलाव से, हमने कंपाइल करने में लगने वाले समय में ~1.3-2% का सुधार किया. इसके अलावा, मेमोरी के इस्तेमाल में ~0.5-1% की कमी आई, क्योंकि हम पहले की तरह बड़े डेटा स्ट्रक्चर का इस्तेमाल नहीं कर रहे थे.
हमने डेटा स्ट्रक्चर को कॉपी करने से बचने के लिए, उन्हें रेफ़रंस के तौर पर लैम्डा को पास करके, कंपाइल करने में लगने वाले समय में ~0.5-1% का सुधार किया. यह एक ऐसी चीज़ थी जो ओरिजनल समीक्षा में नज़रअंदाज़ कर दी गई थी और यह हमारे कोडबेस में कई सालों तक मौजूद रही. pprof में प्रोफ़ाइल देखने के बाद, हमें पता चला कि ये तरीके बहुत सारे डेटा स्ट्रक्चर बना और मिटा रहे थे. इससे हमें इनकी जांच करने और इन्हें ऑप्टिमाइज़ करने में मदद मिली.
हमने कंपाइल किए गए आउटपुट को लिखने वाले फ़ेज़ की स्पीड को, कंपाइल की गई वैल्यू को कैश करके बढ़ाया. इससे कंपाइल करने में लगने वाले कुल समय में ~1.3-2.8% का सुधार हुआ. हालांकि, अतिरिक्त बुककीपिंग बहुत ज़्यादा थी और हमारे ऑटोमेटेड टेस्टिंग ने हमें मेमोरी के इस्तेमाल में कमी के बारे में अलर्ट किया. इसके बाद, हमने उसी कोड को दोबारा देखा और एक नया वर्शन लागू किया. इससे न सिर्फ़ मेमोरी के इस्तेमाल में कमी को ठीक किया गया, बल्कि कंपाइल करने में लगने वाले समय में ~0.5-1.8% का और सुधार हुआ! इस दूसरे बदलाव में, हमें दो डेटा स्ट्रक्चर में से एक को हटाने के लिए, इस फ़ेज़ को रीफ़ैक्टर करना पड़ा और यह सोचना पड़ा कि यह फ़ेज़ कैसे काम करना चाहिए.
हमारे ऑप्टिमाइज़िंग कंपाइलर में एक फ़ेज़ होता है. यह बेहतर परफ़ॉर्मेंस पाने के लिए, फ़ंक्शन कॉल को इनलाइन करता है. इनलाइन करने के लिए, हम किसी भी कंपाइलेशन से पहले ह्यूरिस्टिक्स और काम करने के बाद, लेकिन इनलाइनिंग को फ़ाइनल करने से ठीक पहले, फ़ाइनल जांच, दोनों का इस्तेमाल करते हैं. अगर इनमें से किसी में भी यह पता चलता है कि इनलाइनिंग काम की नहीं है (उदाहरण के लिए, बहुत सारे नए निर्देश जोड़े जाएंगे), तो हम मेथड कॉल को इनलाइन नहीं करते.
हमने दो जांचों को “फ़ाइनल जांच” कैटगरी से “ह्यूरिस्टिक” कैटगरी में ले जाया, ताकि यह अनुमान लगाया जा सके कि कोई इनलाइनिंग सफल होगी या नहीं. ऐसा हमने समय लेने वाले किसी भी कंपाइलेशन से पहले किया. यह अनुमान सटीक नहीं है, लेकिन हमने पुष्टि की कि हमारे नए ह्यूरिस्टिक्स, परफ़ॉर्मेंस पर असर डाले बिना, पहले इनलाइन किए गए 99.9% हिस्से को कवर करते हैं. इन नए ह्यूरिस्टिक्स में से एक, ज़रूरी डीईएक्स रजिस्टर (~0.2-1.3% सुधार) के बारे में था. दूसरा, निर्देशों की संख्या (~2% सुधार) के बारे में था.
हमारे पास BitVector का एक कस्टम लागू करने का तरीका है, जिसका इस्तेमाल हम कई जगहों पर करते हैं. हमने कुछ फ़िक्स्ड साइज़ वाले बिट वेक्टर के लिए, साइज़ बदलने वाली BitVector क्लास को, आसान BitVectorView से बदल दिया. इससे कुछ इनडायरेक्शन और रन-टाइम रेंज की जांच खत्म हो जाती है. साथ ही, बिट वेक्टर ऑब्जेक्ट के कंस्ट्रक्शन की स्पीड बढ़ जाती है.
इसके अलावा, BitVectorView क्लास को, स्टोरेज के टाइप के हिसाब से टेंप्लेट किया गया था. ऐसा पुराने BitVector के तौर पर हमेशा uint32_t का इस्तेमाल करने के बजाय किया गया. इससे कुछ कार्रवाइयां, जैसे कि Union(), 64-बिट प्लैटफ़ॉर्म पर एक साथ दो गुना बिट प्रोसेस कर सकती हैं. Android ओएस को कंपाइल करते समय, असर डालने वाले फ़ंक्शन के सैंपल में कुल मिलाकर 1% से ज़्यादा की कमी आई. यह कई बदलावों [1, 2, 3, 4, 5, 6] के ज़रिए किया गया
अगर हम सभी ऑप्टिमाइज़ेशन के बारे में विस्तार से बात करेंगे, तो हमें पूरा दिन लग जाएगा! अगर आपको कुछ और ऑप्टिमाइज़ेशन के बारे में जानना है, तो हमारे लागू किए गए कुछ अन्य बदलाव देखें:
- कंपाइल करने में लगने वाले समय को ~0.6-1.6% तक बेहतर बनाने के लिए, बुककीपिंग जोड़ना.
- अगर मुमकिन हो, तो साइकल से बचने के लिए डेटा को धीरे-धीरे कंपाइल करना.
- जब प्रीकंपाइलिंग काम का न हो, तो उसे छोड़ने के लिए अपने कोड को रीफ़ैक्टर करना.
- जब एलोकेटर को अन्य जगहों से आसानी से हासिल किया जा सकता है, तब कुछ डिपेंडेंट लोड चेन से बचना.
- गैर-ज़रूरी काम से बचने के लिए, जांच जोड़ने का एक और मामला.
- रजिस्टर एलोकेटर में, रजिस्टर टाइप (कोर/एफ़पी) पर बार-बार ब्रांचिंग से बचना.
- पक्का करें कि कुछ ऐरे, कंपाइल करने के समय ही शुरू किए जाएं. इसके लिए, clang पर निर्भर न रहें.
- कुछ लूप को साफ़ करना. रेंज लूप का इस्तेमाल करना. इन्हें clang बेहतर तरीके से ऑप्टिमाइज़ कर सकता है, क्योंकि लूप के साइड इफ़ेक्ट की वजह से, इसे कंटेनर के इंटरनल पॉइंटर को फिर से लोड करने की ज़रूरत नहीं होती. हर इनपुट के लिए, इनलाइन किए गए `InputAt(.)` के ज़रिए लूप में, वर्चुअल फ़ंक्शन `HInstruction::GetInputRecords()` को कॉल करने से बचना.
- कंपाइलर ऑप्टिमाइज़ेशन का इस्तेमाल करके, विज़िटर पैटर्न के लिए Accept() फ़ंक्शन से बचना.
नतीजा
ART की कंपाइल करने में लगने वाले समय की स्पीड को बेहतर बनाने की हमारी कोशिशों से, काफ़ी सुधार हुए हैं. इससे Android ज़्यादा फ़्लूड और बेहतर हुआ है. साथ ही, बैटरी लाइफ़ और डिवाइस के तापमान को बेहतर बनाने में भी मदद मिली है. ऑप्टिमाइज़ेशन की पहचान करने और उन्हें लागू करने से, हमने यह दिखाया है कि मेमोरी के इस्तेमाल या कोड की क्वालिटी से समझौता किए बिना, कंपाइल करने में लगने वाले समय में काफ़ी बचत की जा सकती है.
हमारी इस कोशिश में, pprof जैसे टूल की मदद से प्रोफ़ाइलिंग करना, बार-बार कोशिश करना, और कभी-कभी कम फ़ायदे वाले तरीकों को छोड़ना शामिल था. ART की टीम की सामूहिक कोशिशों से, कंपाइल करने में लगने वाले समय में काफ़ी कमी आई है. साथ ही, आने वाले समय में होने वाले सुधारों के लिए भी रास्ता खुल गया है.
ये सभी सुधार, Android के साल के आखिर में होने वाले 2025 के अपडेट में उपलब्ध हैं. साथ ही, Android 12 और उसके बाद के वर्शन के लिए, मुख्य अपडेट के ज़रिए भी उपलब्ध हैं. हमें उम्मीद है कि ऑप्टिमाइज़ेशन की हमारी प्रोसेस के बारे में यह जानकारी, कंपाइलर इंजीनियरिंग की जटिलताओं और फ़ायदों के बारे में अहम जानकारी देगी!
-
प्रॉडक्ट से जुड़ी खबरेंआज हमने बताया कि हम “सिर्फ़ एक स्टोर से कहीं ज़्यादा” के अपने विज़न को अगले पड़ाव पर कैसे ले जा रहे हैं. Google Play, लोगों की ज़रूरतों को पूरा करने के लिए हर जगह मौजूद है. 'आप' टैब, अब हर महीने 25 करोड़ से ज़्यादा महीने के हिसाब से सक्रिय उपयोगकर्ता के लिए, उनकी निजी ज़रूरतों के हिसाब से होम बेस बन गया है. वहीं, हमारा यूनिफ़ाइड गेमिंग प्लैटफ़ॉर्म, 70 करोड़ से ज़्यादा गेमर प्रोफ़ाइल को कनेक्ट करता है.
Paul Feng • पढ़ने में 3 मिनट लगेंगे -
प्रॉडक्ट से जुड़ी खबरेंAndroid डेवलपर के तौर पर, आपके पास ऐप्लिकेशन डेवलपमेंट के लिए इस्तेमाल किए जाने वाले एजेंट, एलएलएम, टूल, और कमांड-लाइन इंटरफ़ेस (सीएलआई) के कई विकल्प होते हैं. हमारा लक्ष्य है कि हम आपको अच्छी क्वालिटी के बेहतरीन Android ऐप्लिकेशन बनाने में मदद करें. भले ही, आप उन्हें किसी भी तरीके से बनाएं.
Simona Milanovic • पढ़ने में 4 मिनट लगेंगे -
प्रॉडक्ट से जुड़ी खबरेंGoogle Play पर, हम लगातार अपने सदस्यता प्लैटफ़ॉर्म को बढ़ा रहे हैं, ताकि आपको ग्रोथ बढ़ाने, नए कारोबार के मॉडल के हिसाब से ढलने, और लोगों की ज़रूरतों को पूरा करने में मदद मिल सके.
Sheenam Mittal • पढ़ने में 4 मिनट लगेंगे
Android डेवलपमेंट से जुड़ी नई जानकारी, हर हफ़्ते अपने इनबॉक्स में पाएं.