当前位置: 首页 > news >正文

Android Settings解析

Android Settings 系列文章:

  • Android Settings解析
  • SettingsIntelligence
  • SettingsProvider

首语

Android设置应用是Android系统中一个非常重要的系统应用,它允许用户调整和设置系统的各种参数和功能(系统设置/自定义设置/控制应用权限/开发者选项/系统信息等),使用户获得更好的使用体验。同时它一般也是Android系统开发者了解深入的第一个系统级应用,也是用户使用最频繁的系统应用。

源码目录

AOSP源码路径为packages/apps/Settings。src/com/android/settings目录下包含Settings的主要源码。libs目录下的contextualcards.aar包含实现上下文卡片功能的代码和资源,它可以将相关的内容组织在一起,以卡片的形式展现给用户。res目录下包含各种静态资源。Android.bp文件中可以看到模块名为"Settings"。

设计指南

Display

上图是Settings里一个普通的页面,从这个页面可以看出它将许多设置放在一起,设置列表是多个控件的组合。

它有如下优点:

  • 提供一个很好的概述。用户应该能够浏览设置屏幕并了解所有单独的设置及其值。
  • 直观的设置项目。常用设置放在屏幕顶部。限制一个屏幕上的设置数量。将一些设置移动到单独的屏幕来创建直观的菜单。
  • 使用明确的标题和状态。标题简短而有意义。在标题下方,显示状态以突出设置的值,显示具体细节。

关于Settings设计的详细规则及细节,可以参考官网:设计指南

Preference

在Android 常用组件里,存在一个Preference组件,它提供了一个方便的用户界面,用于管理和显示应用程序的各种设置选项,让用户可以轻松浏览和更改应用程序的设置。Preference还通过SharedPreference实现保存读取数据,以其key作为SharedPreference的键,实现持久化数据。Settings中大多数菜单都是通过Preference去实现,且使用的是androidx包的Preference,因此首先了解下Preference的使用。

Preference组件和其它页面组件使用类似,区别在于XML 资源必须放置于 res/xml/ 目录,Preference的根标签必须为PreferenceScreen。举例如下:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreenxmlns:andoird="http://schemas.android.com/apk/res-auto"><PreferenceCategoryapp:key="prefer_category"><Preferenceandoird:key="feedback"andoird:title="Send feedback"andoird:summary="Report technical issues or suggest new features"/></PreferenceCategory>
</PreferenceScreen>

PreferenceCategory是对Preferences进行分组的标签,显示类别标题,并在视觉上进行分隔Preference。如设计指南中的Settings Display页面的截图中Brightness/Lock display类别标签。

以下是Preference相关属性的介绍:

attrdescription
andoird:allowDividerAbove在菜单上显示一条分割线
andoird:allowDividerBelow在菜单下显示一条分割线
android:defaultValue默认值。
android:dependency设置此元素附属于另一个元素,依赖的可用则当前元素也可用(enable),反之。
andoird:enableCopying启用长按复制
android:enabled设置是否可用。
android:fragment指定跳转fragment。
android:icon指定左侧的图标。
andoird:iconSpaceReserved为图标预留位置,菜单向右偏移,默认false
andoird:isPreferenceVisible菜单是否显示
android:key选项的名称,也是用来存储时唯一的key。
android:layout给当前元素指定一个自定义布局。
android:order偏好的顺序。如果不指定,默认的顺序将字母。
android:persistent是否将其值持久化。
android:selectable设置是否可以选择操作。
android:shouldDisableView当enabled设置为false变暗,同时此属性设置为false时disable但不变暗。
andoird:singleLineTitle菜单title限制为一行,默认为true
android:summary摘要,配置的简要说明,显示在标题下面。
android:title选项的标题,当没有设置summary时自动垂直居中显示。
android:widgetLayout控件可调小部件的布局。是为一个优先选择的布局,比如一个复选框选择要指定一个自定义布局(注意:包括的只是复选框)在这里。

Setting中扩展的attr如下:

    <declare-styleable name="Preference"><!-- 搜索关键词 --><attr name="keywords" format="string" /><!-- 是否可搜索,默认为true --><attr name="searchable" format="boolean" /><!-- Preference controller类 --><attr name="controller" format="string" /><!-- 自定义字幕 --><attr name="unavailableSliceSubtitle" format="string" /><!-- Preference针对work profile,默认为false --><attr name="forWork" format="boolean" /><!-- 用于在双窗格上突出显示菜单首选项的标识符 --><attr name="highlightableMenuKey" format="string" /></declare-styleable>

查看Preference的源码可知,还有一些自定义Preference实现的组件,如CheckBoxPreference/DropDownPreference/EditTextPreference/ListPreference/SwitchPreference等,它是针对不同Android控件(checkbox/dropdown/edittext等)实现的自定义Preference,如需使用只需要在xml引用即可

然后创建一个fragment,继承于PreferenceFragmentCompat。onCreatePreferences方法在PreferenceFragmentCompat的onCreate方法调用,用于创建Prerefence。通过setPreferencesFromResource引用定义的Preference xml资源。这样通过Preference实现的一个简单的菜单就显示在屏幕上了。

public class SettingsFragment extends PreferenceFragmentCompat {@Overridepublic void onCreatePreferences(Bundle savedInstanceState, String rootKey) {setPreferencesFromResource(R.xml.root_preferences, rootKey);}//preference点击事件,通过key区分@Overridepublic boolean onPreferenceTreeClick(@NonNull Preference preference) {return super.onPreferenceTreeClick(preference);}    
}

看下Preference的点击事件实现,如果对应的fragment实现了OnPreferenceStartFragmentCallback,重写了onPreferenceStartFragment方法,那么Preference的跳转实现就在onPreferenceStartFragment方法里,并返回处理结果,如果没有实现OnPreferenceStartFragmentCallback,则去获取xml中设置的android:fragment或者setFragment设置的fragment跳转。

