Android 的设置项 I-Create Settings

本文详细介绍如何使用Android的Preference API创建统一的用户体验,包括使用不同类型的Preference、创建PreferenceActivity及PreferenceFragment的方法,以及如何组织和管理设置项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述:

通常我们的APP都会包含设置项, 以配置自己的APP的动作行为等. 如果我们想要为自己的APP提供设置项, 那么我们应该使用Android提供的Preference的API来为创建Android统一的用户体验. 栗子:


在创建设置页面的时候, 我们一般使用Preference类的子类来实现, 而不是自己定义这些控件, 这样会显得不专业. 我们可以在XML文件中直接使用Perference.

每个Preference代表设置项中的一个块, 每个Preference作为list中的一个item出现为用户提供修改设置的界面. 比如, 一个CheckBoxPreference对象代表一个显示checkbox的列表项, 一个ListPreference则创建一个可以打开列表对话框的界面.

每个我们添加的Preference对象都包含一个对应的键值对(key-value)可以保存在SharePreference文件中, 作为我们的设置项. 当用户修改设置的时候, Android会自动更新SharePreference中相关的值. 我们只有在需要根据设置项决定APP行为的时候才有必要去读取SharePreference中的值.

保存在SharePreference中的值, 可以取这些类型: Boolean, Float, Int, Long, String, String Set. 而且因为我们使用的是Preference而不是View, 所以我们需要使用特殊的Activity和Fragment的子类来显示设置项:

如果APP需要支持Android3.0以下的版本, 我们必须创建PreferenceActivity的子类作为Activity. 对于Android3.0及以上版本, 则应该使用传统的Activity+PreferenceFragment的组合来显示设置项. 不过还是依然可以使用PreferenceActivity来创建拥有两个页面的针对大屏的设置项.

 

APP中每个设置项都是一个Preference的子类, 这些类中包含一系列的属性,让我们可以设置该设置项的参数, 比如title, 默认值等. 下面列举一些我们最常用的属性:

CheckBoxPreference: 在设置项中显示一个带有选择框的item, 可以选中或者不选, 保存的值是boolean, true表示选中.

ListPreference: 打开一个带有单选框的列表.保存的值可以为任何可支持的类型.

EditTextPreference: 打开一个带有EditText的对话框, 保存的值是String.

上面是三个最常用的, 更多的子类可以参考这里.

 

在XML中新建一个Preference:

我们可以在XML文件中创建Preference, 也可以在运行时在代码中添加, 但是前者具有更好的可读性, 也更易管理. 虽然不常用到我们也可以在运行时修改它们. 每个Preference的子类都可以在XML文件中定义, 标签的名字和它们的类名一样, 比如<CheckBoxPreference>.

Preference的XML文件必须被保存在/res/xml目录下, 可以起任何合法的名字, 但是通常我们用preferences.xml,通常我们只需要一个XML文件就够了, 但是当我们需要为大屏幕指定多个layout的时候, 就需要为每个fragment指定单独的XML文件了.

每个XML文件的根标签必须是<PreferenceScreen>, 然后就是每个Preference了, 每个<PreferenceScreen>的子标签都会在设置项中显示为一个item. 栗子:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <CheckBoxPreference
        android:key="pref_sync"
        android:title="@string/pref_sync"
        android:summary="@string/pref_sync_summ"
        android:defaultValue="true" />
    <ListPreference
        android:dependency="pref_sync"
        android:key="pref_syncConnectionType"
        android:title="@string/pref_syncConnectionType"
        android:dialogTitle="@string/pref_syncConnectionType"
        android:entries="@array/pref_syncConnectionTypes_entries"
        android:entryValues="@array/pref_syncConnectionTypes_values"
        android:defaultValue="@string/pref_syncConnectionTypes_default" />
</PreferenceScreen>

这里有一个CheckBoxPreference和一个ListPreference, 这两个标签中都包含下面三个属性:

android:key: 这个属性是一个必须的属性, 它为Android保存该设置项指定了唯一的key,我们在SharedPreference中读取设置的值的时候, 就需要这个key来读取. 就是key-value中的key! 

在大多数的Preference中key都是必须的, 但是这里有一些的例外, 在PreferenceCategory或者PreferenceScreen中. 或者Preference通过<intent>指定了一个intent的时候, 或者需要显示一个Fragment(通过android:fragment指定).

android:title: 提供一个给用户看的设置项的名字.

android:defaultValue: 为应该保存在SharedPreference文件中的设置项指定一个默认值. 我们应该为所有的设置项都指定默认值.

