SharedPreferences.onSharedPreferenceChangeListener không được gọi một cách nhất quán


267

Tôi đang đăng ký một trình lắng nghe thay đổi tùy chọn như thế này (trong onCreate()hoạt động chính của tôi):

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

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

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

Vấn đề là, người nghe không phải lúc nào cũng được gọi. Nó hoạt động trong vài lần đầu tiên tùy chọn được thay đổi, và sau đó nó không còn được gọi cho đến khi tôi gỡ cài đặt và cài đặt lại ứng dụng. Không có số lượng khởi động lại ứng dụng dường như để sửa nó.

Tôi tìm thấy một chủ đề danh sách gửi thư báo cáo vấn đề tương tự, nhưng không ai thực sự trả lời anh ta. Tôi đang làm gì sai?

Câu trả lời:


612

Đây là một trong những lén lút. SharedPreferences giữ người nghe trong WeakHashMap. Điều này có nghĩa là bạn không thể sử dụng một lớp bên trong ẩn danh làm người nghe, vì nó sẽ trở thành mục tiêu của bộ sưu tập rác ngay khi bạn rời khỏi phạm vi hiện tại. Nó sẽ hoạt động lúc đầu, nhưng cuối cùng, sẽ thu gom rác, loại bỏ khỏi WeakHashMap và ngừng hoạt động.

Giữ một tham chiếu đến người nghe trong một lĩnh vực của lớp của bạn và bạn sẽ ổn, miễn là thể hiện của lớp của bạn không bị phá hủy.

tức là thay vì:

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

làm cái này:

// 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);

Lý do không đăng ký trong phương thức onDestroy khắc phục sự cố là vì để làm được điều đó, bạn phải lưu người nghe vào một trường, do đó ngăn chặn sự cố. Đó là tiết kiệm người nghe trong một lĩnh vực khắc phục vấn đề, không phải là không đăng ký trong onDestroy.

CẬP NHẬT : Các tài liệu Android đã được cập nhật với các cảnh báo về hành vi này. Vì vậy, hành vi kỳ quặc vẫn còn. Nhưng bây giờ nó đã được ghi nhận.


20
Điều này đã giết chết tôi, tôi nghĩ rằng tôi đang mất trí. Cảm ơn bạn đã đăng giải pháp này!
Brad Hein

10
Bài đăng này rất LỚN, cảm ơn rất nhiều, điều này có thể khiến tôi mất hàng giờ để gỡ lỗi!
Kevin Gaudin

Tuyệt vời, đây chỉ là những gì tôi cần. Giải thích tốt quá!
tàng hình

Điều này đã cắn tôi và tôi không biết chuyện gì đã xảy ra cho đến khi đọc bài viết này. Cảm ơn bạn! Boo, Android!
Khỏa thân

5
Câu trả lời tuyệt vời, cảm ơn bạn. Chắc chắn nên được đề cập trong các tài liệu. code.google.com/p/android/issues/detail?id=48589
Andrey Chernih

16

câu trả lời được chấp nhận này là ok, vì đối với tôi nó đang tạo ra trường hợp mới mỗi khi hoạt động trở lại

vậy làm thế nào về việc giữ tham chiếu đến người nghe trong hoạt động

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

và trong onResume và onPause của bạn

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



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

}

điều này sẽ rất giống với những gì bạn đang làm ngoại trừ chúng tôi đang duy trì một tài liệu tham khảo cứng.


Tại sao bạn sử dụng super.onResume()trước đây getPreferenceScreen()...?
Yousha Aleayoub

@YoushaAleayoub, hãy đọc về android.app.supernotcalledexception nó được yêu cầu khi thực hiện Android.
Samuel

Ý anh là gì? sử dụng super.onResume()là bắt buộc HOẶC sử dụng nó TRƯỚC getPreferenceScreen()là bắt buộc? bởi vì tôi đang nói về nơi ĐÚNG. cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html
Yousha Aleayoub

