WebView की मेमोरी को मैनेज करना और उसकी परफ़ॉर्मेंस का आकलन करना

WebView आपके Android ऐप्लिकेशन में वेब कॉन्टेंट रेंडर करने के लिए, कई प्रोसेस में नेटिव कोड चलाता है. WebView इंस्टेंस को मैनेज न करने पर, मेमोरी लीक हो सकती है. साथ ही, मेमोरी खत्म होने (ओओएम) की वजह से ऐप्लिकेशन क्रैश हो सकता है और उसकी परफ़ॉर्मेंस खराब हो सकती है.

इस दस्तावेज़ में, WebView के मल्टी-प्रोसेस मेमोरी मॉडल के बारे में बताया गया है. साथ ही, इसमें यह भी बताया गया है कि मेमोरी लीक से बचने के लिए, इसके लाइफ़साइकल को सही तरीके से कैसे मैनेज किया जाए. इसके अलावा, इसमें मेमोरी से जुड़ी समस्याओं की पहचान करने के लिए, व्यावहारिक वर्कफ़्लो भी दिए गए हैं.

WebView की मेमोरी के आर्किटेक्चर के बारे में जानकारी

WebView की मेमोरी को असरदार तरीके से मैनेज करने के लिए, यह समझें कि Android, वेब कॉन्टेंट के लिए संसाधन कैसे बांटता है:

  • मल्टी-प्रोसेस एक्ज़ीक्यूशन: Android 8.0 (एपीआई लेवल 26) और उसके बाद के वर्शन पर, WebView, वेब कॉन्टेंट को आपके ऐप्लिकेशन के मुख्य फ़ंक्शन से अलग करता है. यह काम, कई प्रोसेस में किया जाता है. हालांकि, कम रैम वाले डिवाइसों पर, यह काम एक ही प्रोसेस में किया जा सकता है:

    • होस्ट (ब्राउज़र) प्रोसेस: यह ऐप्लिकेशन की मुख्य प्रोसेस होती है, जिसमें आपकी Activity और Java या Kotlin कोड चलता है.
    • आइसोलेटेड रेंडरर प्रोसेस: यह सैंडबॉक्स वाली एक अलग प्रोसेस (SandboxedProcessService) होती है, जो एचटीएमएल और सीएसएस को पार्स करती है, JavaScript को एक्ज़ीक्यूट करती है, और वेब पेज रेंडर करती है.
  • नेटिव मेमोरी फ़ुटप्रिंट: रेंडर किए गए ग्राफ़िक्स, डीओएम ट्री, और JavaScript रनटाइम मेमोरी सहित, ज़्यादातर WebView मेमोरी, Java हीप पर नहीं, बल्कि नेटिव मेमोरी में ऐलोकेट की जाती है. Java हीप डंप (.hprof) में, सिर्फ़ एक हल्का-फुल्का Java रैपर ऑब्जेक्ट दिखता है. इसमें, वेब कॉन्टेंट के लिए इस्तेमाल की गई असली मेमोरी नहीं दिखती.

  • नेटिव मेमोरी का सिस्टम पर असर: Java हीप ऐलोकेशन के उलट, नेटिव मेमोरी को maxHeap की सीमा से नहीं बांधा जा सकता. साथ ही, OutOfMemoryError की वजह से, यह तुरंत क्रैश नहीं होती. नेटिव मेमोरी, चुपचाप कई जीबी तक बढ़ सकती है. जब रिलीज़ न की गई नेटिव मेमोरी, फ़िज़िकल रैम और स्वैप स्पेस (zRAM) को भर देती है, तो Android का लो मेमोरी किलर (एलएमके), मेमोरी वापस पाने के लिए बैकग्राउंड में चल रही प्रोसेस को बंद करना शुरू कर देता है. इससे, डिवाइस की मल्टीटास्किंग की परफ़ॉर्मेंस खराब हो जाती है. इसके बाद, फ़ोरग्राउंड में चल रहा ऐप्लिकेशन बंद हो जाता है.

WebView के लाइफ़साइकल को मैनेज करना