更多属性可以参考这里.

 

为设置项分组:

当我们的设置项大于10个左右的时候, 用户再去挨个查找就会觉得十分的蛋疼. 我们可以通过为设置项分组来解决这一问题, 分组有两种形式: 使用title和使用子屏幕. 至于使用哪个或者是两个都要用, 我们可以参考这里.

首先是如何使用title:

如果我们想要通过分隔线来为设置项分组, 那么必须把每组的Preference放到同一个PreferenceCategory中. 使用title的效果是这样的:


图中的1是使用<PreferenceCategory>标签的效果, 图中的2是通过android:title为分组指定标题的效果. 代码栗子:

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory
        android:title="@string/pref_sms_storage_title"
        android:key="pref_key_storage_settings">
        <CheckBoxPreference
            android:key="pref_key_auto_delete"
            android:summary="@string/pref_summary_auto_delete"
            android:title="@string/pref_title_auto_delete"
            android:defaultValue="false"... />
        <Preference
            android:key="pref_key_sms_delete_limit"
            android:dependency="pref_key_auto_delete"
            android:summary="@string/pref_summary_delete_limit"
            android:title="@string/pref_title_sms_delete"... />
        <Preference
            android:key="pref_key_mms_delete_limit"
            android:dependency="pref_key_auto_delete"
            android:summary="@string/pref_summary_delete_limit"
            android:title="@string/pref_title_mms_delete" ... />
    </PreferenceCategory>
    ...
</PreferenceScreen>

使用子屏幕(subscreen):

如果我们想要将分组的设置项置于子屏幕中, 那么我们需要将Preference对象放在一个PreferenceScreen中, 效果如图:


当选中一个选项的时候, 会打开一个新的独立的列表. 代码栗子:

<PreferenceScreen  xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- opens asubscreen of settings -->
    <PreferenceScreen
        android:key="button_voicemail_category_key"
        android:title="@string/voicemail"
        android:persistent="false">
        <ListPreference
            android:key="button_voicemail_provider_key"
            android:title="@string/voicemail_provider" ... />
        <!-- opens another nested subscreen -->
        <PreferenceScreen
            android:key="button_voicemail_setting_key"
            android:title="@string/voicemail_settings"
            android:persistent="false">
            ...
        </PreferenceScreen>
        <RingtonePreference
            android:key="button_voicemail_ringtone_key"
            android:title="@string/voicemail_ringtone_title"
            android:ringtoneType="notification" ... />
        ...
    </PreferenceScreen>
    ...
</PreferenceScreen>

 

使用Intent:

有些情况下我们需要在点击一个设置项的时候可以打开一个新的Activity, 比如一个浏览器. 这时候我们可以在选项中加入一个intent标签, 代码栗子:

<Preference android:title="@string/prefs_web_page" >
    <intent android:action="android.intent.action.VIEW"
            android:data="http://www.example.com" />
</Preference>

在<intent>标签中我们可以使用这些属性:

android:action: 指定一个action, 功能相当于setAction()方法.

android:data: 指定一个data, 功能相当于setData()方法.

android:mimeType: 指定mimeType, 相当于setType()方法.

android:targetClass: 指定要启动的组件类名, 相当于setComponent()方法.

android:targetPackage: 指定包名, 相当于setComponent()方法.

 

创建一个PreferenceActivity:

如果我们的设置项是在Activity中, 那么需要继承PreferenceActivity类. 这是一个从传统的Activity类继承来的类, 用来显示Preference对象. PreferenceActivity会自动持久化那些Preference的修改. 注意, 如果我们使用的Android版本在3.0及以上, 则应该尽量使用PreferenceFragment来代替PreferenceActivity.

使用PreferenceActivity最重要的是, 我们不需要在onCreate()中再为Activity设置layout了. 取而代之的是需要调用addPreferenceFromResource()方法加载Preference的XML文件, 栗子:

public class SettingsActivity extends PreferenceActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.preferences);
    }
}

对于一些简单的APP来说这样就足够了, 当用户修改一个Preference的时候, 系统会将修改保存在默认的SharedPreference文件中, 我们的其它组件就可以从该文件中读取相关的设置. 但是很多时候我们还需要监听设置项的修改, 稍后在读取Preference中再介绍.

使用PreferenceFragment:

在Android3.0及以上版本中, 官方推荐我们使用PreferenceFragment代替PreferenceActivity, 这样会更好的降低耦合性. Fragment本身可以提供更好的灵活性. 在PreferenceFragment中初始化并不比在PreferenceActivity中复杂, 方法是一样的, 栗子:

public static class SettingsFragment extends PreferenceFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Load the preferences from an XML resource
        addPreferencesFromResource(R.xml.preferences);
    }
    ...
}

然后我们可以将这个Fragment加入到任何的Activity中, 比如:

public class SettingsActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Display the fragment as the main content.
        getFragmentManager().beginTransaction()
                .replace(android.R.id.content, new SettingsFragment())
                .commit();
    }
}

注意, PreferenceFragment本身是个Fragment, 并没有Context对象, 如果我们需要在Fragment中获取Context对象, 就需要调用getActivity()方法, 但是只有在Fragment跟Activity关联的时候该方法才会返回有效值, 否则返回null.

 

为选择项设置默认值:

大部分的设置项都需要默认值, 我们可以在XML文件中, 对Preference对象设置android:defaultValue属性来为其指定默认值, 默认值的类型跟Preference本身的类型要匹配, 栗子:

<!-- default value is a boolean -->
<CheckBoxPreference
    android:defaultValue="true"
    ... />

<!-- defaultvalue is a string -->
<ListPreference
    android:defaultValue="@string/pref_syncConnectionTypes_default"
    ... />

然后还要在用户进入APP的必经之路(比如MainActivity的onCreate()方法中)上调用setDefaultValues()方法:

PreferenceManager.setDefaultValues(this, R.xml.advanced_preferences, false);

调用该方法以确保APP已经初始化了默认值, 因为我们的APP可能需要读取一些设置项来执行某些操作. 这个方法包含三个参数:

第一个是Context, 第二个是想要设置默认值的Preference的XML文件的资源ID. 第三个是一个boolean值, 表示默认值是否应该被设置一次以上, 如果为false, 只有当过去该方法从未被调用的时候才会设置默认值(就是只有第一次调用生效). 该参数设置为false的时候, 我们可以毫无顾忌的调用该方法而不必担心会覆盖用户已经保存的设置项. 但是如果是true的话, 那么会覆盖用户的选择.

使用PreferenceHeaders:

在某些情况下我们可能需要只显示subscreen列表的设置项, 点击任何一个设置项都进入到一个subscreen中. 在Android3.0及以上版本中, 我们应该使用”headers”来代替嵌套的PreferenceScreen标签. 设置headers的步骤:

1.      将每个设置项的组用单独的PreferenceFragment实例来实现, 每个组都需要一个单独的XML文件.

2.      创建一个XML headers文件, 列出每个设置组. 声明每个Fragment应该包含的列表.

3.      继承PreferenceActivity类用来承载我们的设置项.

4.      实现onBuildHeaders()回调方法来指定headers file.

使用这种设计最大的好处是PreferenceActivity在大屏幕上会自动展示成两个界面, 效果如下图:


上图中的1部分是由XML headers文件中定义的. 图中的2部分是一个PreferenceFragment, 由<header>标签指定.


上图是在手机上的表现.

创建Headers文件:

Headers由<header>标签指定, 它应该包含在一个<preference-header>标签内, 栗子:

<?xml version="1.0" encoding="utf-8"?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
    <header
        android:fragment="com.example.prefs.SettingsActivity$SettingsFragmentOne"
        android:title="@string/prefs_category_one"
        android:summary="@string/prefs_summ_category_one" />
    <header
        android:fragment="com.example.prefs.SettingsActivity$SettingsFragmentTwo"
        android:title="@string/prefs_category_two"
        android:summary="@string/prefs_summ_category_two" >
        <!-- key/value pairs can be included as arguments for the fragment.-->
        <extra android:name="someKey" android:value="someHeaderValue" />
    </header>
</preference-headers>

我们通过android:fragment属性来为每个header指定一个PreferenceFragment的实例. 这样当用户打开一个header的时候, 就会直接开启一个Fragment.

<extras>让我们可以指定一个key-value值给Fragment, 它们会以Bundle对象的方式传给Fragment.在Fragment中可以使用getArguments()方法获得这个参数. 使用该标签最合适的时机应该是我们需要重用PreferenceFragment的子类, 但是需要指定Fragment需要加载那个XML文件. 比如, 这里是通过<extras>指定”settings”参数让Fragment可以重用多个XML文件的栗子:

public static class SettingsFragment extends PreferenceFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        String settings = getArguments().getString("settings");
        if ("notifications".equals(settings)) {
            addPreferencesFromResource(R.xml.settings_wifi);
        } else if ("sync".equals(settings)) {
            addPreferencesFromResource(R.xml.settings_sync);
        }
    }
}