public abstract class PreferenceFragmentCompat extends Fragment implementsPreferenceManager.OnPreferenceTreeClickListener,PreferenceManager.OnDisplayPreferenceDialogListener,PreferenceManager.OnNavigateToScreenListener,DialogPreference.TargetFragment {@Overridepublic boolean onPreferenceTreeClick(@NonNull Preference preference) {if (preference.getFragment() != null) {boolean handled = false;if (getCallbackFragment() instanceof OnPreferenceStartFragmentCallback) {handled = ((OnPreferenceStartFragmentCallback) getCallbackFragment()).onPreferenceStartFragment(this, preference);}Fragment callbackFragment = this;while (!handled && callbackFragment != null) {if (callbackFragment instanceof OnPreferenceStartFragmentCallback) {handled = ((OnPreferenceStartFragmentCallback) callbackFragment).onPreferenceStartFragment(this, preference);}callbackFragment = callbackFragment.getParentFragment();}if (!handled && getContext() instanceof OnPreferenceStartFragmentCallback) {handled = ((OnPreferenceStartFragmentCallback) getContext()).onPreferenceStartFragment(this, preference);}if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback) {handled = ((OnPreferenceStartFragmentCallback) getActivity()).onPreferenceStartFragment(this, preference);}if (!handled) {   final FragmentManager fragmentManager = getParentFragmentManager();final Bundle args = preference.getExtras();final Fragment fragment = fragmentManager.getFragmentFactory().instantiate(requireActivity().getClassLoader(), preference.getFragment());fragment.setArguments(args);fragment.setTargetFragment(this, 0);fragmentManager.beginTransaction().replace(((View) requireView().getParent()).getId(), fragment).addToBackStack(null).commit();}return true;}return false;}            
}

androidx包中Preference只有针对Fragment的实现,没有针对Activity的实现。还有针对Dialog实现的PreferenceDialogFragmentCompat。在dialog里引用preference。

页面加载分析

本文以Android 13 Settings源码为例进行分析。

首页加载流程

在AndroidManifest.xml中可以看到,启动activity为Settings,Settings中包含大量的静态类继承于SettingsActivity。

 <activity-alias android:name="Settings"android:label="@string/settings_label_launcher"android:taskAffinity="com.android.settings.root"android:launchMode="singleTask"android:exported="true"android:targetActivity=".homepage.SettingsHomepageActivity"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter><meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/></activity-alias>

查看SettingsActivity的onCreate方法,首先读取fragment class和HighlightMenuKey,设置布局为settings_main_prefs.xml,通过intent传递的数据显示不同的布局。切换fragment或根据之前保存状态显示页面。settings_main_prefs.xml中包含顶部的switch bar,底部的button(Back/Skip/Next),和中间的framelayout显示Fragment,switch bar和button默认隐藏。