मेमोरी लीक से बचने के लिए, लाइफ़साइकल को सही तरीके से मैनेज करना ज़रूरी है. आम तौर पर, लोग यह मान लेते हैं कि लेआउट से WebView हटाने या Activity को अपने-आप खत्म होने देने से, उसकी मेमोरी खाली हो जाती है. हालांकि, ऐसा नहीं होता.

Java कॉन्टेक्स्ट रेफ़रंस और नेटिव रेंडर संसाधनों, दोनों को पूरी तरह से साफ़ करने के लिए, आपको अपने होस्ट कॉम्पोनेंट के लाइफ़साइकल (जैसे, onDestroy()) में, साफ़ करने की प्रोसेस को साफ़ तौर पर मैनेज करना होगा. इसके लिए, चालू पेज के एक्ज़ीक्यूशन को रोकें, व्यू को उसके कंटेनर से अलग करें, और नेटिव बाइंडिंग रिलीज़ करें.

WebView इंस्टेंस साफ़ करना

यह पक्का करने के लिए कि Activity या Fragment के बंद होने पर, संसाधन रिलीज़ हो जाएं और पूरी तरह से साफ़ हो जाएं, यह तरीका अपनाएं:

  1. WebView को उसके पैरंट कंटेनर (ViewGroup) से हटाएं.
  2. चालू लोडिंग रोकें और नेविगेशन इतिहास मिटाएं.
  3. destroy() को कॉल करें.
  4. null के रेफ़रंस को साफ़ करें.

यहां दिए गए उदाहरण में, WebView को सही तरीके से साफ़ करने का तरीका बताया गया है:

Kotlin

override fun onDestroy() {
    myWebView?.let {
        // Remove the WebView from its parent ViewGroup.
        (it.parent as? ViewGroup)?.removeView(it)
        // Stop active loading and clear history.
        it.stopLoading()
        it.clearHistory()
        // Destroy the instance.
        it.destroy()
    }
    myWebView = null
    super.onDestroy()
}

Java

@Override
protected void onDestroy() {
    if (myWebView != null) {
        // Remove the WebView from its parent ViewGroup.
        if (myWebView.getParent() instanceof ViewGroup) {
            ((ViewGroup) myWebView.getParent()).removeView(myWebView);
        }
        // Stop active loading and clear history.
        myWebView.stopLoading();
        myWebView.clearHistory();
        // Destroy the instance.
        myWebView.destroy();
    }
    myWebView = null;
    super.onDestroy();
}

बंद होने के बाद की मेमोरी के बारे में जानकारी

जब destroy() को कॉल किया जाता है, तो सिस्टम Activity कॉन्टेक्स्ट को रिलीज़ कर देता है, व्यू हैरारकी को साफ़ कर देता है, और वेब बैकग्राउंड में चल रहे काम को रोक देता है. हालांकि, आपको दिख सकता है कि प्रोसेस की फ़िज़िकल मेमोरी (रेज़िडेंट सेट साइज़), `WebView` के इस्तेमाल से पहले के बेसलाइन पर तुरंत नहीं आती.

यह सामान्य बात है. नेटिव रनटाइम कैश, शेयर की गई लाइब्रेरी, और ऐलोकेट किए गए मेमोरी पेज, प्रोसेस में तब तक मौजूद रहते हैं, जब तक ऑपरेटिंग सिस्टम उन्हें वापस नहीं ले लेता या प्रोसेस बंद नहीं हो जाती. destroy() का मुख्य मकसद, वेब-पावर्ड स्क्रीन पर उपयोगकर्ताओं के आने-जाने के दौरान, Activity मेमोरी लीक को रोकना है.

डीबग करने से जुड़ी अहम मेट्रिक

