SharedPreferences.onSharedPreferenceChangeListener ไม่ได้ถูกเรียกอย่างสม่ำเสมอ


267

ฉันลงทะเบียนผู้ฟังการเปลี่ยนแปลงค่ากำหนดเช่นนี้ (ในonCreate()กิจกรรมหลักของฉัน):

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

prefs.registerOnSharedPreferenceChangeListener(
   new SharedPreferences.OnSharedPreferenceChangeListener() {
       public void onSharedPreferenceChanged(
         SharedPreferences prefs, String key) {

         System.out.println(key);
       }
});

ปัญหาคือผู้ฟังไม่ได้เรียกเสมอ ใช้งานได้สองสามครั้งแรกที่มีการเปลี่ยนแปลงการกำหนดค่าตามความชอบจากนั้นจะไม่มีการเรียกใช้อีกต่อไปจนกว่าฉันจะถอนการติดตั้งและติดตั้งแอปอีกครั้ง ดูเหมือนว่าจะไม่มีการรีสตาร์ทแอปพลิเคชัน

ฉันพบเธรดรายชื่อผู้รับจดหมายที่รายงานปัญหาเดียวกัน แต่ไม่มีใครตอบเขาจริงๆ ผมทำอะไรผิดหรือเปล่า?

คำตอบ:


612

นี่เป็นเรื่องลับๆล่อๆ SharedPreferences ทำให้ผู้ฟังอยู่ใน WeakHashMap ซึ่งหมายความว่าคุณไม่สามารถใช้คลาสภายในที่ไม่ระบุชื่อเป็นผู้ฟังได้เนื่องจากมันจะกลายเป็นเป้าหมายของการรวบรวมขยะทันทีที่คุณออกจากขอบเขตปัจจุบัน มันจะทำงานในตอนแรก แต่ในที่สุดจะได้รับการรวบรวมขยะลบออกจาก WeakHashMap และหยุดทำงาน

เก็บการอ้างอิงถึงผู้ฟังในฟิลด์ของคลาสของคุณและคุณจะโอเคถ้าอินสแตนซ์ของคลาสของคุณไม่ถูกทำลาย

เช่นแทน:

prefs.registerOnSharedPreferenceChangeListener(
  new SharedPreferences.OnSharedPreferenceChangeListener() {
  public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    // Implementation
  }
});

ทำเช่นนี้:

// Use instance field for listener
// It will not be gc'd as long as this instance is kept referenced
listener = new SharedPreferences.OnSharedPreferenceChangeListener() {
  public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    // Implementation
  }
};

prefs.registerOnSharedPreferenceChangeListener(listener);

สาเหตุที่การลงทะเบียนในเมธอด onDestroy ช่วยแก้ไขปัญหาได้เนื่องจากคุณต้องบันทึกผู้ฟังในฟิลด์ดังนั้นการป้องกันปัญหา เป็นการบันทึกผู้ฟังในฟิลด์ที่แก้ไขปัญหาไม่ใช่การลงทะเบียนใน onDestroy

อัปเดต : เอกสาร Android ได้รับการอัปเดตพร้อมคำเตือนเกี่ยวกับพฤติกรรมนี้ ดังนั้นพฤติกรรมคี่บอลยังคงอยู่ แต่ตอนนี้มันเป็นเอกสาร


20
นี่คือการฆ่าฉันฉันคิดว่าฉันสูญเสียความคิดของฉัน ขอบคุณสำหรับการโพสต์โซลูชั่นนี้!
แบรดไฮน์

10
โพสต์นี้มีขนาดใหญ่มากขอบคุณมากซึ่งอาจมีค่าใช้จ่ายฉันชั่วโมงของการแก้จุดบกพร่องเป็นไปไม่ได้!
Kevin Gaudin

เยี่ยมมากนี่คือสิ่งที่ฉันต้องการ คำอธิบายที่ดีเช่นกัน!
stealthcopter

นี่ทำให้ฉันแล้วและฉันก็ไม่รู้ว่าอะไรเกิดขึ้นจนกระทั่งอ่านบทความนี้ ขอบคุณ! บู, Android!
Nakedible