Tôi nhớ đã đọc nó ở đây developer.android.com/training/basics/activity-lifecycle/ , xem nhận xét trong mã. nhưng đặt nó ở đuôi là hợp lý. nhưng cho đến bây giờ tôi không gặp phải bất kỳ vấn đề nào trong việc này.
Samuel

Cảm ơn rất nhiều, ở mọi nơi khác mà tôi tìm thấy phương thức onResume () và onPause () mà họ đã đăng ký thisvà không listener, nó gây ra lỗi và tôi có thể giải quyết vấn đề của mình. Btw hai phương thức này hiện đang công khai, không được bảo vệ
Nicolas

16

Vì đây là trang chi tiết nhất cho chủ đề tôi muốn thêm 50ct của mình.

Tôi gặp vấn đề là OnSharedPreferenceChangeListener không được gọi. SharedPreferences của tôi được truy xuất khi bắt đầu Hoạt động chính bằng cách:

prefs = PreferenceManager.getDefaultSharedPreferences(this);

Mã PreferenceActivity của tôi ngắn và không làm gì ngoài việc hiển thị các tùy chọn:

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

Mỗi lần nhấn nút menu tôi sẽ tạo PreferenceActivity từ Hoạt động chính:

@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;
}

Lưu ý rằng việc đăng ký OnSharedPreferenceChangeListener cần được thực hiện SAU khi tạo PreferenceActivity trong trường hợp này, nếu không thì Handler trong Hoạt động chính sẽ không được gọi !!! Phải mất một thời gian ngọt ngào để nhận ra rằng ...


9

Câu trả lời được chấp nhận tạo ra SharedPreferenceChangeListenermỗi lần onResumeđược gọi. @Samuel giải quyết nó bằng cách làm SharedPreferenceListenerthành viên của lớp Activity. Nhưng có một giải pháp thứ ba và đơn giản hơn mà Google cũng sử dụng trong codelab này . Làm cho lớp hoạt động của bạn thực hiện OnSharedPreferenceChangeListenergiao diện và ghi đè onSharedPreferenceChangedtrong Hoạt động, thực hiện chính hoạt động đó a 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
chính xác, điều này nên là nó thực hiện giao diện, đăng ký trong onStart và unRegister trong onStop.
Junaed

2

Mã Kotlin để đăng ký SharedPreferenceChangeListener mà nó phát hiện khi thay đổi sẽ xảy ra trên khóa đã lưu:

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

bạn có thể đặt mã này vào onStart () hoặc ở nơi khác .. * Hãy xem xét rằng bạn phải sử dụng

 if(key=="YourKey")

hoặc mã của bạn trong khối "// Do Something" sẽ bị chạy sai cho mọi thay đổi sẽ xảy ra trong bất kỳ khóa nào khác trong sharedPreferences


1

Vì vậy, tôi không biết nếu điều này thực sự sẽ giúp bất cứ ai, nó đã giải quyết vấn đề của tôi. Mặc dù tôi đã thực hiện OnSharedPreferenceChangeListenernhư đã nêu trong câu trả lời được chấp nhận . Tuy nhiên, tôi đã có một sự không nhất quán với người nghe được gọi.

Tôi đến đây để hiểu rằng Android chỉ gửi nó để thu gom rác sau một thời gian. Vì vậy, tôi nhìn qua mã của tôi. Để xấu hổ, tôi đã không tuyên bố người nghe TOÀN CẦU mà thay vào đó bên trong onCreateView. Và đó là vì tôi đã nghe Android Studio bảo tôi chuyển đổi người nghe thành một biến cục bộ.


0

Nó có ý nghĩa rằng người nghe được giữ trong WeakHashMap. Bởi vì hầu hết thời gian, các nhà phát triển thích viết mã như thế này.

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

