SettingsProvider

Android Settings 系列文章:

  • Android Settings解析
  • SettingsIntelligence
  • SettingsProvider

首语

为啥要聊到这个模块呢?因为Settings里存在大量的设置项,这些设置项的状态需要保存,它们就是通过SettingsProvider来处理的。以状态栏显示电量百分比菜单为例(Battery->Battery percentage),分析下它的状态保存。
本文以Android 13 SettingsProvider源码进行分析。

Settings模块调用

这个菜单的核心实现在BatteryPercentagePreferenceController.java中,可以发现菜单的状态保存实现在Settings类中,状态读取通过getInt方法,状态保存通过putInt方法,

public class BatteryPercentagePreferenceController extends BasePreferenceController implementsPreferenceControllerMixin, Preference.OnPreferenceChangeListener {@Overridepublic void updateState(Preference preference) {//菜单状态保存读取int setting = Settings.System.getInt(mContext.getContentResolver(),SHOW_BATTERY_PERCENT, 0);((SwitchPreference) preference).setChecked(setting == 1);}@Overridepublic boolean onPreferenceChange(Preference preference, Object newValue) {boolean showPercentage = (Boolean) newValue;//菜单状态保存Settings.System.putInt(mContext.getContentResolver(), SHOW_BATTERY_PERCENT,showPercentage ? 1 : 0);FeatureFactory.getFactory(mContext).getMetricsFeatureProvider().action(mContext, SettingsEnums.OPEN_BATTERY_PERCENTAGE, showPercentage);return true;}
}

Settings类分析

在Settings中可以看到,getInt最终实现是通过ContentProvider的query方法去查询数据,putInt方法同理。mProviderHolder通过NameValueCache构造函数传入,uri为"content://settings/system"。mCallGetCommand为CALL_METHOD_GET_GLOBAL,调用ContentProvider的call方法。mContentProvider是authority为settings的ContentProvider。这里其实就知道为啥跟SettingsProvider相关联了。

因为在SettingsProvider中,定义了一个SettingsProvider,authority为settings。

继续分析下Settings类,可以发现它只能保存int,float,string等基本类型的数据,同时以键值对的形式保存,Settings中定义了大量的设置项KEY。其次除了System类外还有Global,Secure,Config,Bookmarks类分别构造了不同URI操作数据。因为Settings对数据进行了分类。

  • System。包含各种系统设置。
  • Global。包含各种对用户公开的系统设置,第三方应用程序可以读取,不可以写入。
  • Secure。包含各种安全系统设置。第三方应用程序可以读取,不可以写入。
  • Config。配置系统设置。只有Android可以读取,特定的配置服务可以写入。
  • Bookmarks。用户定义的书签和快捷方式。 每个书签的目标是一个 Intent URL,允许它是网页或特定的应用程序活动。

修改数据需要权限:

  • android.permission.WRITE_SETTINGS
  • android.permission.WRITE_SECURE_SETTINGS
public final class Settings {public static final class System extends NameValueTable {public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/system");public static int getInt(ContentResolver cr, String name, int def) {return getIntForUser(cr, name, def, cr.getUserId());}/** @hide */@UnsupportedAppUsagepublic static int getIntForUser(ContentResolver cr, String name, int def, int userHandle) {String v = getStringForUser(cr, name, userHandle);return parseIntSettingWithDefault(v, def);}  public static String getStringForUser(ContentResolver resolver, String name,int userHandle) {return sNameValueCache.getStringForUser(resolver, name, userHandle);}}public static final class Global extends NameValueTable {public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/global");  private static final NameValueCache sNameValueCache = new NameValueCache(CONTENT_URI,CALL_METHOD_GET_GLOBAL,CALL_METHOD_PUT_GLOBAL,CALL_METHOD_DELETE_GLOBAL,sProviderHolder,Global.class);}public static final class Secure extends NameValueTable {public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/secure");                } public static final class Config extends NameValueTable {public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/config");                       }    private static class NameValueCache {<T extends NameValueTable> NameValueCache(Uri uri, String getCommand,String setCommand, String deleteCommand, ContentProviderHolder providerHolder,Class<T> callerClass) {this(uri, getCommand, setCommand, deleteCommand, null, null, providerHolder,callerClass);}public String getStringForUser(ContentResolver cr, String name, final int userHandle) {IContentProvider cp = mProviderHolder.getProvider(cr);...if (mCallGetCommand != null) {b = cp.call(cr.getAttributionSource(),mProviderHolder.mUri.getAuthority(), mCallGetCommand, name,args);String value = b.getString(Settings.NameValueTable.VALUE);return value;}if (Settings.isInSystemServer() && Binder.getCallingUid() != Process.myUid()) {final long token = Binder.clearCallingIdentity();try {c = cp.query(cr.getAttributionSource(), mUri,SELECT_VALUE_PROJECTION, queryArgs, null);} finally {Binder.restoreCallingIdentity(token);}} else {c = cp.query(cr.getAttributionSource(), mUri,SELECT_VALUE_PROJECTION, queryArgs, null);}...}private static final class ContentProviderHolder {private final Object mLock = new Object();private final Uri mUri;private IContentProvider mContentProvider;public ContentProviderHolder(Uri uri) {mUri = uri;}public IContentProvider getProvider(ContentResolver contentResolver) {synchronized (mLock) {if (mContentProvider == null) {mContentProvider = contentResolver.acquireProvider(mUri.getAuthority());}return mContentProvider;}}
}

SettingsProvider模块分析

SettingsProvider模块源码为frameworks/base/packages/SettingsProvider/,模块名为SettingsProvider,包名为com.android.providers.settings,Manifest中定义了authority为settings的ContentProvider。

<provider android:name="SettingsProvider"android:authorities="settings"android:multiprocess="false"android:exported="true"android:singleUser="true"android:initOrder="100"android:visibleToInstantApps="true" />

查看下SettingsProvider的实现,首先在onCreate方法中有迁移处理,用户相关监听,添加了两个服务SettingsService,DeviceConfigService。

public class SettingsProvider extends ContentProvider {@Overridepublic boolean onCreate() {...synchronized (mLock) {//迁移处理mSettingsRegistry.migrateAllLegacySettingsIfNeededLocked();mSettingsRegistry.syncSsaidTableOnStartLocked();}mHandler.post(() -> {//用户移除停止广播注册registerBroadcastReceivers();//用户限制变更监听startWatchingUserRestrictionChanges();});ServiceManager.addService("settings", new SettingsService(this));ServiceManager.addService("device_config", new DeviceConfigService(this));}
}

SettingsService类重写onShellCommand方法来处理adb shell 命令。

final public class SettingsService extends Binder {@Overridepublic void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,String[] args, ShellCallback callback, ResultReceiver resultReceiver) {(new MyShellCommand(mProvider, false)).exec(this, in, out, err, args, callback, resultReceiver);}
}

执行 adb shell settings,打印了以下command使用信息。可以使用这些命令快速进行数据操作。

Settings provider (settings) commands:helpPrint this help text.get [--user <USER_ID> | current] NAMESPACE KEYRetrieve the current value of KEY.put [--user <USER_ID> | current] NAMESPACE KEY VALUE [TAG] [default]Change the contents of KEY to VALUE.TAG to associate with the setting.{default} to set as the default, case-insensitive only for global/secure namespacedelete [--user <USER_ID> | current] NAMESPACE KEYDelete the entry for KEY.reset [--user <USER_ID> | current] NAMESPACE {PACKAGE_NAME | RESET_MODE}Reset the global/secure table for a package with mode.RESET_MODE is one of {untrusted_defaults, untrusted_clear, trusted_defaults}, case-insensitivelist [--user <USER_ID> | current] NAMESPACEPrint all defined keys.NAMESPACE is one of {system, secure, global}, case-insensitive

SettingsService进行了adb shell命令的扩展,让我们操作数据更加方便。DeviceConfigService同理,通过adb shell device_config查看command信息。

分析了SettingsProvider的onCreate方法后,再看下insert方法是如何插入数据的,它从uri取出table,对应不同uri为system/global/secure等。以插入global数据为例分析,System,Global等实现类似。operation来判断是增删改查那种操作,通过SettingsState类的insertSettingLocked方法来进行插入操作,而SettingsState是通过ensureSettingsStateLocked方法创建的,然后保存到mSettingsStates中。

public class SettingsProvider extends ContentProvider {public static final String TABLE_SYSTEM = "system";@Overridepublic Uri insert(Uri uri, ContentValues values) {...String table = getValidTableOrThrow(uri);switch (table) {case TABLE_GLOBAL: {if (insertGlobalSetting(name, value, null, false,UserHandle.getCallingUserId(), false, /* overrideableByRestore */ false)) {return Uri.withAppendedPath(Settings.Global.CONTENT_URI, name);}} break;}}private boolean insertGlobalSetting(String name, String value, String tag,boolean makeDefault, int requestingUserId, boolean forceNotify,boolean overrideableByRestore) {return mutateGlobalSetting(name, value, tag, makeDefault, requestingUserId,MUTATION_OPERATION_INSERT, forceNotify, 0, overrideableByRestore);}private boolean mutateGlobalSetting(String name, String value, String tag,boolean makeDefault, int requestingUserId, int operation, boolean forceNotify,int mode, boolean overrideableByRestore) {switch (operation) {//插入操作case MUTATION_OPERATION_INSERT: {return mSettingsRegistry.insertSettingLocked(SETTINGS_TYPE_GLOBAL,UserHandle.USER_SYSTEM, name, value, tag, makeDefault,callingPackage, forceNotify,CRITICAL_GLOBAL_SETTINGS, overrideableByRestore);}}}final class SettingsRegistry {private static final String SETTINGS_FILE_GLOBAL = "settings_global.xml";public boolean insertSettingLocked(int type, int userId, String name, String value,String tag, boolean makeDefault, String packageName, boolean forceNotify,Set<String> criticalSettings, boolean overrideableByRestore) {return insertSettingLocked(type, userId, name, value, tag, makeDefault, false,packageName, forceNotify, criticalSettings, overrideableByRestore);}public boolean insertSettingLocked(int type, int userId, String name, String value,String tag, boolean makeDefault, boolean forceNonSystemPackage, String packageName,boolean forceNotify, Set<String> criticalSettings, boolean overrideableByRestore) {...SettingsState settingsState = peekSettingsStateLocked(key);if (settingsState != null) {success = settingsState.insertSettingLocked(name, value,tag, makeDefault, forceNonSystemPackage, packageName, overrideableByRestore);}}@Nullableprivate SettingsState peekSettingsStateLocked(int key) {...if (!ensureSettingsForUserLocked(getUserIdFromKey(key))) {return null;}return mSettingsStates.get(key);}public boolean ensureSettingsForUserLocked(int userId) {...if (userId == UserHandle.USER_SYSTEM) {final int globalKey = makeKey(SETTINGS_TYPE_GLOBAL, UserHandle.USER_SYSTEM);ensureSettingsStateLocked(globalKey);}}private void ensureSettingsStateLocked(int key) {...if (mSettingsStates.get(key) == null) {final int maxBytesPerPackage = getMaxBytesPerPackageForType(getTypeFromKey(key));SettingsState settingsState = new SettingsState(getContext(), mLock,getSettingsFile(key), key, maxBytesPerPackage, mHandlerThread.getLooper());mSettingsStates.put(key, settingsState);}}private File getSettingsFile(int key) {if (isGlobalSettingsKey(key)) {final int userId = getUserIdFromKey(key);return new File(Environment.getUserSystemDirectory(userId),SETTINGS_FILE_GLOBAL);} else if (isSystemSettingsKey(key)) {final int userId = getUserIdFromKey(key);return new File(Environment.getUserSystemDirectory(userId),SETTINGS_FILE_SYSTEM);...}}}
}

继续分析SettingsState类的insertSettingLocked方法,先将数据保存到mSettings,创建了一个Handler延时加锁进行写数据操作,核心写数据操作在doWriteState方法里。mStatePersistFile是从SettingsState传递过来的,由创建SettingsState的ensureSettingsStateLocked方法可知,通过getSettingsFile创建mStatePersistFile,文件路径为用户系统目录(/data/system/users/0/),文件名为settings_global.xml,然后在xml中进行写数据。

final class SettingsState {public boolean insertSettingLocked(String name, String value, String tag,boolean makeDefault, boolean forceNonSystemPackage, String packageName,boolean overrideableByRestore) {...mSettings.put(name, newState);scheduleWriteIfNeededLocked();}private void scheduleWriteIfNeededLocked() {...writeStateAsyncLocked();}private void writeStateAsyncLocked() {...Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS);mHandler.sendMessageDelayed(message, writeDelayMillis);}private final class MyHandler extends Handler {@Overridepublic void handleMessage(Message message) {switch (message.what) {case MSG_PERSIST_SETTINGS: {Runnable callback = (Runnable) message.obj;doWriteState();if (callback != null) {callback.run();}}private void doWriteState() {synchronized (mLock) {version = mVersion;settings = new ArrayMap<>(mSettings);namespaceBannedHashes = new ArrayMap<>(mNamespaceBannedHashes);mDirty = false;mWriteScheduled = false;}synchronized (mWriteLock){AtomicFile destination = new AtomicFile(mStatePersistFile, mStatePersistTag);FileOutputStream out = null;try {out = destination.startWrite();TypedXmlSerializer serializer = Xml.resolveSerializer(out);serializer.startDocument(null, true);serializer.startTag(null, TAG_SETTINGS);serializer.attributeInt(null, ATTR_VERSION, version);final int settingCount = settings.size();for (int i = 0; i < settingCount; i++) {Setting setting = settings.valueAt(i);if (setting.isTransient()) {if (DEBUG_PERSISTENCE) {Slog.i(LOG_TAG, "[SKIPPED PERSISTING]" + setting.getName());}continue;}if (writeSingleSetting(mVersion, serializer, setting.getId(), setting.getName(),setting.getValue(), setting.getDefaultValue(), setting.getPackageName(),setting.getTag(), setting.isDefaultFromSystem(),setting.isValuePreservedInRestore())) {}}}serializer.endTag(null, TAG_SETTINGS);serializer.startTag(null, TAG_NAMESPACE_HASHES);for (int i = 0; i < namespaceBannedHashes.size(); i++) {String namespace = namespaceBannedHashes.keyAt(i);String bannedHash = namespaceBannedHashes.get(namespace);if (writeSingleNamespaceHash(serializer, namespace, bannedHash)) {}}}serializer.endTag(null, TAG_NAMESPACE_HASHES);serializer.endDocument();destination.finishWrite(out);}}

到这里,才知道数据是保存在xml文件中的,而并非数据库里。Global类型数据保存在settings_global.xml中,System类型数据保存在settings_system.xml中,Secure类型数据保存在settings_secure.xml中,都在用户系统目录(/data/system/users/0/)下保存,截取部分内容如下:

<settings version="213">
<setting id="127" name="adb_wifi_enabled" value="0" package="android" defaultValue="0" defaultSysSet="true" />
<setting id="44" name="low_battery_sound_timeout" value="0" package="android" defaultValue="0" defaultSysSet="true" />
<setting id="95" name="wear_os_version_string" value="" package="android" defaultValue="" defaultSysSet="true" />...
</settings>    

查看时可能乱码,这是因为Android13保存的xml文件使用的是一种二进制格式,通过以下命令修改:

adb shell setprop persist.sys.binary_xml false

xml配置文件的格式就变为ASCII 码格式文件,就不会乱码可以正常查看了。

对于其它的query,update,delete方法,也不需赘述了,都是对mSettings进行操作,根据mSettings变化重新写入xml。核心实现都在SettingsState类中,通过锁来确保多个修改以原子方式持久保存到内存和磁盘中。

再看下call方法,前面Settings类中getStringForUser方法就调用了call方法去获取数据。method是区分各种类型数据操作的,不同类型数据操作有不同的method定义,之后的数据操作流程就和增删改查方法中的一致。

public class SettingsProvider extends ContentProvider {@Overridepublic Bundle call(String method, String name, Bundle args) {case Settings.CALL_METHOD_GET_GLOBAL: {Setting setting = getGlobalSetting(name);return packageValueForCallResult(setting, isTrackingGeneration(args));}case Settings.CALL_METHOD_PUT_GLOBAL: {String value = getSettingValue(args);String tag = getSettingTag(args);final boolean makeDefault = getSettingMakeDefault(args);final boolean overrideableByRestore = getSettingOverrideableByRestore(args);insertGlobalSetting(name, value, tag, makeDefault, requestingUserId, false,overrideableByRestore);break;}}
}

对SettingsProvider的基本方法分析以后,我们分析下数据迁移方法migrateLegacySettingsForUserIfNeededLocked,它在onCreate方法中调用。通过DatabaseHelper类获取数据库实例来操作数据库,在TABLE_GLOBAL表内查询name 、value列,然后通过SettingsState的insertSettingLocked方法将数据插入到xml,插入完成后删除数据库。

public class SettingsProvider extends ContentProvider {private static final boolean DROP_DATABASE_ON_MIGRATION = true;public static final String TABLE_GLOBAL = "global";private void migrateLegacySettingsForUserIfNeededLocked(int userId) {// Every user has secure settings and if no file we need to migrate.final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId);File secureFile = getSettingsFile(secureKey);if (SettingsState.stateFileExists(secureFile)) {return;}DatabaseHelper dbHelper = new DatabaseHelper(getContext(), userId);SQLiteDatabase database = dbHelper.getWritableDatabase();migrateLegacySettingsForUserLocked(dbHelper, database, userId);}private void migrateLegacySettingsForUserLocked(DatabaseHelper dbHelper,SQLiteDatabase database, int userId) {...if (userId == UserHandle.USER_SYSTEM) {final int globalKey = makeKey(SETTINGS_TYPE_GLOBAL, userId);ensureSettingsStateLocked(globalKey);SettingsState globalSettings = mSettingsStates.get(globalKey);migrateLegacySettingsLocked(globalSettings, database, TABLE_GLOBAL);// If this was just createdif (mSettingsCreationBuildId != null) {globalSettings.insertSettingLocked(Settings.Global.DATABASE_CREATION_BUILDID,mSettingsCreationBuildId, null, true,SettingsState.SYSTEM_PACKAGE_NAME);}globalSettings.persistSyncLocked();}// 已经迁移,删除数据库if (DROP_DATABASE_ON_MIGRATION) {dbHelper.dropDatabase();} else {dbHelper.backupDatabase();}private void migrateLegacySettingsLocked(SettingsState settingsState, SQLiteDatabase database, String table) {SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();queryBuilder.setTables(table);Cursor cursor = queryBuilder.query(database, LEGACY_SQL_COLUMNS,null, null, null, null, null);try {if (!cursor.moveToFirst()) {return;}final int nameColumnIdx = cursor.getColumnIndex(Settings.NameValueTable.NAME);final int valueColumnIdx = cursor.getColumnIndex(Settings.NameValueTable.VALUE);settingsState.setVersionLocked(database.getVersion());while (!cursor.isAfterLast()) {String name = cursor.getString(nameColumnIdx);String value = cursor.getString(valueColumnIdx);//插入数据到xmlsettingsState.insertSettingLocked(name, value, null, true,SettingsState.SYSTEM_PACKAGE_NAME);cursor.moveToNext();}} finally {cursor.close();}}}
}

那看下DatabaseHelper实现,数据库名为settings.db,onCreate方法中创建了多张表,还是以Global为例,其它同理。在global表插入数据,KEY一般都是在Settings中定义,VALUE则一般都是本地资源。给这些KEY对应的设置项添加了初始值。可以在res/values/defaults.xml文件中看到定义了大量菜单的初始值。

class DatabaseHelper extends SQLiteOpenHelper {private static final String DATABASE_NAME = "settings.db";@Overridepublic void onCreate(SQLiteDatabase db) {//创建表db.execSQL("CREATE TABLE system (" +"_id INTEGER PRIMARY KEY AUTOINCREMENT," +"name TEXT UNIQUE ON CONFLICT REPLACE," +"value TEXT" +");");db.execSQL("CREATE INDEX systemIndex1 ON system (name);");createSecureTable(db);...//加载数据// Load initial volume levels into DBloadVolumeLevels(db);// Load inital settings valuesloadSettings(db);}private void loadSettings(SQLiteDatabase db) {loadSystemSettings(db);loadSecureSettings(db);// The global table only exists for the 'owner/system' userif (mUserHandle == UserHandle.USER_SYSTEM) {loadGlobalSettings(db);}}private void loadGlobalSettings(SQLiteDatabase db) {SQLiteStatement stmt = null;final Resources res = mContext.getResources();try {//插入sqlstmt = db.compileStatement("INSERT OR IGNORE INTO global(name,value)"+ " VALUES(?,?);");loadBooleanSetting(stmt, Settings.Global.AIRPLANE_MODE_ON,R.bool.def_airplane_mode_on);loadStringSetting(stmt, Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS,R.string.airplane_mode_toggleable_radios);loadIntegerSetting(stmt, Settings.Global.WIFI_SLEEP_POLICY,R.integer.def_wifi_sleep_policy);...}private void loadBooleanSetting(SQLiteStatement stmt, String key, int resid) {loadSetting(stmt, key,mContext.getResources().getBoolean(resid) ? "1" : "0");} private void loadSetting(SQLiteStatement stmt, String key, Object value) {stmt.bindString(1, key);stmt.bindString(2, value.toString());//执行sqlstmt.execute();}
}

其它源码就是关于升级和备份相关的,这里就不展开分析了。

整理下SettingsProvider的流程,Settings.db初始化往表里添加大量数据,然后从Settings.db将数据迁移到到不同类型(Global/System/Secure)数据的xml中,最后删除数据库。

总结

SettingsProvider 模块使用 ContentProvider 的方式来管理和访问设置数据。它提供了一组标准的 URI用于访问不同类型的设置信息。通过使用这些 URI,应用程序可以读取、写入和监听设置的变化。

通过与 SettingsProvider 模块交互,Settings等应用程序和系统组件可以轻松地管理设备的各种设置,为用户提供更好的个性化和控制体验。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/116235.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

.net6部署到linux上(CentOS Linux 7)

目录 一、先在linux上配置.net环境 添加 Microsoft 包存储库 安装 SDK 安装运行时 检查 SDK 版本可使用终端查看当前安装的 .NET SDK 版本。 打开终端并运行以下命令。 二、创建.net6 mvc项目 并发布 创建项目 修改默认端口 打包发布到文件夹 运行打包项目查看项目是否…

macOS telnet替代方式

前言 经过使用Linux&#xff0c;常常用Linux的telnet查看端口畅通&#xff0c;是否有防火墙&#xff0c;但是在mac上已经没有这个命令了&#xff0c;那么怎么使用这个命令或者有没有其他替代呢&#xff0c;win和linux是否可以使用相同的替代。macOS可以原生用nc命令替代&#…

C算法:使用选择排序实现从(大到小/从小到大)排序数组,且元素交换不可使用第三变量。

需求&#xff1a; 使用选择排序实现从(大到小/从小到大)排序&#xff0c;且元素交换不可使用第三变量 (异或交换法) 代码实现&#xff1a; #include <stdio.h> void maopao(int* array,int len,int(*swap)(int a,int b)) {int i,j;for(i0;i<len-1;i){for(ji1;j<…

【数据结构与算法】two X 树的遍历以及功能实现

前言&#xff1a; 前面我们已经提到过树、二叉树的概念及结构、堆排序、Top-k问题等的知识点&#xff0c;这篇文章我们来详解一下二叉树的链式结构等问题。 &#x1f4a5;&#x1f388;个人主页:​​​​​​Dream_Chaser&#xff5e; &#x1f388;&#x1f4a5; ✨✨专栏:htt…

安全渗透测试网络基础知识之路由技术

#1.静态路由技术 ##1.1路由技术种类: 静态路由技术、动态路由技术 ##1.2静态路由原理 静态路由是网络中一种手动配置的路由方式,用于指定数据包在网络中的传输路径。与动态路由协议不同,静态路由需要管理员手动配置路由表,指定目的网络和下一跳路由器的关联关系。 比较适合…

【智能家居】

面向Apple developer学习&#xff1a;AirPlay | Apple Developer Documentation Airplay AirPlay允许人们将媒体内容从iOS、ipad、macOS和tvOS设备无线传输到支持AirPlay的Apple TV、HomePod以及电视和扬声器上。 网页链接的最佳实践 首选系统提供的媒体播放器。内置的媒体播…

SpringCloud和Kubernetes的区别

又见小道仙&#xff1a; https://blog.csdn.net/Tomwildboar/article/details/129531315 对于SpringCloud在实际项目中并未使用过&#xff0c;只是自学过SpringCloud和SpringCloud Alibaba&#xff0c;也基于学习搭建过demo。 对于Kubernetes&#xff0c;目前这家公司就是使用…

Web APIs——事件监听以及案例

1、事件监听 什么是事件&#xff1f; 事件是在编程时系统内发生的动作或者发生的事情 比如用户在网页上单击一个按钮 什么是事件监听&#xff1f; 就是让程序检测是否有事件产生&#xff0c;一旦有事件触发&#xff0c;就立即调用一个函数做出响应&#xff0c;也称为绑定事…

【RocketMQ集群】Linux搭建RocketMQ双主双从集群

在当今大数据时代&#xff0c;消息队列系统成为了构建高可用、可扩展和可靠的分布式应用的重要组件之一。而Apache RocketMQ作为一款开源的分布式消息中间件&#xff0c;以其高吞吐量、低延迟和可靠性而备受青睐。为了满足大规模应用的需求&#xff0c;搭建RocketMQ集群是一种常…

K8S集群中Node节点资源不足导致Pod无法运行的故障排查思路

K8S集群中Node节点资源不足导致Pod无法运行的故障排查思路 Node节点资源不足可能会产生的故障 故障一&#xff1a;Pod数量太多超出物理节点的限制每一台Node节点中默认限制最多运行110个Pod资源&#xff0c;当一个应用程序有成百上千的Pod资源时&#xff0c;如果不扩容Node节…

基于springboot实现基于Java的超市进销存系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现基于Java的超市进销存系统演示 摘要 随着信息化时代的到来&#xff0c;管理系统都趋向于智能化、系统化&#xff0c;超市进销存系统也不例外&#xff0c;但目前国内仍都使用人工管理&#xff0c;市场规模越来越大&#xff0c;同时信息量也越来越庞大&#x…

Ubuntu 20.04 安装 Docker

大家好&#xff0c;我叫徐锦桐&#xff0c;个人博客地址为www.xujintong.com。平时记录一下学习计算机过程中获取的知识&#xff0c;还有日常折腾的经验&#xff0c;欢迎大家来访。 介绍 Docker容器具有以下三大特点&#xff1a; 轻量化&#xff1a;一台主机上运行的多个Dock…

Weights and Biases使用教程

Weights and Biases使用教程 安装和初始化实验跟踪跟踪指标跟踪超参数可视化模型检查日志 数据和模型版本控制使用Sweeps进行超参数调优数据可视化report Weights and Biases已经成为AI社区中最受欢迎的库之一。该团队在创建了一个平台&#xff0c;使机器深度学习学习工程师能够…

云安全—docker原理

0x00 前言 因为要学习docker相关的检测技术&#xff0c;所以需要对docker的原理进行基本的原因&#xff0c;不求彻底弄懂&#xff0c;但求懂点皮毛&#xff0c;如有不妥之处&#xff0c;还请斧正。 0x01 docker概述 docker起源 docker公司是在旧金山&#xff0c;由法裔美籍…

Java操作Elasticsearch(新增数据)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

广告掘金全自动挂机项目,单设备30+【软件脚本+技术教程】

广告掘金项目是一种越来越受欢迎的赚钱方式&#xff0c;它通过观看广告视频来获取收益。然而&#xff0c;手动观看每个广告视频可能会耗费大量时间和精力。为了简化操作并提升效率&#xff0c;我们可以利用全自动挂机脚本来完成这一任务。接下来&#xff0c;将为您介绍如何使用…

Type Script的变量类型

Typescript 的重要特性之一就是数据有类型了。 常见的类型如&#xff1a;字符串、数值、布尔等都有了明确的定义。 变量声明的格式 let 变量名:类型 初始值&#xff1b;字符型 let str:string "abc";数值型 数值型也支持不同的进制&#xff0c;用前缀区分 支持 整…

UI设计中设计文章列表左右的思考优漫动游

当我们仔细看APP的文章列表时我们会发现&#xff1a;有些采用的是左文右图;有些采用的则是左图右文。这个时候我们有没有进一步思考过&#xff0c;这两种方式有什么区别呢?各自的优缺点又是什么呢? UI设计中设计文章列表左右的思考   在对比各大APP时&#xff0c;我们会…

PLC单按钮启停算法汇总

单按钮启停在三菱PLC里可以通过简单的取反指令"ALT"实现,西门子PLC如何实现ALT指令,请参考下面文章链接,这篇博客我们汇总常用的单按钮启停实现方法,希望大家读了本篇博客后有所收获。 博途ALT指令 博途S7-1200/1500PLC 取反指令(ALT)-CSDN博客SMART PLC的ALT指…

安卓 实现60s倒计时的CountDownTimer(小坑)

安卓 实现60s倒计时的CountDownTimer&#xff08;小坑&#xff09; 前言一、CountDownTimer 是什么&#xff1f;二、代码示例1.使用2.小坑的点误差及时取消 总结 前言 前段时间写倒计时没有用线程&#xff0c;想换一种实现方式结果踩了个小坑&#xff0c;特此记录。 一、Count…