5
คำตอบที่ดีขอบคุณ ควรกล่าวถึงอย่างแน่นอนในเอกสาร code.google.com/p/android/issues/detail?id=48589
Andrey Chernih

16

คำตอบที่ยอมรับนี้ก็โอเคสำหรับฉันมันคือการสร้างอินสแตนซ์ใหม่ทุกครั้งที่กิจกรรมดำเนินการต่อ

ดังนั้นวิธีการเกี่ยวกับการเก็บการอ้างอิงถึงผู้ฟังภายในกิจกรรม

OnSharedPreferenceChangeListener myPrefListner = new OnSharedPreferenceChangeListener(){
      public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
         // your stuff
      }
};

และใน onResume และ onPause ของคุณ

@Override     
protected void onResume() {
    super.onResume();          
    getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(myPrefListner);     
}



@Override     
protected void onPause() {         
    super.onPause();          
    getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(myPrefListner);

}

สิ่งนี้จะคล้ายกับสิ่งที่คุณกำลังทำอยู่ยกเว้นว่าเราจะทำการอ้างอิงที่หนักหน่วง


ทำไมคุณใช้super.onResume()ก่อนgetPreferenceScreen()...?
Yousha Aleayoub

@YoushaAleayoub อ่านเกี่ยวกับandroid.app.supernotcalledexceptionซึ่งจำเป็นสำหรับการใช้งาน android
ซามูเอล

คุณหมายถึงอะไร ใช้super.onResume()ไม่จำเป็นต้องมีหรือใช้มันก่อนที่getPreferenceScreen()จะถูกต้อง? เพราะฉันกำลังพูดถึงสถานที่ที่เหมาะสม cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html
Yousha Aleayoub

ฉันจำได้ว่าอ่านมันได้ที่นี่developer.android.com/training/basics/activity-lifecycle/ ......ดูความคิดเห็นในรหัส แต่การวางไว้หางเป็นสิ่งที่สมเหตุสมผล แต่จนถึงตอนนี้ฉันไม่ได้ประสบปัญหาใด ๆ ในเรื่องนี้
ซามูเอล

ขอบคุณมากทุกที่ที่ฉันพบ onResume () และ onPause () วิธีการที่พวกเขาลงทะเบียนthisและไม่listenerมันทำให้เกิดข้อผิดพลาดและฉันสามารถแก้ปัญหาของฉัน Btw ทั้งสองวิธีมีประชาชนตอนนี้ไม่ได้รับการคุ้มครอง
นิโคลัส

16

เนื่องจากนี่เป็นหน้าที่มีรายละเอียดมากที่สุดสำหรับหัวข้อที่ฉันต้องการเพิ่ม 50ct ของฉัน

ฉันมีปัญหาที่ไม่ได้เรียก OnSharedPreferenceChangeListener SharedPreferences ของฉันถูกเรียกคืนเมื่อเริ่มต้นกิจกรรมหลักโดย:

prefs = PreferenceManager.getDefaultSharedPreferences(this);

รหัส PreferenceActivity ของฉันสั้นและไม่ทำอะไรเลยนอกจากแสดงการตั้งค่า:

public class Preferences extends PreferenceActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // load the XML preferences file
        addPreferencesFromResource(R.xml.preferences);
    }
}

ทุกครั้งที่กดปุ่มเมนูฉันสร้าง PreferenceActivity จากกิจกรรมหลัก:

@Override
public boolean onPrepareOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    //start Preference activity to show preferences on screen
    startActivity(new Intent(this, Preferences.class));
    //hook into sharedPreferences. THIS NEEDS TO BE DONE AFTER CREATING THE ACTIVITY!!!
    prefs.registerOnSharedPreferenceChangeListener(this);
    return false;
}

โปรดทราบว่าการลงทะเบียน OnSharedPreferenceChangeListener จะต้องทำหลังจากการสร้าง PreferenceActivity ในกรณีนี้มิฉะนั้นตัวจัดการในกิจกรรมหลักจะไม่ถูกเรียก !!! ฉันใช้เวลาพอสมควรที่จะรู้ว่า ...


9