Điều này có vẻ không tệ. Nhưng nếu bộ chứa của OnSharedPreferenceChangeListener không phải là WeakHashMap, thì nó sẽ rất tệ. Nếu đoạn mã trên được viết trong một Hoạt động. Vì bạn đang sử dụng lớp bên trong không tĩnh (ẩn danh), nó sẽ hoàn toàn giữ tham chiếu của thể hiện kèm theo. Điều này sẽ gây rò rỉ bộ nhớ.

Hơn thế nữa, nếu bạn giữ người nghe như một trường, bạn có thể sử dụng registerOnSharedPreferenceChangeListener khi bắt đầu và gọi unregisterOnSharedPreferenceChangeListener ở cuối. Nhưng bạn không thể truy cập một biến cục bộ trong một phương thức ngoài phạm vi của nó. Vì vậy, bạn chỉ có cơ hội để đăng ký nhưng không có cơ hội để hủy đăng ký người nghe. Do đó, sử dụng WeakHashMap sẽ giải quyết vấn đề. Đây là cách tôi đề nghị.

Nếu bạn tạo cá thể người nghe dưới dạng trường tĩnh, Nó sẽ tránh rò rỉ bộ nhớ do lớp bên trong không tĩnh. Nhưng vì người nghe có thể là nhiều người, nên liên quan đến thể hiện. Điều này sẽ giảm chi phí xử lý cuộc gọi lại onSharedPreferenceChanged .


-3

Trong khi đọc dữ liệu Word có thể đọc được chia sẻ bởi ứng dụng đầu tiên, chúng ta nên

Thay thế

getSharedPreferences("PREF_NAME", Context.MODE_PRIVATE);

với

getSharedPreferences("PREF_NAME", Context.MODE_MULTI_PROCESS);

trong ứng dụng thứ hai để có được giá trị cập nhật trong ứng dụng thứ hai.

Nhưng nó vẫn không hoạt động ...


Android không hỗ trợ truy cập SharedPreferences từ nhiều quy trình. Làm như vậy gây ra các vấn đề tương tranh, có thể dẫn đến tất cả các ưu tiên bị mất. Ngoài ra, MODE_MULTI_PROCESS không còn được hỗ trợ.
Sam

@Sam câu trả lời này đã được 3 tuổi, vì vậy đừng bỏ phiếu, nếu nó không hoạt động với bạn trong các phiên bản Android mới nhất. câu trả lời thời gian đã được viết nó là cách tiếp cận tốt nhất để làm như vậy.
shridutt kothari

1
Không, cách tiếp cận đó không bao giờ là quá trình an toàn, ngay cả khi bạn viết câu trả lời này.
Sam

Như @Sam tuyên bố, chính xác, các prefs được chia sẻ không bao giờ được xử lý an toàn. Ngoài ra với shridutt kothari - nếu bạn không thích các downvote loại bỏ câu trả lời không chính xác của bạn (dù sao nó cũng không trả lời câu hỏi của OP). Tuy nhiên, nếu bạn vẫn muốn sử dụng các prefs được chia sẻ theo cách an toàn cho quy trình, bạn cần tạo một quá trình trừu tượng hóa an toàn trên nó, đó là ContentProvider, quy trình này an toàn và vẫn cho phép bạn sử dụng các tùy chọn chia sẻ như cơ chế lưu trữ được duy trì. , Tôi đã làm điều này trước đây và đối với các bộ dữ liệu / sở thích nhỏ thực hiện sqlite bằng một mức lợi nhuận hợp lý.
Đánh dấu Keen

1
@MarkKeen Nhân tiện, tôi thực sự đã thử sử dụng các nhà cung cấp nội dung cho việc này và các nhà cung cấp nội dung có xu hướng thất bại ngẫu nhiên trong Sản xuất do lỗi Android, vì vậy ngày nay tôi chỉ sử dụng các chương trình phát sóng để đồng bộ hóa cấu hình với các quy trình phụ khi cần.
Sam
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.