public class SettingsActivity extends SettingsBaseActivityimplements PreferenceManager.OnPreferenceTreeClickListener,PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,ButtonBarHandler, FragmentManager.OnBackStackChangedListener {@Overrideprotected void onCreate(Bundle savedState) {// Should happen before any call to getIntent()getMetaData();final Intent intent = getIntent();if (shouldShowTwoPaneDeepLink(intent) && tryStartTwoPaneDeepLink(intent)) {finish();super.onCreate(savedState);return;}super.onCreate(savedState);Log.d(LOG_TAG, "Starting onCreate");createUiFromIntent(savedState, intent);}private void getMetaData() {try {ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),PackageManager.GET_META_DATA);if (ai == null || ai.metaData == null) return;//读取设置的fragmentmFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);mHighlightMenuKey = ai.metaData.getString(META_DATA_KEY_HIGHLIGHT_MENU_KEY);} catch (NameNotFoundException nnfe) {// No recoveryLog.d(LOG_TAG, "Cannot get Metadata for: " + getComponentName().toString());}}protected void createUiFromIntent(Bundle savedState, Intent intent) {...setContentView(R.layout.settings_main_prefs);...if (savedState != null) {// We are restarting from a previous saved state; used that to initialize, instead// of starting fresh.setTitleFromIntent(intent);ArrayList<DashboardCategory> categories =savedState.getParcelableArrayList(SAVE_KEY_CATEGORIES);if (categories != null) {mCategories.clear();mCategories.addAll(categories);setTitleFromBackStack();}} else {//加载fragmentlaunchSettingFragment(initialFragmentName, intent);}}
}

AndroidManifest中并没有传递Settings对应的fragment数据,而是指定targetActivity为SettingsHomepageActivity,查看SettingsHomepageActivity的onCreate方法,布局为settings_homepage_container.xml,然后初始化搜索栏。设备不是低内存的情况下加载Suggestion菜单,设置fragment为TopLevelSettings。

@Override
protected void onCreate(Bundle savedInstanceState) {...setContentView(R.layout.settings_homepage_container);...initSearchBarView();...if (!getSystemService(ActivityManager.class).isLowRamDevice()) {initAvatarView();final boolean scrollNeeded = mIsEmbeddingActivityEnabled&& !TextUtils.equals(getString(DEFAULT_HIGHLIGHT_MENU_KEY), highlightMenuKey);showSuggestionFragment(scrollNeeded);if (FeatureFlagUtils.isEnabled(this, FeatureFlags.CONTEXTUAL_HOME)) {showFragment(() -> new ContextualCardsFragment(), R.id.contextual_cards_content);((FrameLayout) findViewById(R.id.main_content)).getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);}}mMainFragment = showFragment(() -> {final TopLevelSettings fragment = new TopLevelSettings();fragment.getArguments().putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY,highlightMenuKey);return fragment;}, R.id.main_content);...
}
private void initSearchBarView() {final Toolbar toolbar = findViewById(R.id.search_action_bar);FeatureFactory.getFactory(this).getSearchFeatureProvider().initSearchToolbar(this /* activity */, toolbar, SettingsEnums.SETTINGS_HOMEPAGE);if (mIsEmbeddingActivityEnabled) {final Toolbar toolbarTwoPaneVersion = findViewById(R.id.search_action_bar_two_pane);//初始化搜索实现FeatureFactory.getFactory(this).getSearchFeatureProvider().initSearchToolbar(this /* activity */, toolbarTwoPaneVersion,SettingsEnums.SETTINGS_HOMEPAGE);}}

在搜索的实现类SearchFeatureProviderImpl中可以看到构造的搜索跳转intent如下,可以发现Settings的搜索核心实现在另外一个app内,包名为com.android.settings.intelligence。在SettingsIntelligence 这篇文章会对Settings搜索和SettingsIntelligence模块进行深入分析,继续分析Settings页面加载。

    @Overridepublic Intent buildSearchIntent(Context context, int pageId) {return new Intent(Settings.ACTION_APP_SEARCH_SETTINGS).setPackage(getSettingsIntelligencePkgName(context)).putExtra(Intent.EXTRA_REFERRER, buildReferrer(context, pageId));}default String getSettingsIntelligencePkgName(Context context) {return context.getString(R.string.config_settingsintelligence_package_name);}<!-- Settings intelligence package name --><string name="config_settingsintelligence_package_name" translatable="false">com.android.settings.intelligence</string>

首先看下TopLevelSettings的继承关系,TopLevelSettings继承于DashboardFragment,DashboardFragment是静态和动态Settings 菜单的基类,Settings中大多数菜单对应的fragment继承于DashboardFragment,它继承于SettingsPreferenceFragment,SettingsPreferenceFragment是Settings fragment的基类,它继承于InstrumentedPreferenceFragment,它记录fragment显示状态,继承于ObservablePreferenceFragment,ObservablePreferenceFragment是在SettingsLib里定义的,模块路径:frameworks/base/packages/SettingsLib,在后面会对这个模块深入分析。ObservablePreferenceFragment继承于PreferenceFragmentCompat。

public class TopLevelSettings extends DashboardFragment implements SplitLayoutListener,PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {public TopLevelSettings() {final Bundle args = new Bundle();// Disable the search icon because this page uses a full search view in actionbar.args.putBoolean(NEED_SEARCH_ICON_IN_ACTION_BAR, false);setArguments(args);}//设置preference对应xml资源@Overrideprotected int getPreferenceScreenResId() {return R.xml.top_level_settings;}@Overridepublic void onAttach(Context context) {super.onAttach(context);HighlightableMenu.fromXml(context, getPreferenceScreenResId());use(SupportPreferenceController.class).setActivity(getActivity());}
}

首先查看onAttach方法,调用DashboardFragment的use方法获取SupportPreferenceController实例。SupportPreferenceController存储在mPreferenceControllers中,通过addPreferenceController方法将PreferenceController添加到mPreferenceControllers中,在DashboardFragment的onAttach方法中会调用addPreferenceController,通过createPreferenceControllers方法将代码设置的controller添加到mControllers集合,然后读取xml中设置的controller添加到mControllers集合。

然后看下加载Preference的onCreatePreferences方法,首先通过getPreferenceScreenResId获取对应的xml资源,TopLevelSettings对应的是top_level_settings.xml,xml中定义了Settings首页菜单,最终通过addPreferencesFromResource方法显示Preference。在TopLevelSettings的onCreatePreferences方法还对图标颜色进行了处理。Preference点击事件调用Preferencecontroller的handlePreferenceTreeClick方法。

public abstract class DashboardFragment extends SettingsPreferenceFragmentimplements CategoryListener, Indexable, PreferenceGroup.OnExpandButtonClickListener,BasePreferenceController.UiBlockListener {@Overridepublic void onAttach(Context context) {super.onAttach(context);...// Load preference controllers from codefinal List<AbstractPreferenceController> controllersFromCode =createPreferenceControllers(context);// Load preference controllers from xml definitionfinal List<BasePreferenceController> controllersFromXml = PreferenceControllerListHelper.getPreferenceControllersFromXml(context, 			             getPreferenceScreenResId());// Filter xml-based controllers in case a similar controller is created from code already.final List<BasePreferenceController> uniqueControllerFromXml =PreferenceControllerListHelper.filterControllers(controllersFromXml, controllersFromCode);// Add unique controllers to list.if (controllersFromCode != null) {mControllers.addAll(controllersFromCode);}mControllers.addAll(uniqueControllerFromXml);for (AbstractPreferenceController controller : mControllers) {addPreferenceController(controller);}	}//获取对应Preference controller实例protected <T extends AbstractPreferenceController> T use(Class<T> clazz) {List<AbstractPreferenceController> controllerList = mPreferenceControllers.get(clazz);if (controllerList != null) {if (controllerList.size() > 1) {Log.w(TAG, "Multiple controllers of Class " + clazz.getSimpleName()+ " found, returning first one.");}return (T) controllerList.get(0);}return null;}protected void addPreferenceController(AbstractPreferenceController controller) {if (mPreferenceControllers.get(controller.getClass()) == null) {mPreferenceControllers.put(controller.getClass(), new ArrayList<>());}mPreferenceControllers.get(controller.getClass()).add(controller);}@Overridepublic void onCreatePreferences(Bundle savedInstanceState, String rootKey) {checkUiBlocker(mControllers);refreshAllPreferences(getLogTag());...}private void refreshAllPreferences(final String tag) {...// Add resource based tiles.displayResourceTiles();...}private void displayResourceTiles() {final int resId = getPreferenceScreenResId();if (resId <= 0) {return;}addPreferencesFromResource(resId);final PreferenceScreen screen = getPreferenceScreen();screen.setOnExpandButtonClickListener(this);displayResourceTilesToScreen(screen);}@Overridepublic boolean onPreferenceTreeClick(Preference preference) {final Collection<List<AbstractPreferenceController>> controllers =mPreferenceControllers.values();for (List<AbstractPreferenceController> controllerList : controllers) {for (AbstractPreferenceController controller : controllerList) {if (controller.handlePreferenceTreeClick(preference)) {// log here since calling super.onPreferenceTreeClick will be skippedwritePreferenceClickMetric(preference);return true;}}}return super.onPreferenceTreeClick(preference);}
}

接下来我们以SupportPreferenceController为例,分析下PreferenceController。它继承于BasePreferenceController,BasePreferenceController继承于AbstractPreferenceController。

public class SupportPreferenceController extends BasePreferenceController {//指定显示状态@Overridepublic int getAvailabilityStatus() {return mSupportFeatureProvider == null ? UNSUPPORTED_ON_DEVICE : AVAILABLE;}//点击事件@Overridepublic boolean handlePreferenceTreeClick(Preference preference) {if (preference == null || mActivity == null ||!TextUtils.equals(preference.getKey(), getPreferenceKey())) {return false;}mSupportFeatureProvider.startSupport(mActivity);return true;}
}

AbstractPreferenceController是一个抽象类,主要方法如下:

public abstract class AbstractPreferenceController {//preference是否有效public abstract boolean isAvailable();//preference点击事件public boolean handlePreferenceTreeClick(Preference preference) {return false;}//显示preferencepublic void displayPreference(PreferenceScreen screen) {final String prefKey = getPreferenceKey();if (TextUtils.isEmpty(prefKey)) {Log.w(TAG, "Skipping displayPreference because key is empty:" + getClass().getName());return;}if (isAvailable()) {setVisible(screen, prefKey, true /* visible */);if (this instanceof Preference.OnPreferenceChangeListener) {final Preference preference = screen.findPreference(prefKey);preference.setOnPreferenceChangeListener((Preference.OnPreferenceChangeListener) this);}} else {setVisible(screen, prefKey, false /* visible */);}}//更新preference状态(summary)public void updateState(Preference preference) {refreshSummary(preference);}//preference keypublic abstract String getPreferenceKey();
}

BasePreferenceController对AbstractPreferenceController进行了简单封装,对Preference状态进行处理,共有6中状态,其次对Preference搜索支持也进行了处理。

public abstract class BasePreferenceController extends AbstractPreferenceController implements Sliceable {@Retention(RetentionPolicy.SOURCE)@IntDef({AVAILABLE, AVAILABLE_UNSEARCHABLE, UNSUPPORTED_ON_DEVICE, DISABLED_FOR_USER,DISABLED_DEPENDENT_SETTING, CONDITIONALLY_UNAVAILABLE})public @interface AvailabilityStatus {}//preference 有效public static final int AVAILABLE = 0;//preference 有效不能搜索public static final int AVAILABLE_UNSEARCHABLE = 1;//当前不可用,将来可能可用public static final int CONDITIONALLY_UNAVAILABLE = 2;//设备不支持public static final int UNSUPPORTED_ON_DEVICE = 3;//当前用户不支持public static final int DISABLED_FOR_USER = 4;//preference置灰,无法更改,依赖其它设置public static final int DISABLED_DEPENDENT_SETTING = 5;//指定Preference状态@AvailabilityStatuspublic abstract int getAvailabilityStatus();//preference 有效的实现@Overridepublic final boolean isAvailable() {if (mIsForWork && mWorkProfileUser == null) {return false;}final int availabilityStatus = getAvailabilityStatus();return (availabilityStatus == AVAILABLE|| availabilityStatus == AVAILABLE_UNSEARCHABLE|| availabilityStatus == DISABLED_DEPENDENT_SETTING);}//针对DISABLED_DEPENDENT_SETTING状态进行置灰@Overridepublic void displayPreference(PreferenceScreen screen) {super.displayPreference(screen);if (getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) {// Disable preference if it depends on another setting.final Preference preference = screen.findPreference(getPreferenceKey());if (preference != null) {preference.setEnabled(false);}}}
}

从上面可以看出来,Preferencecontroller它是Preference的控制器,控制Preference的显示,点击事件,搜索。Settings中大多数Preference的控制器都继承于BasePreferenceController。以上就是首页菜单加载的流程。

那SupportPreferenceContoller是那个菜单的控制器呢,它是首页Tips & support菜单的控制器。top_level_settings.xml有定义这个preference。Settings中大多数是以这样的实现构成的,xml定义Preference和引用Preferencecontroller,Preferencecontroller去实现对应菜单的逻辑。

二级页面加载流程

首先查看首页菜单的点击事件,它是获取了Preference controller的handlePreferenceTreeClick方法处理点击事件。

public abstract class DashboardFragment extends SettingsPreferenceFragmentimplements CategoryListener, Indexable, PreferenceGroup.OnExpandButtonClickListener,BasePreferenceController.UiBlockListener {@Overridepublic boolean onPreferenceTreeClick(Preference preference) {final Collection<List<AbstractPreferenceController>> controllers =mPreferenceControllers.values();for (List<AbstractPreferenceController> controllerList : controllers) {for (AbstractPreferenceController controller : controllerList) {if (controller.handlePreferenceTreeClick(preference)) {// log here since calling super.onPreferenceTreeClick will be skippedwritePreferenceClickMetric(preference);return true;}}}return super.onPreferenceTreeClick(preference);}
}

Preference controller的点击基础实现如下:

public abstract class BasePreferenceController extends AbstractPreferenceController implements  Sliceable {@Overridepublic boolean handlePreferenceTreeClick(Preference preference) {if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) {return super.handlePreferenceTreeClick(preference);}if (!mIsForWork || mWorkProfileUser == null) {return super.handlePreferenceTreeClick(preference);}final Bundle extra = preference.getExtras();extra.putInt(EXTRA_USER_ID, mWorkProfileUser.getIdentifier());new SubSettingLauncher(preference.getContext()).setDestination(preference.getFragment()).setSourceMetricsCategory(preference.getExtras().getInt(CATEGORY,SettingsEnums.PAGE_UNKNOWN)).setArguments(preference.getExtras()).setUserHandle(mWorkProfileUser).launch();return true;}
}

如果Preference controller不处理,则通过onPreferenceStartFragment方法,TopLevelSettings实现了OnPreferenceStartFragmentCallback,

public class TopLevelSettings extends DashboardFragment implements SplitLayoutListener,PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {@Overridepublic boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {new SubSettingLauncher(getActivity()).setDestination(pref.getFragment()).setArguments(pref.getExtras()).setSourceMetricsCategory(caller instanceof Instrumentable? ((Instrumentable) caller).getMetricsCategory(): Instrumentable.METRICS_CATEGORY_UNKNOWN).setTitleRes(-1).setIsSecondLayerPage(true).launch();return true;}
}

可以发现,跳转二级页面的实现都是通过SubSettingLauncher来传递参数并且跳转目标fragment。toIntent方法构造调整intent,可以看到跳转的类是SubSettings,launcher方法进行跳转。

public class SubSettingLauncher {public void launch() {...final Intent intent = toIntent();boolean launchAsUser = mLaunchRequest.mUserHandle != null&& mLaunchRequest.mUserHandle.getIdentifier() != UserHandle.myUserId();boolean launchForResult = mLaunchRequest.mResultListener != null;if (launchAsUser && launchForResult) {launchForResultAsUser(intent, mLaunchRequest.mUserHandle,mLaunchRequest.mResultListener, mLaunchRequest.mRequestCode);} else if (launchAsUser && !launchForResult) {launchAsUser(intent, mLaunchRequest.mUserHandle);} else if (!launchAsUser && launchForResult) {launchForResult(mLaunchRequest.mResultListener, intent, mLaunchRequest.mRequestCode);} else {launch(intent);}}public Intent toIntent() {final Intent intent = new Intent(Intent.ACTION_MAIN);copyExtras(intent);intent.setClass(mContext, SubSettings.class);if (TextUtils.isEmpty(mLaunchRequest.mDestinationName)) {throw new IllegalArgumentException("Destination fragment must be set");}intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, mLaunchRequest.mDestinationName);if (mLaunchRequest.mSourceMetricsCategory < 0) {throw new IllegalArgumentException("Source metrics category must be set");}intent.putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY,mLaunchRequest.mSourceMetricsCategory);intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, mLaunchRequest.mArguments);intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RES_PACKAGE_NAME,mLaunchRequest.mTitleResPackageName);intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID,mLaunchRequest.mTitleResId);intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE, mLaunchRequest.mTitle);intent.addFlags(mLaunchRequest.mFlags);intent.putExtra(SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,mLaunchRequest.mTransitionType);intent.putExtra(SettingsActivity.EXTRA_IS_SECOND_LAYER_PAGE,mLaunchRequest.mIsSecondLayerPage);return intent;}@VisibleForTestingvoid launch(Intent intent) {mContext.startActivity(intent);}
}

SubSettings继承于SettingsActivity,说明首页菜单除了自定义实现页面跳转逻辑的之外,其它都是跳转到SubSettings这个Activity,这里有一个小技巧,正常情况下我们抓取当前页面的Activity可以通过以下命令:

adb shell dumpsys window | grep mCurrentFocus

但是我们不清楚这个页面对应的fragment,通过上面命令都是SubSettings。在跳转时可以通过以下命令获取fragment:

adb logcat -s "SubSettings"

这样就打印出了具体的启动fragment。

public class SubSettings extends SettingsActivity {@Overridepublic boolean onNavigateUp() {finish();return true;}@Overrideprotected boolean isValidFragment(String fragmentName) {//打印页面Log.d("SubSettings", "Launching fragment " + fragmentName);return true;}
}

剩下的相关逻辑都和TopLevelSettings类似,这里不继续展开分析了。

动态插入菜单

在Settings里的一些菜单,我们会发现一些菜单在xml和代码中并未添加,但实际上显示在页面上,这是为什么呢?原来是Settings支持动态插入菜单。实现逻辑如下:

在创建Preference的时候,refreshAllPreferences方法刷新Preference,包括来自xml的静态Preference和动态Preference。动态Preference添加实现在refreshDashboardTiles方法中。

public abstract class DashboardFragment extends SettingsPreferenceFragmentimplements CategoryListener, Indexable, PreferenceGroup.OnExpandButtonClickListener,BasePreferenceController.UiBlockListener {@Overridepublic void onCreatePreferences(Bundle savedInstanceState, String rootKey) {checkUiBlocker(mControllers);refreshAllPreferences(getLogTag());...}private void refreshAllPreferences(final String tag) {...// Add resource based tiles.displayResourceTiles();//动态PreferencerefreshDashboardTiles(tag);}private void refreshDashboardTiles(final String tag) {final PreferenceScreen screen = getPreferenceScreen();final DashboardCategory category =mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());...final List<Tile> tiles = category.getTiles();// Create a list to track which tiles are to be removed.final Map<String, List<DynamicDataObserver>> remove = new ArrayMap(mDashboardTilePrefKeys);// Install dashboard tiles and collect pending observers.final boolean forceRoundedIcons = shouldForceRoundedIcon();final List<DynamicDataObserver> pendingObservers = new ArrayList<>();for (Tile tile : tiles) {final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);...final List<DynamicDataObserver> observers;if (mDashboardTilePrefKeys.containsKey(key)) {// Have the key already, will rebind.final Preference preference = screen.findPreference(key);observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(), this, forceRoundedIcons, preference, tile, key,mPlaceholderPreferenceController.getOrder());} else {// Don't have this key, add it.final Preference pref = createPreference(tile);observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(), this, forceRoundedIcons, pref, tile, key,mPlaceholderPreferenceController.getOrder());//添加Preferencescreen.addPreference(pref);registerDynamicDataObservers(observers);mDashboardTilePrefKeys.put(key, observers);}if (observers != null) {pendingObservers.addAll(observers);}remove.remove(key);...//类别key             public String getCategoryKey() {return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName());}
}

首先获取了类别 key,PARENT_TO_CATEGORY_KEY_MAP中实现了页面和key的对应。通过页面class name来确定页面对应的key。

public class DashboardFragmentRegistry {static {PARENT_TO_CATEGORY_KEY_MAP = new ArrayMap<>();PARENT_TO_CATEGORY_KEY_MAP.put(TopLevelSettings.class.getName(),CategoryKey.CATEGORY_HOMEPAGE);PARENT_TO_CATEGORY_KEY_MAP.put(NetworkDashboardFragment.class.getName(), CategoryKey.CATEGORY_NETWORK);PARENT_TO_CATEGORY_KEY_MAP.put(ConnectedDeviceDashboardFragment.class.getName(),CategoryKey.CATEGORY_CONNECT);PARENT_TO_CATEGORY_KEY_MAP.put(AdvancedConnectedDeviceDashboardFragment.class.getName(),...}
}

页面key的定义在CategoryKey类中。这样通过key就清楚当前页面是否动态加载那些菜单。

public final class CategoryKey {// Activities in this category shows up in Settings homepage.public static final String CATEGORY_HOMEPAGE = "com.android.settings.category.ia.homepage";// Top level category.public static final String CATEGORY_NETWORK = "com.android.settings.category.ia.wireless";public static final String CATEGORY_CONNECT = "com.android.settings.category.ia.connect";public static final String CATEGORY_DEVICE = "com.android.settings.category.ia.device";public static final String CATEGORY_APPS = "com.android.settings.category.ia.apps";...
}

getTilesForCategory方法的实现在DashboardFeatureProviderImpl类中,它是通过CategoryManager类的getTilesByCategory方法实现。mCategories是获取所有动态菜单的集合。

public class CategoryManager {public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) {tryInitCategories(context);return mCategoryByKeyMap.get(categoryKey);}private synchronized void tryInitCategories(Context context) {// Keep cached tiles by default. The cache is only invalidated when InterestingConfigChange// happens.tryInitCategories(context, false /* forceClearCache */);}private synchronized void tryInitCategories(Context context, boolean forceClearCache) {if (mCategories == null) {final boolean firstLoading = mCategoryByKeyMap.isEmpty();if (forceClearCache) {mTileByComponentCache.clear();}mCategoryByKeyMap.clear();//获取categories listmCategories = TileUtils.getCategories(context, mTileByComponentCache);for (DashboardCategory category : mCategories) {mCategoryByKeyMap.put(category.key, category);}backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap);sortCategories(context, mCategoryByKeyMap);filterDuplicateTiles(mCategoryByKeyMap);if (firstLoading) {logTiles(context);final DashboardCategory homepageCategory = mCategoryByKeyMap.get(CategoryKey.CATEGORY_HOMEPAGE);if (homepageCategory == null) {return;}for (Tile tile : homepageCategory.getTiles()) {final String key = tile.getKey(context);if (TextUtils.isEmpty(key)) {Log.w(TAG, "Key hint missing for homepage tile: " + tile.getTitle(context));continue;}HighlightableMenu.addMenuKey(key);}}}}
}

从loadActivityTiles方法里可以看出,在Settings里动态插入菜单只能是系统应用。

源码路径:frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java

public class TileUtils {public static final String EXTRA_SETTINGS_ACTION = "com.android.settings.action.EXTRA_SETTINGS";public static final String IA_SETTINGS_ACTION = "com.android.settings.action.IA_SETTINGS";private static final String SETTINGS_ACTION = "com.android.settings.action.SETTINGS";public static List<DashboardCategory> getCategories(Context context,Map<Pair<String, String>, Tile> cache) {final long startTime = System.currentTimeMillis();final boolean setup =Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0) != 0;final ArrayList<Tile> tiles = new ArrayList<>();final UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);for (UserHandle user : userManager.getUserProfiles()) {// TODO: Needs much optimization, too many PM queries going on here.if (user.getIdentifier() == ActivityManager.getCurrentUser()) {loadTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true);loadTilesForAction(context, user, OPERATOR_SETTINGS, cache,OPERATOR_DEFAULT_CATEGORY, tiles, false);loadTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,MANUFACTURER_DEFAULT_CATEGORY, tiles, false);}if (setup) {loadTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);loadTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false);}}...return categories;}static void loadTilesForAction(Context context,UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,String defaultCategory, List<Tile> outTiles, boolean requireSettings) {final Intent intent = new Intent(action);if (requireSettings) {// 只允许settings通过SETTINGS_ACTION添加intent.setPackage(SETTING_PKG);}loadActivityTiles(context, user, addedCache, defaultCategory, outTiles, intent);loadProviderTiles(context, user, addedCache, defaultCategory, outTiles, intent);}private static void loadActivityTiles(Context context,UserHandle user, Map<Pair<String, String>, Tile> addedCache,String defaultCategory, List<Tile> outTiles, Intent intent) {final PackageManager pm = context.getPackageManager();final List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,PackageManager.GET_META_DATA, user.getIdentifier());for (ResolveInfo resolved : results) {//系统应用if (!resolved.system) {// Do not allow any app to add to settings, only system ones.continue;}final ActivityInfo activityInfo = resolved.activityInfo;final Bundle metaData = activityInfo.metaData;loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData, activityInfo);}}private static void loadTile(UserHandle user, Map<Pair<String, String>, Tile> addedCache,String defaultCategory, List<Tile> outTiles, Intent intent, Bundle metaData,ComponentInfo componentInfo) {// Skip loading tile if the component is tagged primary_profile_only but not running on// the current user.if (user.getIdentifier() != ActivityManager.getCurrentUser()&& Tile.isPrimaryProfileOnly(componentInfo.metaData)) {Log.w(LOG_TAG, "Found " + componentInfo.name + " for intent "+ intent + " is primary profile only, skip loading tile for uid "+ user.getIdentifier());return;}String categoryKey = defaultCategory;// Load categorycategoryKey = metaData.getString(EXTRA_CATEGORY_KEY);final boolean isProvider = componentInfo instanceof ProviderInfo;final Pair<String, String> key = isProvider? new Pair<>(((ProviderInfo) componentInfo).authority,metaData.getString(META_DATA_PREFERENCE_KEYHINT)): new Pair<>(componentInfo.packageName, componentInfo.name);Tile tile = addedCache.get(key);if (tile == null) {tile = isProvider? new ProviderTile((ProviderInfo) componentInfo, categoryKey, metaData): new ActivityTile((ActivityInfo) componentInfo, categoryKey);addedCache.put(key, tile);} else {tile.setMetaData(metaData);}if (!tile.userHandle.contains(user)) {tile.userHandle.add(user);}if (!outTiles.contains(tile)) {outTiles.add(tile);}}
}

然后遍历tiles集合,Tile类里包含Preference的数据(Key/order/intent等等),也可以设置这些字段的key。

最后一个动态菜单就被成功添加到当前页面了。我们以System->Developer options 菜单为例,它是被动态添加到Settings里的菜单。它定义在Settings的AndroidManifest.xml中。

<activityandroid:name="Settings$DevelopmentSettingsDashboardActivity"android:label="@string/development_settings_title"android:icon="@drawable/ic_settings_development"android:exported="true"android:enabled="false"><intent-filter android:priority="1"><action android:name="android.settings.APPLICATION_DEVELOPMENT_SETTINGS" /><action android:name="com.android.settings.APPLICATION_DEVELOPMENT_SETTINGS" /><action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/><category android:name="android.intent.category.DEFAULT" /></intent-filter><intent-filter><action android:name="com.android.settings.action.SETTINGS" /></intent-filter><meta-data android:name="com.android.settings.order" android:value="-40"/><meta-data android:name="com.android.settings.category"android:value="com.android.settings.category.ia.system" /><meta-data android:name="com.android.settings.summary"android:resource="@string/summary_empty"/><meta-data android:name="com.android.settings.icon"android:resource="@drawable/ic_settings_development" /><meta-data android:name="com.android.settings.FRAGMENT_CLASS"android:value="com.android.settings.development.DevelopmentSettingsDashboardFragment" /><meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"android:value="@string/menu_key_system"/><meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"android:value="true" /></activity>

首先它设置action为com.android.settings.action.SETTING,从前面TileUtils类分析知道这个action只能Settings里添加时设置。然后设置菜单顺序,菜单category为com.android.settings.category.ia.system,查阅DashboardFragmentRegistry类中PARENT_TO_CATEGORY_KEY_MAP的对应关系,可知对应页面fragment为SystemDashboardFragment,即System页面,接着指定了Summary,icon,fragment等等。这样开发者选项菜单就被插入到了System页面下。

Settings中还存在其它动态插入的选项,例如Google GMS插入的首页菜单Google和Digital Wellbeing & parental controlls。

因为很多应用需要在Settings中增加菜单,作为应用的入口,这种不需要修改Settings代码,而直接修改应用的AndroidManifest.xml文件,实现解耦并自动适配。当然只有系统应用可以动态在Settings插入菜单。

SettingsLib

在分析Settings页面加载分析的时候,发现有部分类来自SettingsLib模块,这个模块是干嘛的呢?

SettingsLib是Android系统中一个专注于为Settings应用提供服务的库。它包含了许多Settings基础功能,并封装了一些操作。

源码路径:frameworks/base/packages/SettingsLib

从bp文件可知,编译后会生成一个SettingsLib的jar包。SettingsLib下根据不同功能,UI基础实现有许多模块,SettingsLib引用这些模块。

SettingsLib模块只有具有系统级别权限如系统应用,framework等才可以调用,第三方应用无法使用。

此时在想,为什么不直接在Settings中直接实现呢?因为将不同功能,UI等的基础实现放在一个公共模块中,可以方便其它与Settings交互的模块或framework使用,进行定制使用,因此,SettingsLib虽专注于为Settings,但它服务于系统,供系统进行Settings相关扩展使用,例如SystemUI模块就在使用SettingsLib相关实现。

相关资料

官方文档:Android“设置”菜单

总结

AndroidSettings具有以下优势:

  • 界面。引入Preference显示菜单设置项。统一的页面风格,页面简单,标题状态清晰。

  • 扩展性。Android Settings页面采用单个Activity(SubSettings),多Fragment,支持其它系统应用在Settings添加菜单,可扩展性强。

    Preference和PreferenceController的配合使用,方便定制新的设置项和页面,厂商定制性高。

  • 使用。添加了搜索,让用户可以轻松快速找到设置项。界面也决定了用户可以轻松修改各种设置项。

相关文章:

Android Settings解析

Android Settings 系列文章&#xff1a; Android Settings解析SettingsIntelligenceSettingsProvider 首语 Android设置应用是Android系统中一个非常重要的系统应用&#xff0c;它允许用户调整和设置系统的各种参数和功能&#xff08;系统设置/自定义设置/控制应用权限/开发…...

Spring+spring mvc+mybatis整合的框架

Spring是一个轻量级的企业级应用开发框架&#xff0c;于2004年由Rod Johnson发布了1.0版本&#xff0c;经过多年的更新迭代&#xff0c;已经逐渐成为Java开源世界的第一框架&#xff0c;Spring框架号称Java EE应用的一站式解决方案&#xff0c;与各个优秀的MVC框架如SpringMVC、…...

02-2、PyCharm中文乱码的三处解决方法

PyCharm中文乱码 修改处1&#xff1a; 修改处2&#xff1a;这个也没用 在Pycharm中可以创建一个模版&#xff0c;每次新建python文件时Pycharm会默认在前两行生成utf-8 #!/user/bin/env python3 # -- coding: utf-8 -- 还是乱码 再在这里设置以下 添加 &#xff1a; -Dfi…...

Axi接口的DDR3:参数,时序,握手机制

参考 AXI总线的Burst Type以及地址计算 | WRAP到底是怎么一回事&#xff1f;_axi wrap-CSDN博客 还有官方手册&#xff0c;名字太长想起来再写。 Transaction/Burst/Transfer/Beat Transaction指一次传输事务&#xff0c;实际上包括了address phase, data phase与response ph…...

浏览器标签上添加icon图标;html引用ico文件

实例 <link rel"shortcut icon" href"./XXX.ico" type"image/x-icon">页面和图标在同一目录内 则 <link rel"shortcut icon" type"text/css" href"study.ico"/>可以阿里矢量图库关键字搜索下载自己…...

深入解析i++和++i的区别及性能影响

在我们编写代码时&#xff0c;经常需要对变量进行自增操作。这种情况下&#xff0c;我们通常会用到两种常见的操作符&#xff1a;i和i。最近在阅读博客时&#xff0c;我偶然看到了有关i和i性能的讨论。之前我一直在使用它们&#xff0c;但从未从性能的角度考虑过&#xff0c;这…...

2023年中国酒类新零售行业发展概况分析:线上线下渠道趋向深度融合[图]

近年来&#xff0c;我国新零售业态不断发展&#xff0c;线上便捷性和个性化推荐的优势逐步在放大&#xff0c;线下渠道智慧化水平持续提升&#xff0c;线上线下渠道趋向深度融合。2022年&#xff0c;我国酒类新零售市场规模约为1516亿元&#xff0c;预计2025年酒类新零售市场规…...

交通 | 实现可泛化性:机器学习求解VRP

​ 推文作者&#xff1a;缪昌昊&#xff0c;张景琪&#xff0c;张云天 论文作者&#xff1a;Jieyi Bi, Yining Ma, Jiahai Wang, Zhiguang Cao, Jinbiao Chen, Yuan Sun, and Yeow Meng Chee​ 论文原文&#xff1a;Bi, Jieyi, et al. “Learning generalizable models for veh…...

php使用sqlServer

sqlServer扩展 PDO_MSSQL|sqlsrv|odbc}mssql|pdo_odbc PHP 安装php_sqlsrv php_pdo_sqlsrv https://pecl.php.net/package/sqlsrv/5.8.1/windows PECL :: Package :: pdo_sqlsrv 5.8.1 for Windows SqlServer驱动&#xff1a;msodbcsql...

H3C SecParh堡垒机 get_detail_view.php 任意用户登录漏洞

与齐治堡垒机出现的漏洞不能说毫不相关&#xff0c;只能说一模一样 POC验证的url为&#xff1a; /audit/gui_detail_view.php?token1&id%5C&uid%2Cchr(97))%20or%201:%20print%20chr(121)%2bchr(101)%2bchr(115)%0d%0a%23&loginadmin成功获取admin权限 文笔生疏…...

python爬虫涨姿势板块

Python有许多用于网络爬虫和数据采集的库和框架。这些库和框架使爬取网页内容、抓取数据、进行数据清洗和分析等任务变得更加容易。以下是一些常见的Python爬虫库和框架&#xff1a; Beautiful Soup: Beautiful Soup是一个HTML和XML解析库&#xff0c;用于从网页中提取数据。它…...

软件设计原则-里氏替换原则讲解以及代码示例

里氏替换原则 一&#xff0c;介绍 1.前言 里氏替换原则&#xff08;Liskov Substitution Principle&#xff0c;LSP&#xff09;是面向对象设计中的一条重要原则&#xff0c;它由Barbara Liskov在1987年提出。 里氏替换原则的核心思想是&#xff1a;父类的对象可以被子类的…...

Sui提供dApp Kit 助力快速构建React Apps和dApps

近日&#xff0c;Mysten Labs推出了dApp Kit&#xff0c;这是一个全新的解决方案&#xff0c;可用于在Sui上开发React应用程序和去中心化应用程序&#xff08;dApps&#xff09;。mysten/dapp-kit是专门为React定制的全新SDK&#xff0c;旨在简化诸如连接钱包、签署交易和从RPC…...

2023年系统设计面试如何破解?进入 FAANG 面试的实战指南

如果您正在准备编码面试&#xff0c;但想知道如何准备关键的系统设计主题&#xff0c;并寻找正确方法、技巧和问题的分步指导&#xff0c;那么您来对地方了。在本文中&#xff0c;我将分享 2023 年系统设计面试的完整指南。 在软件开发领域&#xff0c;如果您正在申请高级工程…...

(react+ts)vite项目中的路径别名的配置

简单两个步骤 找到vite.config.ts,这里会现实报错&#xff0c;需要安装一下 npm i -D types/node 这个库的ts声明配置 import path from path // https://vitejs.dev/config/ export default defineConfig({plugins: [react()],resolve:{alias:{"":path.resolve(__…...

【MATLAB源码-第51期】基于matlab的粒子群算法(PSO)的栅格地图路径规划。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 粒子群算法&#xff08;Particle Swarm Optimization&#xff0c;简称PSO&#xff09;是一种模拟鸟群觅食行为的启发式优化方法。以下是其详细描述&#xff1a; 基本思想&#xff1a; 鸟群在寻找食物时&#xff0c;每只鸟都会…...

React之render

一、原理 首先&#xff0c;render函数在react中有两种形式&#xff1a; 在类组件中&#xff0c;指的是render方法&#xff1a; class Foo extends React.Component {render() {return <h1> Foo </h1>;} }在函数组件中&#xff0c;指的是函数组件本身&#xff1a…...

基于springboot实现财务管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现财务管理系统演示 摘要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#x…...

设计模式:组合模式(C#、JAVA、JavaScript、C++、Python、Go、PHP)

上一篇《模板模式》 下一篇《代理模式》 简介&#xff1a; 组合模式&#xff0c;它是一种用于处理树形结构、表示“部分-整体”层次结构的设计模式。它允许你将对象组合成树形结构&#xff0c;以表示部分…...

超强满血不收费的AI绘图教程来了(在线Stable Diffusion一键即用)

超强满血不收费的AI绘图教程来了&#xff08;在线Stable Diffusion一键即用&#xff09; 一、简介1.1 AI绘图1.2 Stable Diffusion1.2.1 原理简述1.2.2 应用流程 二、AI绘图工具2.1 吐司TusiArt2.2 哩布哩布LibLibAI2.3 原生部署 三、一键即用3.1 开箱尝鲜3.2 模型关联3.3 Cont…...

Vue记事本应用实现教程

文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展&#xff1a;显示创建时间8. 功能扩展&#xff1a;记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

(十)学生端搭建

本次旨在将之前的已完成的部分功能进行拼装到学生端&#xff0c;同时完善学生端的构建。本次工作主要包括&#xff1a; 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...

React hook之useRef

React useRef 详解 useRef 是 React 提供的一个 Hook&#xff0c;用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途&#xff0c;下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...

iPhone密码忘记了办?iPhoneUnlocker,iPhone解锁工具Aiseesoft iPhone Unlocker 高级注册版​分享

平时用 iPhone 的时候&#xff0c;难免会碰到解锁的麻烦事。比如密码忘了、人脸识别 / 指纹识别突然不灵&#xff0c;或者买了二手 iPhone 却被原来的 iCloud 账号锁住&#xff0c;这时候就需要靠谱的解锁工具来帮忙了。Aiseesoft iPhone Unlocker 就是专门解决这些问题的软件&…...

系统设计 --- MongoDB亿级数据查询优化策略

系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log&#xff0c;共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题&#xff0c;不能使用ELK只能使用…...

(二)原型模式

原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...

从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(九)

设备树移植 和uboot设备树修改的内容同步到kernel将设备树stm32mp157d-stm32mp157daa1-mx.dts复制到内核源码目录下 源码修改及编译 修改arch/arm/boot/dts/st/Makefile&#xff0c;新增设备树编译 stm32mp157f-ev1-m4-examples.dtb \stm32mp157d-stm32mp157daa1-mx.dtb修改…...

ardupilot 开发环境eclipse 中import 缺少C++

目录 文章目录 目录摘要1.修复过程摘要 本节主要解决ardupilot 开发环境eclipse 中import 缺少C++,无法导入ardupilot代码,会引起查看不方便的问题。如下图所示 1.修复过程 0.安装ubuntu 软件中自带的eclipse 1.打开eclipse—Help—install new software 2.在 Work with中…...

2025年渗透测试面试题总结-腾讯[实习]科恩实验室-安全工程师(题目+回答)

安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 腾讯[实习]科恩实验室-安全工程师 一、网络与协议 1. TCP三次握手 2. SYN扫描原理 3. HTTPS证书机制 二…...

32单片机——基本定时器

STM32F103有众多的定时器&#xff0c;其中包括2个基本定时器&#xff08;TIM6和TIM7&#xff09;、4个通用定时器&#xff08;TIM2~TIM5&#xff09;、2个高级控制定时器&#xff08;TIM1和TIM8&#xff09;&#xff0c;这些定时器彼此完全独立&#xff0c;不共享任何资源 1、定…...