WebView की मेमोरी के इस्तेमाल का विश्लेषण करते समय, इन मेट्रिक पर फ़ोकस करें:

  • रेज़िडेंट सेट साइज़ (आरएसएस): प्रोसेस में मैप की गई कुल फ़िज़िकल रैम. इसमें शेयर किया गया कोड और लाइब्रेरी भी शामिल हैं. Android Studio Profiler में इसे कुल के तौर पर लेबल किया जाता है.

  • बिना सोर्स फ़ाइल वाली आरएसएस मेमोरी (आरएसएस एनॉन): प्रोसेस ने सीधे तौर पर कितनी मेमोरी इस्तेमाल की है. जैसे, नेटिव हीप और JavaScript रनटाइम ऐलोकेशन. इसे डिस्क पर मौजूद किसी फ़ाइल से बैक अप नहीं किया जाता. यह आपके वेब कॉन्टेंट की मुख्य मेमोरी लागत को दिखाता है. Android Studio Profiler में इसे ऐलोकेट की गई के तौर पर लेबल किया जाता है.

  • प्राइवेट मेमोरी फ़ुटप्रिंट (पीएमएफ़): यह बिना सोर्स फ़ाइल वाली आरएसएस मेमोरी और स्वैप (zRAM) का योग होता है. पीएमएफ़ से पता चलता है कि आपका ऐप्लिकेशन, सिस्टम पर कितनी मेमोरी का लोड डालता है.

  • ब्राउज़र पीएमएफ़ बनाम रेंडरर पीएमएफ़: आपके ऐप्लिकेशन की मुख्य प्रोसेस ने कितनी मेमोरी इस्तेमाल की है बनाम आइसोलेटेड रेंडरर प्रोसेस ने कितनी मेमोरी इस्तेमाल की है. ज़्यादा वेब कॉन्टेंट की वजह से, मुख्य तौर पर रेंडरर प्रोसेस में स्पाइक दिखते हैं.

  • लाइव ऑब्जेक्ट की संख्या (WebViews, Activities, Views): मेमोरी में सेव किए गए चालू यूज़र इंटरफ़ेस (यूआई), कॉन्टेक्स्ट, और WebView इंस्टेंस की संख्या. इनको ट्रैक करने से यह पता चलता है कि मेमोरी में बढ़ोतरी, सेव किए गए Java रेफ़रंस या सिर्फ़ नेटिव ऐलोकेशन की वजह से हुई है.

  • प्राइवेट अदर और नेटिव हीप: dumpsys meminfo में, नेटिव C/C++ ऐलोकेशन और कस्टम मेमोरी मैपिंग (जैसे, Chromium PartitionAlloc या एम्बेड किए गए JavaScript रनटाइम हीप), Java हीप के बजाय नेटिव हीप और प्राइवेट अदर में दिखते हैं.

प्रोसेस मेमोरी काउंटर और उनकी कैटगरी के बारे में ज़्यादा जानने के लिए, प्रोसेस मेमोरी की शब्दावली देखें.

पहचान करने के लिए व्यावहारिक वर्कफ़्लो

WebView कई प्रोसेस में काम करता है और नेटिव मेमोरी ऐलोकेट करता है. इसलिए, इसके फ़ुटप्रिंट की जांच करने के लिए, इन टूल और तकनीकों का इस्तेमाल करें:

प्रोफ़ाइलिंग और पहचान करने वाले टूल

मेमोरी ऐलोकेशन की जांच करने और लीक की पहचान करने के लिए, इन टूल का इस्तेमाल करें:

  • Android Studio Memory Profiler: नेटिव ऐलोकेशन को विज़ुअलाइज़ करने, समय के साथ मेमोरी की कैटगरी को ट्रैक करने, और स्क्रीन ट्रांज़िशन के दौरान Activity लीक का पता लगाने के लिए, Memory Profiler का इस्तेमाल करें.

  • Perfetto की मदद से मेमोरी ट्रैक करना: सिस्टम-लेवल के मेमोरी काउंटर (जैसे, आरएसएस और बिना सोर्स फ़ाइल वाली आरएसएस मेमोरी) रिकॉर्ड करने के लिए, Perfetto का इस्तेमाल करें. इससे, मेमोरी में होने वाली कुल बढ़ोतरी को देखा जा सकता है. ध्यान दें कि WebView नेटिव इंजन ऐलोकेशन के कॉलस्टैक Perfetto के हीप प्रोफ़ाइलिंग टूल में नहीं दिखते. वेब कॉन्टेंट में मौजूद JavaScript हीप स्नैपशॉट और डीओएम ऐलोकेशन की जांच करने के लिए, Chrome DevTools का इस्तेमाल करें.