คำตอบที่ยอมรับจะสร้างSharedPreferenceChangeListenerทุกครั้งที่onResumeถูกเรียก @Samuelแก้ปัญหาด้วยการทำให้SharedPreferenceListenerสมาชิกของคลาสกิจกรรม แต่มีวิธีแก้ปัญหาที่สามและตรงไปตรงมามากกว่าที่Googleใช้ในcodelabนี้ ทำให้คลาสกิจกรรมของคุณใช้OnSharedPreferenceChangeListenerอินเทอร์เฟซและแทนที่onSharedPreferenceChangedในกิจกรรมทำให้กิจกรรมนั้นเป็นSharedPreferenceListenerไปอย่างมีประสิทธิภาพ

public class MainActivity extends Activity implements SharedPreferences.OnSharedPreferenceChangeListener {

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {

    }

    @Override
    protected void onStart() {
        super.onStart();
        PreferenceManager.getDefaultSharedPreferences(this)
                .registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        PreferenceManager.getDefaultSharedPreferences(this)
                .unregisterOnSharedPreferenceChangeListener(this);
    }
}

1
ตรงนี้ควรจะเป็น ใช้อินเตอร์เฟสลงทะเบียนใน onStart และถอนการลงทะเบียนใน onStop
Junaed

2

รหัส Kotlin สำหรับการลงทะเบียน SharedPreferenceChange ฟังมันตรวจพบเมื่อมีการเปลี่ยนแปลงเกิดขึ้นในคีย์ที่บันทึกไว้:

  PreferenceManager.getDefaultSharedPreferences(this)
        .registerOnSharedPreferenceChangeListener { sharedPreferences, key ->
            if(key=="language") {
                //Do Something 
            }
        }

คุณสามารถใส่รหัสนี้ใน onStart () หรือที่อื่น .. * พิจารณาว่าคุณต้องใช้

 if(key=="YourKey")

หรือรหัสของคุณในบล็อค "// Do Something" จะทำงานผิดพลาดสำหรับทุกการเปลี่ยนแปลงที่จะเกิดขึ้นในคีย์อื่น ๆ ใน sharedPreferences


1

ดังนั้นฉันไม่รู้ว่าสิ่งนี้จะช่วยใครได้จริงหรือเปล่ามันช่วยแก้ปัญหาของฉันได้ไหม แม้ว่าฉันจะได้ดำเนินการOnSharedPreferenceChangeListenerตามที่ระบุไว้โดยคำตอบที่ได้รับการยอมรับ ยังฉันมีความไม่สอดคล้องกับผู้ฟังที่ถูกเรียก

ฉันมาที่นี่เพื่อทำความเข้าใจว่า Android เพิ่งส่งไปเก็บขยะหลังจากเวลาผ่านไป ดังนั้นฉันมองไปที่รหัสของฉัน เพื่อความอัปยศของฉันฉันไม่ได้ประกาศผู้ฟังทั่วโลกonCreateViewแต่ภายใน และนั่นเป็นเพราะฉันฟัง Android Studio บอกให้ฉันแปลงผู้ฟังเป็นตัวแปรในตัวเครื่อง


0

มันสมเหตุสมผลแล้วที่ผู้ฟังจะถูกเก็บไว้ใน WeakHashMap เพราะส่วนใหญ่นักพัฒนามักจะชอบเขียนโค้ดแบบนี้

PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).registerOnSharedPreferenceChangeListener(
    new OnSharedPreferenceChangeListener() {
    @Override
    public void onSharedPreferenceChanged(
        SharedPreferences sharedPreferences, String key) {
        Log.i(LOGTAG, "testOnSharedPreferenceChangedWrong key =" + key);
    }
});

นี่อาจดูไม่เลว แต่ถ้าคอนเทนเนอร์ของ OnSharedPreferenceChangeListeners ไม่ใช่ WeakHashMap มันจะแย่มากถ้าโค้ดข้างต้นถูกเขียนในกิจกรรม เนื่องจากคุณใช้คลาสภายในแบบไม่คงที่ (ไม่ระบุชื่อ) ซึ่งจะเก็บการอ้างอิงของอินสแตนซ์ที่ปิดล้อมโดยปริยาย นี่จะทำให้หน่วยความจำรั่ว