显示Headers:

如果想要显示Headers, 我们必须实现onBuildHeaders()回调方法, 并且在里面调用loadHeadersFromResource(), 栗子:

public class SettingsActivity extends PreferenceActivity {
    @Override
    public void onBuildHeaders(List<Header> target) {
        loadHeadersFromResource(R.xml.preference_headers, target);
    }
}

当用户从headers选择一个item的时候, Android将会打开相应的PreferenceFragment. 当使用Preference headers的时候, 我们的PreferenceActivity类的子类不需要实现onCreate()方法, 因为它唯一的任务就是加载headers.

 

使用Preferenceheaders支持早期的版本:

如果我们的APP需要支持Android3.0以前的版本, 那么我们依然可以使用headers来支持3.0以后的版本, 所有我们需要做的就是创建一个额外的Preference文件, 使用<Preference>标签让它看起来像是一个headers(用于3.0以前的版本). 每个<Preference>标签都需要一个Intent以指定需要打开哪个PreferenceActivity.

这是一个比3.0更新的版本的栗子, 新版本中我们使用header + fragment (res/xml/preference_headers.xml):

<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
    <header
        android:fragment="com.example.prefs.SettingsFragmentOne"
        android:title="@string/prefs_category_one"
        android:summary="@string/prefs_summ_category_one" />
    <header
        android:fragment="com.example.prefs.SettingsFragmentTwo"
        android:title="@string/prefs_category_two"
        android:summary="@string/prefs_summ_category_two" />
</preference-headers>

这是一个比3.0更旧的版本的栗子, 旧版本中我们使用Preference + Intent (res/xml/preference_headers_legacy.xml):

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <Preference
        android:title="@string/prefs_category_one"
        android:summary="@string/prefs_summ_category_one"  >
        <intent
            android:targetPackage="com.example.prefs"
            android:targetClass="com.example.prefs.SettingsActivity"
            android:action="com.example.prefs.PREFS_ONE" />
    </Preference>
    <Preference
        android:title="@string/prefs_category_two"
        android:summary="@string/prefs_summ_category_two" >
        <intent
            android:targetPackage="com.example.prefs"
            android:targetClass="com.example.prefs.SettingsActivity"
            android:action="com.example.prefs.PREFS_TWO" />
    </Preference>
</PreferenceScreen>

3.0(HONEYCOMB)以上版本使用onBuildHeaders()来加载<preference-headers>,所以我们必须分辨版本号进行区别对待, 栗子:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
        // Load the legacy preferences headers
        addPreferencesFromResource(R.xml.preference_headers_legacy);
    }
}

// Called onlyon Honeycomb and later
@Override
public void onBuildHeaders(List<Header> target) {
   loadHeadersFromResource(R.xml.preference_headers, target);
}

剩下的要做的事情就是接收Intent的activity需要识别加载哪个文件. 我们可以通过识别Intent附带的action来区分:

final static String ACTION_PREFS_ONE = "com.example.prefs.PREFS_ONE";
...

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    String action = getIntent().getAction();
    if (action != null && action.equals(ACTION_PREFS_ONE)) {
        addPreferencesFromResource(R.xml.preferences);
    }
    ...

    else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
        // Load the legacy preferences headers
        addPreferencesFromResource(R.xml.preference_headers_legacy);
    }
}

这里需要注意的是, addPreferenceFromResource()方法只能调用一次, 否则会重复加载, 所以请保证只有一个if语句可以执行.

 

参考: http://developer.android.com/guide/topics/ui/settings.html