लाइव ऑब्जेक्ट की संख्या की जांच करना

यह पता लगाने के लिए कि मेमोरी में बढ़ोतरी, सेव किए गए Java फ़्रेमवर्क ऑब्जेक्ट (जैसे, यूज़र इंटरफ़ेस (यूआई) कॉम्पोनेंट) या नेटिव ऐलोकेशन की वजह से हुई है, Objects सेक्शन की जांच करें: dumpsys meminfo

adb shell dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"

आउटपुट में, लाइव ऑब्जेक्ट की संख्या दिखती है:

 Objects
               Views:     142         ViewRootImpl:        1
         AppContexts:       3           Activities:        1
              Assets:      12        AssetManagers:        0
       Local Binders:      32        Proxy Binders:       45
       Parcel memory:      15         Parcel count:       30
    Death Recipients:       2             WebViews:        1

इस सेक्शन में, चालू फ़्रेमवर्क ऑब्जेक्ट, आईपीसी हैंडल, और पार्सल ऐलोकेशन की संख्या दिखती है. की पहचान करने के लिए, मुख्य तौर पर WebView और Activities पर फ़ोकस करें.WebViews

टारगेट उपयोगकर्ता इंटरैक्शन (जैसे, वेब स्क्रीन खोलना और बंद करना) को बार-बार करें और संख्या की तुलना करें:

  • इंस्टेंस लीक: अगर हर नेविगेशन पर WebViews या Activities की संख्या बढ़ती है और बेसलाइन पर वापस नहीं आती है, तो आपका ऐप्लिकेशन, Java WebView इंस्टेंस या होस्ट Activity लीक कर रहा है. उदाहरण के लिए, ऐसा ViewGroup.removeView() के न होने या सेव किए गए लिसनर रेफ़रंस की वजह से हो सकता है. लीक हुई Activity, अपने पूरे व्यू ट्री और डिकोड की गई इमेज संसाधनों को मेमोरी में सेव रखती है. इसलिए, बार-बार आने पर, Java हीप तेज़ी से खत्म हो जाएगा और OutOfMemoryError की वजह से ऐप्लिकेशन क्रैश हो जाएगा.

  • नेटिव या डीओएम लीक: अगर WebViews और Activities की संख्या स्थिर रहती है, जबकि प्रोसेस के कुल आरएसएस और प्राइवेट अदर में लगातार बढ़ोतरी होती है, तो लीक, रिलीज़ न किए गए नेटिव संसाधनों, डीओएम एलिमेंट या JavaScript इंजन बाइंडिंग में होता है. ये ऐलोकेशन, नेटिव मेमोरी में मौजूद होते हैं और एआरटी गार्बेज कलेक्टर को बायपास करते हैं. इसलिए, ये Java लीक का पता लगाने वाले स्टैंडर्ड टूल को नहीं दिखते और तब तक इकट्ठा होते रहते हैं, जब तक ऑपरेटिंग सिस्टम ऐप्लिकेशन को बंद नहीं कर देता.

सीएलआई का इस्तेमाल करके, आइसोलेटेड रेंडरर प्रोसेस की प्रोफ़ाइल बनाना