ยิ่งไปกว่านั้นถ้าคุณให้ผู้ฟังเป็นฟิลด์คุณสามารถใช้registerOnSharedPreferenceChangeListenerในตอนเริ่มต้นและโทรunregisterOnSharedPreferenceChangeListenerในท้ายที่สุด แต่คุณไม่สามารถเข้าถึงตัวแปรโลคัลในเมธอดนอกขอบเขต ดังนั้นคุณมีโอกาสที่จะลงทะเบียน แต่ไม่มีโอกาสที่จะถอนการลงทะเบียนผู้ฟัง ดังนั้นการใช้ WeakHashMap จะแก้ไขปัญหาได้ นี่คือวิธีที่ฉันแนะนำ

หากคุณสร้างอินสแตนซ์ผู้ฟังเป็นฟิลด์คงที่จะหลีกเลี่ยงการรั่วไหลของหน่วยความจำที่เกิดจากคลาสภายในที่ไม่คงที่ แต่เนื่องจากผู้ฟังอาจมีหลาย ๆ รายการจึงควรเกี่ยวข้องกับอินสแตนซ์ สิ่งนี้จะลดค่าใช้จ่ายในการจัดการการโทรกลับonSharedPreferenceChanged


-3

ในขณะที่อ่านข้อมูลที่อ่านได้ของ Word ที่แบ่งปันโดยแอปแรกเราควร

แทนที่

getSharedPreferences("PREF_NAME", Context.MODE_PRIVATE);

กับ

getSharedPreferences("PREF_NAME", Context.MODE_MULTI_PROCESS);

ในแอพที่สองเพื่อรับค่าที่อัปเดตในแอปที่สอง

แต่ก็ยังไม่ทำงาน ...


Android ไม่รองรับการเข้าถึง SharedPreferences จากหลายกระบวนการ การทำเช่นนั้นจะทำให้เกิดปัญหาการทำงานพร้อมกันซึ่งอาจทำให้การตั้งค่าทั้งหมดหายไป นอกจากนี้ MODE_MULTI_PROCESS ไม่ได้รับการสนับสนุนอีกต่อไป
แซม

@Sam คำตอบนี้มีอายุ 3 ปีโปรดอย่าลงคะแนนถ้ามันไม่ทำงานสำหรับคุณใน Android รุ่นล่าสุด เวลาเขียนคำตอบมันเป็นวิธีที่ดีที่สุดที่จะทำ
shridutt kothari

1
ไม่ได้วิธีการนั้นไม่ปลอดภัยหลายขั้นตอนแม้เมื่อคุณเขียนคำตอบนี้
Sam

เนื่องจากสถานะ @Sam ถูกต้องการตั้งค่าที่ใช้ร่วมกันจึงไม่เคยปลอดภัย นอกจากนี้สำหรับ shridutt kothari - หากคุณไม่ชอบ downvotes ให้ลบคำตอบที่ไม่ถูกต้องออกไป (ไม่ตอบคำถามของ OP) อย่างไรก็ตามหากคุณยังต้องการใช้การตั้งค่าแบบแชร์ในกระบวนการที่ปลอดภัยคุณจะต้องสร้างกระบวนการที่เป็นนามธรรมอย่างปลอดภัยข้างต้นนั่นคือ ContentProvider ซึ่งเป็นกระบวนการที่ปลอดภัยและยังช่วยให้คุณใช้การตั้งค่าแบบแบ่งใช้เป็นกลไกการจัดเก็บข้อมูล ฉันได้ทำสิ่งนี้มาก่อนและสำหรับชุดข้อมูลขนาดเล็ก / การตั้งค่าออกทำการ sqlite ด้วยอัตรากำไรขั้นต้น
Mark Keen

1
@ MarkKeen อย่างไรก็ตามฉันลองใช้ผู้ให้บริการเนื้อหานี้และผู้ให้บริการเนื้อหามักจะล้มเหลวในการผลิตแบบสุ่มเนื่องจากข้อบกพร่องของ Android ดังนั้นวันนี้ฉันเพิ่งใช้ Broadcasts เพื่อซิงค์การกำหนดค่ากับกระบวนการรองตามที่ต้องการ
Sam
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.