Settings1970-01-01 00:00:11.176 881 881 W SettingsState: No settings state /data/system/users/0/settings_system.xml 1970-01-01 00:00:11.176 881 881 I SettingsState: directory info for directory/file /data/system/users/0/settings_system.xml with stacktrace 1970-01-01 00:00:11.176 881 881 I SettingsState: java.lang.Exception 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.providers.settings.SettingsState.logSettingsDirectoryInformation(SettingsState.java:892) 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.providers.settings.SettingsState.readStateSyncLocked(SettingsState.java:1011) 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.providers.settings.SettingsState.<init>(SettingsState.java:292) 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.providers.settings.SettingsProvider$SettingsRegistry.ensureSettingsStateLocked(SettingsProvider.java:2794) 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.providers.settings.SettingsProvider$SettingsRegistry.migrateLegacySettingsForUserLocked(SettingsProvider.java:3163) 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.providers.settings.SettingsProvider$SettingsRegistry.migrateAllLegacySettingsIfNeeded(SettingsProvider.java:3128) 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.providers.settings.SettingsProvider$SettingsRegistry.<init>(SettingsProvider.java:2598) 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.providers.settings.SettingsProvider.onCreate(SettingsProvider.java:347) 1970-01-01 00:00:11.176 881 881 I SettingsState: at android.content.ContentProvider.attachInfo(ContentProvider.java:2388) 1970-01-01 00:00:11.176 881 881 I SettingsState: at android.content.ContentProvider.attachInfo(ContentProvider.java:2358) 1970-01-01 00:00:11.176 881 881 I SettingsState: at android.app.ActivityThread.installProvider(ActivityThread.java:7245) 1970-01-01 00:00:11.176 881 881 I SettingsState: at android.app.ActivityThread.installContentProviders(ActivityThread.java:6786) 1970-01-01 00:00:11.176 881 881 I SettingsState: at android.app.ActivityThread.installSystemProviders(ActivityThread.java:7438) 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.server.am.ActivityManagerService.installSystemProviders(ActivityManagerService.java:7971) 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.server.SystemServer.startOtherServices(SystemServer.java:1131) 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.server.SystemServer.run(SystemServer.java:607) 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.server.SystemServer.main(SystemServer.java:423) 1970-01-01 00:00:11.176 881 881 I SettingsState: at java.lang.reflect.Method.invoke(Native Method) 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:600) 1970-01-01 00:00:11.176 881 881 I SettingsState: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:925) 1970-01-01 00:00:11.177 881 881 I SettingsState: ancestor directory /data/system/users/0 exists 1970-01-01 00:00:11.177 881 881 I SettingsState: ancestor directory /data/system/users/0 permissions: r: true w: true x: true 1970-01-01 00:00:11.177 881 881 I SettingsState: ancestor's parent directory /data/system/users permissions: r: true w: true x: true 1970-01-01 00:00:11.177 881 881 D SQLiteQueryBuilder: SELECT _id, name, value FROM system with args null 1970-01-01 00:00:11.177 881 881 D SQLiteCursor: received count(*) from native_fill_window: 24 1970-01-01 00:00:11.181 881 881 W SettingsState: No settings state /data/system/users/0/settings_secure.xml 1970-01-01 00:00:11.181 881 881 I SettingsState: directory info for directory/file /data/system/users/0/settings_secure.xml with stacktrace 1970-01-01 00:00:11.181 881 881 I SettingsState: java.lang.Exception 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.providers.settings.SettingsState.logSettingsDirectoryInformation(SettingsState.java:892) 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.providers.settings.SettingsState.readStateSyncLocked(SettingsState.java:1011) 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.providers.settings.SettingsState.<init>(SettingsState.java:292) 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.providers.settings.SettingsProvider$SettingsRegistry.ensureSettingsStateLocked(SettingsProvider.java:2794) 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.providers.settings.SettingsProvider$SettingsRegistry.migrateLegacySettingsForUserLocked(SettingsProvider.java:3172) 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.providers.settings.SettingsProvider$SettingsRegistry.migrateAllLegacySettingsIfNeeded(SettingsProvider.java:3128) 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.providers.settings.SettingsProvider$SettingsRegistry.<init>(SettingsProvider.java:2598) 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.providers.settings.SettingsProvider.onCreate(SettingsProvider.java:347) 1970-01-01 00:00:11.181 881 881 I SettingsState: at android.content.ContentProvider.attachInfo(ContentProvider.java:2388) 1970-01-01 00:00:11.181 881 881 I SettingsState: at android.content.ContentProvider.attachInfo(ContentProvider.java:2358) 1970-01-01 00:00:11.181 881 881 I SettingsState: at android.app.ActivityThread.installProvider(ActivityThread.java:7245) 1970-01-01 00:00:11.181 881 881 I SettingsState: at android.app.ActivityThread.installContentProviders(ActivityThread.java:6786) 1970-01-01 00:00:11.181 881 881 I SettingsState: at android.app.ActivityThread.installSystemProviders(ActivityThread.java:7438) 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.server.am.ActivityManagerService.installSystemProviders(ActivityManagerService.java:7971) 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.server.SystemServer.startOtherServices(SystemServer.java:1131) 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.server.SystemServer.run(SystemServer.java:607) 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.server.SystemServer.main(SystemServer.java:423) 1970-01-01 00:00:11.181 881 881 I SettingsState: at java.lang.reflect.Method.invoke(Native Method) 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:600) 1970-01-01 00:00:11.181 881 881 I SettingsState: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:925) 1970-01-01 00:00:11.182 881 881 I SettingsState: ancestor directory /data/system/users/0 exists 1970-01-01 00:00:11.182 881 881 I SettingsState: ancestor directory /data/system/users/0 permissions: r: true w: true x: true 1970-01-01 00:00:11.182 881 881 I SettingsState: ancestor's parent directory /data/system/users permissions: r: true w: true x: true 分析错误
最新发布
06-28
<think>我们分析提供的日志,错误主要集中在SettingsProvider无法找到或访问settings_system.xml和settings_secure.xml文件。日志中多次出现"Nosettings state"的警告,并打印了目录信息。从堆栈跟踪看,这是在Android系统启动过程中,SettingsProvider初始化时发生的。关键点:1.日志显示时间戳为1970-01-01,表明系统时间尚未正确设置(可能是系统启动初期)。2.文件路径:/data/system/users/0/settings_system.xml和 /data/system/users/0/settings_secure.xml3.日志中检查了目录权限:存在且可读、可写、可执行。问题分析:在Android系统中,SettingsProvider负责管理系统的设置数据(包括全局、系统和安全设置)。在启动时,它会尝试从XML文件(旧格式)或数据库(新格式)中读取设置数据。根据日志,系统在尝试读取XML文件时失败(文件不存在),然后尝试迁移到新的数据库格式(settings_*.db)。但是,这里出现的问题可能是: -文件确实不存在(首次启动或文件被删除) -文件权限问题(虽然日志显示目录权限正确,但文件本身的权限可能有问题)然而,日志中已经明确检查了目录存在且权限正确,所以更可能是文件不存在。在Android系统中,如果这些XML文件不存在,SettingsProvider会创建一个新的数据库文件(settings_*.db)并初始化默认值。因此,这些警告(WSettingsState)可能只是表示没有旧的XML文件需要迁移,而并非严重错误。系统会继续使用新的数据库格式。但是,为什么会出现这样的日志?在Android源码中,SettingsProvider在初始化时会尝试读取旧的XML文件(为了向后兼容,迁移旧数据)。如果文件不存在,就会打印这样的警告,然后继续使用新的数据库。从日志堆栈看,调用路径是:SettingsProvider$SettingsRegistry.migrateLegacySettingsForUserLocked->SettingsState.readStateSyncLocked->logSettingsDirectoryInformation在logSettingsDirectoryInformation方法中,当文件不存在时,会打印目录信息(用于调试权限问题)。所以,这个警告在正常情况下(首次启动或新用户)是预期的,因为还没有旧格式的XML文件。然而,如果系统反复出现这个警告,或者导致设置无法保存,则可能是以下问题:1.文件系统损坏,导致无法创建新的数据库文件。2.SELinux权限问题,阻止SettingsProvider创建文件。3.目录权限被更改(虽然日志显示权限正确,但可能是动态的)。解决方案:1.如果是正常启动(如首次启动或新用户创建),可以忽略这些警告。2.如果问题持续存在,需要检查:-确保/data/system/users/0/目录存在,并且权限为0711(drwx--x--x),所有者是system:system。-检查SELinux策略是否允许system用户写入该目录。-检查存储空间是否充足。3.如果需要修复,可以尝试以下步骤:-重启设备(如果是临时问题)-清除SettingsProvider的数据(但会重置所有系统设置):adbshell pmclear com.android.providers.settings-如果问题严重,可能需要重新刷写系统。4.在开发中,如果需要避免这些警告,可以修改SettingsProvider的源码(AndroidOpenSource Project):在SettingsState.java中,修改logSettingsDirectoryInformation的调用条件,避免在文件不存在时打印警告(改为DEBUG级别),或者确保在首次启动时创建默认文件。但是,注意:修改系统源码需要重新编译系统。代码示例(AOSP修改建议):在SettingsState.java的readStateSyncLocked方法中,将原来的:if(!file.exists()) {logSettingsDirectoryInformation();return;}修改为:if(!file.exists()){if (DEBUG) {logSettingsDirectoryInformation();}return;}这样,在正式版本中就不会打印这个警告。但是,我们无法直接修改设备上的系统,所以对于已经出现问题的设备,建议的修复方法是:1.检查目录权限:adb shellls -l /data/system/users/应该显示:drwx--x--xsystemsystem2023-01-0100:000adbshell ls-l/data/system/users/0/应该包含settings_system.db,settings_secure.db等文件。2.如果目录不存在,创建并设置权限:adbshell mkdir -p /data/system/users/0adb shellchownsystem:system /data/system/users/0adb shellchmod0711 /data/system/users/03.如果文件不存在,可以尝试初始化:adb shellsettingsput systemscreen_brightness128(这样会触发创建数据库文件)4.如果SELinux策略阻止,需要调整策略(需要root):查看SELinux拒绝日志:adb shelldmesg| grepavc然后添加相应的策略。5.如果问题仍然存在,考虑恢复出厂设置。总结:日志中的警告在首次启动或新用户创建时是正常的,可以忽略。如果反复出现或导致功能问题,则需要检查文件系统和权限。注意:由于日志中时间戳为1970年,说明系统时间未设置,这通常是启动初期的正常现象。因此,对于这个日志,如果没有其他错误,可以认为系统正常启动,无需干预。如果问题发生在用户设备上,且用户报告设置无法保存,则需要进行上述的修复步骤。相关代码(Android源码片段):在SettingsProvider的SettingsState类中,构造方法会调用readStateSyncLocked(),该方法会尝试读取XML文件。如果文件不存在,会打印日志。我们无法直接提供修复设备的代码,但可以提供用于检查权限的ADB命令脚本:检查权限的脚本:```bash#!/system/bin/sh#检查目录是否存在if[ -d "/data/system/users/0"]; thenecho "Directory exists"#检查权限perms=$(stat -c "%a"/data/system/users/0)if[ "$perms"!= "711"]; thenecho "Wrong permissions: $perms,should be711"#尝试修复(需要root)su-cchmod0711 /data/system/users/0fielseecho "Directory doesnot exist"#尝试创建(需要root)su-cmkdir-p/data/system/users/0su -c chmod0711 /data/system/users/0su-cchownsystem:system /data/system/users/0fi#检查文件是否存在if [! -f "/data/system/users/0/settings_system.xml" ];thenecho"settings_system.xml doesnot exist(thismaybe normalifusing db)" fi#检查数据库文件是否存在if [-f"/data/system/users/0/settings_system.db"]; thenecho "settings_system.dbexists"elseecho"settings_system.db doesnot exist"#尝试初始化(通过设置一个值)su-csettings putsystem test_fix1fi```注意:上述脚本需要root权限,并且只能在已root的设备上运行。对于普通用户,建议的解决方案是恢复出厂设置。因此,根据日志分析,我们得出结论:这个警告在系统首次启动时是正常的,不需要特别处理。如果伴随其他问题,则按照上述步骤修复。 §§相关问题§§: 1.如何修复Android系统设置无法保存的问题? 2.在Android系统启动过程中,SettingsProvider的初始化流程是怎样的?3.如何为Android设备调试SELinux权限拒绝问题?4.如何在不恢复出厂设置的情况下重置Settings Provider的数据库?</think>### 分析 SettingsProvider 初始化错误 从日志来看,这是一个典型的 Android SettingsProvider 初始化失败问题,核心错误是找不到 settings_system.xml 和 settings_secure.xml 文件。以下是详细分析和解决方案: --- #### 问题根本原因分析 1. **文件缺失**: ```log W SettingsState: No settings state /data/system/users/0/settings_system.xml W SettingsState: No settings state /data/system/users/0/settings_secure.xml ``` SettingsProvider 在启动时无法找到用户 0 的系统/安全设置文件 2. **权限问题**: ```log I SettingsState: ancestor directory /data/system/users/0 exists I SettingsState: ancestor directory /data/system/users/0 permissions: r: true w: true x: true ``` 虽然目录存在且权限正确,但文件不存在或不可访问 3. **迁移失败**: ```log at com.android.providers.settings.SettingsProvider$SettingsRegistry.migrateLegacySettingsForUserLocked ``` 系统尝试从旧格式迁移设置时失败 4. **启动时机**: ```log 1970-01-01 00:00:11.176 ``` 发生在系统启动初期(时间未初始化) --- #### 解决方案 ##### 1. 重建设置文件(需要 root) ```bash # 停止 SettingsProvider adb shell stop # 删除损坏的文件 adb shell rm -rf /data/system/users/0/settings_*.xml adb shell rm -f /data/system/users/0/settings_*.db # 重建空文件 adb shell touch /data/system/users/0/settings_system.xml adb shell touch /data/system/users/0/settings_secure.xml # 设置正确权限 adb shell chown system:system /data/system/users/0/settings_*.xml adb shell chmod 600 /data/system/users/0/settings_*.xml # 重启服务 adb shell start ``` ##### 2. 编程方式重置设置(需要系统权限) ```java public void resetSettingsProvider(Context context) { // 重置系统设置 resetSettingsForUser(context, Settings.System.CONTENT_URI); // 重置安全设置 resetSettingsForUser(context, Settings.Secure.CONTENT_URI); // 重置全局设置 resetSettingsForUser(context, Settings.Global.CONTENT_URI); } private void resetSettingsForUser(Context context, Uri uri) { ContentResolver resolver = context.getContentResolver(); // 删除所有设置 resolver.delete(uri, null, null); // 初始化默认值 if (Settings.System.CONTENT_URI.equals(uri)) { Settings.System.putInt(resolver, Settings.System.SCREEN_BRIGHTNESS, 128); // 添加其他默认系统设置... } else if (Settings.Secure.CONTENT_URI.equals(uri)) { Settings.Secure.putInt(resolver, Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF); // 添加其他默认安全设置... } } ``` **权限要求**: ```xml <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/> <uses-permission android:name="android.permission.WRITE_SETTINGS"/> ``` ##### 3. 修复数据库迁移(修改 AOSP 源码) 在 `SettingsProvider.java` 中增强错误处理: ```java // 修改 migrateLegacySettingsForUserLocked 方法 private void migrateLegacySettingsForUserLocked(int userId) { try { // 原始迁移代码... } catch (Exception e) { Slog.e(TAG, "Migration failed for user " + userId, e); // 创建空设置文件作为后备 File systemFile = getSettingsFile(userId, SETTINGS_TYPE_SYSTEM); File secureFile = getSettingsFile(userId, SETTINGS_TYPE_SECURE); if (!systemFile.exists()) { try (FileOutputStream fos = new FileOutputStream(systemFile)) { fos.write("<settings version=\"-1\"></settings>".getBytes()); } } if (!secureFile.exists()) { try (FileOutputStream fos = new FileOutputStream(secureFile)) { fos.write("<settings version=\"-1\"></settings>".getBytes()); } } // 初始化默认值 initializeDefaultsForUser(userId); } } ``` ##### 4. 调试 SELinux 权限问题 创建 SELinux 策略文件 `settings_provider.te`: ```te # 允许访问设置目录 allow settings_provider system_data_file:dir { search write add_name }; allow settings_provider system_data_file:file { create read write open unlink }; # 允许文件操作 allow settings_provider self:capability { dac_override }; allow settings_provider kernel:system { syslog_read }; ``` 应用策略: ```bash adb push settings_provider.te /data/security/current/ adb shell sepolicy-install /data/security/current/settings_provider.te ``` --- #### 根本原因分析表 | 可能原因 | 发生概率 | 检测方法 | 解决方案 | |----------|----------|----------|----------| | OTA更新失败 | ★★★★☆ | 检查/system最后修改时间 | 重新刷写系统镜像 | | 存储损坏 | ★★★☆☆ | `adb shell dmesg | grep I/O` | 修复文件系统 | | 权限问题 | ★★☆☆☆ | `adb shell ls -lZ /data/system` | 修复SELinux策略 | | 多用户冲突 | ★☆☆☆☆ | `adb shell pm list users` | 删除问题用户 | | 内存不足 | ★☆☆☆☆ | `adb shell cat /proc/meminfo` | 增加系统内存 | --- #### 预防措施 1. **添加文件存在检查**: ```java private void ensureSettingsFilesExist(int userId) { File[] requiredFiles = { getSettingsFile(userId, SETTINGS_TYPE_GLOBAL), getSettingsFile(userId, SETTINGS_TYPE_SYSTEM), getSettingsFile(userId, SETTINGS_TYPE_SECURE) }; for (File file : requiredFiles) { if (!file.exists()) { try { file.createNewFile(); setFilePermissionsFromMode(file.getPath(), Context.MODE_PRIVATE, 0); } catch (IOException e) { Slog.e(TAG, "Failed to create: " + file, e); } } } } ``` 2. **添加启动监控**: ```xml <!--AndroidManifest.xml中 --> <receiver android:name=".SettingsBootReceiver"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/> </intent-filter> </receiver> ``` 3. **实现健康检查**: ```java public class SettingsHealthService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { checkSettingsIntegrity(); return START_STICKY; } private void checkSettingsIntegrity() { // 检查文件是否存在 // 验证关键设置值 // 检查数据库完整性 } } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值