अपने ऐप्लिकेशन के पैकेज के नाम के साथ dumpsys meminfo चलाने पर, सिर्फ़ मुख्य होस्ट प्रोसेस की मेमोरी का आउटपुट मिलता है. आइसोलेटेड रेंडरर प्रोसेस की जांच करने के लिए, जिसमें वेब पेज रेंडर किए जाते हैं:

  1. आइसोलेटेड रेंडरर सेवा का प्रोसेस आईडी (पीआईडी) ढूंढें:

    adb shell dumpsys activity processes <var>PACKAGE_NAME</var> | grep "Isolated.*SandboxedProcessService"

    आउटपुट में, आइसोलेटेड प्रोसेस रिकॉर्ड और उसका पीआईडी RENDERER_PID दिखता है. उदाहरण के लिए, 22155:

    Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}
    
  2. रेंडरर प्रोसेस के पीआईडी का इस्तेमाल करके, उसकी मेमोरी ब्रेकडाउन की जांच करें:

    adb shell dumpsys meminfo <var>RENDERER_PID</var>
  3. ब्राउज़र-साइड फ़ुटप्रिंट का आकलन करने के लिए, होस्ट ऐप्लिकेशन प्रोसेस की जांच करें:

    adb shell dumpsys meminfo <var>PACKAGE_NAME</var>

मेमोरी मैप और ऐलोकेशन की जांच करना

यह देखने के लिए कि कौनसे नेटिव सबसिस्टम या ऐलोकेटर, बिना सोर्स फ़ाइल वाली मेमोरी का इस्तेमाल करते हैं, प्रोसेस मेमोरी मैप की जांच करें:

adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"

यहां दी गई टेबल में, बिना सोर्स फ़ाइल वाली मेमोरी के सामान्य टैग और मेमोरी में बढ़ोतरी से उनका संबंध बताया गया है:

मेमोरी टैग सबसिस्टम ऐप्लिकेशन और वेब कॉन्टेंट से संबंध क्या मेमोरी में बढ़ोतरी की यह आम वजह है?
[anon:partition_alloc] Chromium PartitionAlloc WebView में डीओएम ट्री, रेंडरिंग बफ़र, V8 JavaScript हीप, और WebAssembly एक्ज़ीक्यूशन के लिए ऐलोकेशन. हां (ज़्यादा): ज़्यादा वेब पेज, मीडिया से भरपूर डीओएम लोड करने या बंद किए गए WebView इंस्टेंस पर सीधे destroy() को कॉल न करने से, इस टैग की वैल्यू बढ़ जाती है.
[anon:scudo...] या [anon:libc_malloc] Android नेटिव हीप ऐलोकेटर (Scudo / jemalloc) एनडीके लाइब्रेरी, जेएनआई ब्रिज, और नेटिव ग्राफ़िक्स पाइपलाइन के ज़रिए इस्तेमाल किए जाने वाले सामान्य C/C++ नेटिव ऐलोकेशन. हां (मध्यम से ज़्यादा): जब नेटिव जेएनआई रैपर या तीसरे पक्ष की C++ डिपेंडेंसी, नेविगेशन के दौरान रिलीज़ न किए गए ऐलोकेशन को सेव रखती हैं, तब मेमोरी में बढ़ोतरी होती है.
[anon:...] (उदाहरण के लिए, [anon:quickjs_heap...]) कस्टम स्क्रिप्टिंग या नेटिव रनटाइम एम्बेड किए गए JavaScript इंजन, कस्टम WebAssembly रनटाइम या कस्टम नेटिव बफ़र पूल. हां (कॉन्टेक्स्ट के हिसाब से): हाइब्रिड ऐप्लिकेशन में आम तौर पर ऐसा होता है. इनमें, नेटिव व्यू के साथ स्क्रिप्टिंग इंजन एक्ज़ीक्यूट किए जाते हैं और रनटाइम बाइंडिंग को साफ़ नहीं किया जाता.

ऐप्लिकेशन में मौजूद मेमोरी एपीआई की सीमाएं

ऐप्लिकेशन में मौजूद मेमोरी एपीआई (जैसे, Debug.getMemoryInfo या ActivityManager.getProcessMemoryInfo), सिर्फ़ कॉल करने वाली प्रोसेस को मेज़र करते हैं. मल्टी-प्रोसेस मोड में, ये एपीआई, आइसोलेटेड रेंडरर प्रोसेस के ज़रिए इस्तेमाल की गई मेमोरी को कैप्चर नहीं कर सकते. मेमोरी के कुल इस्तेमाल का सटीक आकलन करने के लिए, dumpsys meminfo, Perfetto या Android Studio Profiler जैसे सिस्टम टूल का इस्तेमाल करें.

हाइब्रिड ऐप्लिकेशन में ज़्यादा मेमोरी इस्तेमाल होने की समस्या को हल करना

WebView के साथ बार-बार इंटरैक्शन (जैसे, वेब लिंक खोलना या वेब-पावर्ड फ़ीड पर नेविगेट करना) के दौरान, मेमोरी में होने वाली बढ़ोतरी की पहचान करने के लिए, इस वर्कफ़्लो का इस्तेमाल करें. इससे यह पता लगाया जा सकता है कि लीक, Java लेयर या नेटिव इंजन में हुआ है:

  1. लीक के टाइप की पहचान करना (Java बनाम नेटिव): उपयोगकर्ता के बार-बार ट्रांज़िशन (जैसे, वेब लेख खोलना और बंद करना या फ़ीड में स्वाइप करना) से पहले और बाद में, dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects" चलाएं.

    • ऑब्ज़र्वेशन: अगर Activities और WebViews की संख्या स्थिर रहती है (उदाहरण के लिए, 1–2 चालू इंस्टेंस), तो ऐप्लिकेशन, Activity कॉन्टेक्स्ट या Java WebView इंस्टेंस लीक नहीं कर रहा है.
  2. इंटरैक्शन के दौरान मेमोरी डेल्टा मेज़र करना (टाइम-सीरीज़ ट्रैकिंग): हर ट्रांज़िशन के लिए ऐलोकेशन रेट का हिसाब लगाने के लिए, उपयोगकर्ता के कई इंटरैक्शन के दौरान dumpsys meminfo स्नैपशॉट कैप्चर करें:

    • ऑब्ज़र्वेशन: Java हीप, सीमित और सही रहता है. इस्तेमाल के दौरान इसमें स्पाइक दिखता है और गार्बेज कलेक्शन के बाद यह कम हो जाता है. हालांकि, हर ट्रांज़िशन के दौरान प्राइवेट अदर और नेटिव हीप में कई मेगाबाइट की बढ़ोतरी होती है. इससे यह साबित होता है कि लीक, एआरटी रनटाइम के बाहर, पूरी तरह से नेटिव मेमोरी में हुआ है. Java हीप डंप (.hprof) में कोई समस्या नहीं दिखेगी.
  3. बिना सोर्स फ़ाइल वाली मेमोरी मैप की जांच करना: एडीबी का इस्तेमाल करके, प्रोसेस मेमोरी मैप की जांच करें (मेमोरी मैप और ऐलोकेशन की जांच करना देखें):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • ऑब्ज़र्वेशन: मेमोरी में बढ़ोतरी, [anon:partition_alloc] या एम्बेड किए गए स्क्रिप्टिंग इंजन हीप में होती है. इसके साथ ही, जेएनआई ग्लोबल रेफ़रंस में धीरे-धीरे बढ़ोतरी होती है. इससे पता चलता है कि Java व्यू को बदल दिया गया था, लेकिन उसके नीचे मौजूद नेटिव पेज ऑब्जेक्ट या JavaScript बाइंडिंग रिलीज़ नहीं किए गए थे.
  4. समस्या ठीक करना:

    • पक्का करें कि रीसाइकल या बंद किए गए हर WebView में, चालू स्क्रिप्ट (stopLoading()) को साफ़ तौर पर रोका गया हो, इतिहास मिटाया गया हो, और destroy() को कॉल किया गया हो.
    • बंद किए गए व्यू से जुड़े कस्टम JavaScript ब्रिज कॉलबैक या जेएनआई ग्लोबल रेफ़रंस को हटाएं.
    • पुष्टि करें कि Private Other और प्रोसेस आरएसएस नेविगेशन ट्रांज़िशन के बाद स्थिर हो जाते हैं.

अन्य संसाधन

मेमोरी और WebView की परफ़ॉर्मेंस को डीबग करने और उसकी प्रोफ़ाइल बनाने के बारे में ज़्यादा जानने के लिए, ये लेख पढ़ें और वीडियो देखें: