完整代码Gitee地址:kotlin-demo: 15天Kotlin学习计划

第六天学习内容代码:Chapter6

前言

简介

知识点1:文件存储

知识点2:sharedPreferences存储

知识点3:SQLite数据库存储

创建数据库

添加数据

更新数据

删除数据

读取数据

知识点4:BuildConfig分包

知识点5:实战封装高性能存储

总结


前言

任何一个应用程序,其实说白了就是在不停地和数据打交道,没有数据的应用程序就变成了一个空壳子,对用户来说没有任何实际用途。那么这些数据是从哪儿来的呢?现在多数的数据基本是由用户产生的,比如你发微博、评论新闻,其实都是在产生数据。


简介

数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机的情况下,这些数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的。持久化技术提供了一种机制,可以让数据在瞬时状态和持久状态之间进行转换。持久化技术被广泛应用于各种程序设计领域,而本节要探讨的自然是Android中的数据持久化技术。

Android系统中主要提供了3种方式用于简单地实现数据持久化功能:文件存储、SharedPreferences存储以及数据库存储。下面来看一看这三种方式,以及封装的高性能缓存工具是如何实现的。


知识点1:文件存储

文件存储是Android中最基本的数据存储方式,它不对存储的内容进行任何格式化处理,所有数据都是原封不动地保存到文件当中,下面来写一例子存储到内部路径,不需要额外申请读写权限,封装一个FileUtils工具类,实现了保存、读取文件:

class FileUtils {companion object {/*保存文件*/@JvmStaticfun saveFile(context: Context, fileName: String, cont: String) {try {//第一个参数是文件名//第二个参数是文件的操作模式(相同文件MODE_PRIVATE覆盖,MODE_APPEND追加内容)val output = context.openFileOutput(fileName, Context.MODE_PRIVATE)val writer = BufferedWriter(OutputStreamWriter(output))writer.use {it.write(cont)}} catch (e: Exception) {}}/*读取文件*/@JvmStaticfun readerFile(context: Context, fileName: String): String {val content = StringBuffer()try {val input = context.openFileInput(fileName)val reader = BufferedReader(InputStreamReader(input))reader.use {reader.forEachLine {content.append(it)}}} catch (e: Exception) {}return content.toString()}}
}

下面我们就编写一个完整的例子,并修改activity_learn6.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#e8e8e8"android:orientation="vertical"><com.example.kotlin_demo.TitleLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"/><EditTextandroid:id="@+id/edit_cont"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_margin="10dp"android:hint="输入储存内容,退出保存"android:text=""android:textColor="@color/teal_700"android:textSize="16sp" />
</LinearLayout>

在文本输入框中随意输入点什么内容,再按下Back键,这时输入的内容肯定就已经丢失了,因为它只是瞬时数据,在Activity被销毁后就会被回收。而这里我们要做的,就是在数据被回收之前,将它存储到文件当中。修改Learn6Activity中的代码,如下所示:

    private val fileName = "strData"//文件名    override fun onDestroy() {super.onDestroy()//页面关闭储存文件FileUtils.saveFile(this, fileName, editCont.text.toString())}

现在重新运行一下程序,并在EditText中输入一些内容,这时我们输入的内容就保存到文件中了。那么如何才能证实数据确实已经保存成功了呢?我们可以借助Device File Explorer工具查看一下。这个工具在Android Studio的右侧边栏当中,我们来打开看一看,路径data - data - 下。

从文件中读取数据,代码如下所示:

    //1、文件储存,onDestroy方法中调用存储,存储到应用内不需要申请权限;editCont = findViewById(R.id.edit_cont)//读取文件val fileCont = FileUtils.readerFile(this, fileName)if (fileCont.isNotEmpty()) {editCont.setText(fileCont)editCont.setSelection(fileCont.length)}

现在重新运行一下程序,刚才保存的Something important字符串肯定会被填充到EditText中,然后编写一点其他的内容,比如在EditText中输入“Hello world”,接着按下Back键退出程序,再重新启动程序,这时刚才输入的内容并不会丢失,而是还原到了EditText中,如下图所示:


知识点2:sharedPreferences存储

不同于文件的存储方式,SharedPreferences是使用键值对的方式来存储数据的,如下所示:

    //2、sharedPreferences存储butSave = findViewById(R.id.but_save)butSave.setOnClickListener {//第一个参数是文件名//第二个参数默认的MODE_PRIVATE这一种模式可选,可为0val editor = getSharedPreferences(fileName, Context.MODE_PRIVATE).edit()editor.putString("account", "PeaceJay")editor.putInt("passWord", 123456)editor.putBoolean("remember", false)editor.apply()}//读取文件butRead = findViewById(R.id.but_read)butRead.setOnClickListener {val editor = getSharedPreferences(fileName, Context.MODE_PRIVATE)val userName = editor.getString("account", "")val userAge = editor.getInt("passWord", 0)val toast = "account= $userName   passWord= $userAge"Toast.makeText(this, toast, Toast.LENGTH_SHORT).show()}

可以看到生成了一个data.xml文件,一般用来轻量存储,比如登录账号信息,效果如下所示:


知识点3:SQLite数据库存储

SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百KB的内存就足够了,特别适合在移动设备上使用。文件存储和SharedPreferences存储毕竟只适用于保存一些简单的数据和键值对,当需要存储大量复杂的关系型数据的时候,你就会发现以上两种存储方式很难应付得了。

创建数据库

Android为了让我们能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper帮助类,借助这个类可以非常简单地对数据库进行创建和升级,新建一个SqliteHelper帮助类:

//第二个参数是数据库名,创建数据库时使用的就是这里指定的名称;
//第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,一般传入null即可;
//第四个参数表示当前数据库的版本号
class SqliteHelper(val context: Context, name: String, version: Int) :SQLiteOpenHelper(context, name, null, version) {//建表语句 primary key将id列设为主键,并用autoincrement关键字表示id列是自增长//integer表示整型,real表示浮点型,text表示文本类型,blob表示二进制类型private val createBook ="create table Book(id integer primary key autoincrement,author text,price real,pages integer,name text)"override fun onCreate(db: SQLiteDatabase) {//execSQL()方法去执行这条建表语句db.execSQL(createBook)//Book表存放书的各种详细数据Toast.makeText(context, "create succeeded", Toast.LENGTH_SHORT).show()}override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}
}

可以看到,我们把建表语句定义成了一个字符串变量,然后在onCreate()方法中又调用了SQLiteDatabase的execSQL()方法去执行这条建表语句,并弹出一个Toast提示创建成功,这样就可以保证在数据库创建完成的同时还能成功创建Book表。

在Learn6Activity新增创建表方法:

    //3、SQLite数据库存储 SQLiteOpenHelper帮助类,不能降版本val dbHelper = SqliteHelper(this, "bookInfo.db", 1)butSqliteAdd = findViewById(R.id.but_Sqlite_add)butSqliteAdd.setOnClickListener {//当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用MyDatabaseHelper中的onCreate()方法,//再次点击“Create Database”按钮时,会发现此时已经存在BookStore.db数据库了,因此不会再创建一次。dbHelper.writableDatabase}

这里我们在onCreate()方法中构建了一个SqliteHelper对象,并且通过构造函数的参数将数据库名指定为bookInfo.db,版本号指定为1,点击事件里调用了getWritableDatabase()方法。这样当第一次点击按钮时,就会检测到当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用MyDatabaseHelper中的onCreate()方法,这样Book表也就创建好了,然后会弹出一个Toast提示创建成功。当你再次点击按钮时,不会再有Toast弹出。版本改为2,会进入onUpgrade更新方法。来验证一下是否创建成功:

这个目录下还存在另外一个bookKinfo.db-journal文件,这是一个为了让数据库能够支持事务而产生的临时日志文件,通常情况下这个文件的大小是0字节,我们可以暂时不用管它。

添加数据

SQLiteDatabase中提供了一个insert()方法,专门用于添加数据。它接收3个参数:第一个参数是表名,我们希望向哪张表里添加数据,这里就传入该表的名字;第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般我们用不到这个功能,直接传入null即可;第三个参数是一个ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。

    //添加数据butSqliteInsert = findViewById(R.id.but_Sqlite_insert)butSqliteInsert.setOnClickListener {val db = dbHelper.writableDatabaseval value1 = ContentValues().apply {put("author", "汉斯·克里斯汀·安徒生")put("name", "安徒生童话")put("pages", 230)put("price", 16.08)}db.insert("Book", null, value1)val value2 = ContentValues().apply {put("author", "格林")put("name", "格林童话")put("pages", 430)put("price", 26.28)}db.insert("Book", null, value2)Toast.makeText(this, "插入完成", Toast.LENGTH_SHORT).show()}

点击一下数据添加按钮,此时两条数据应该都已经添加成功了。要怎么验证数据是否添加成功呢,这时间就要用到插件了,Database Navigator,Preferences→Plugins,就可以进入插件管理界面了,安装成功后重启工具。

现在对着bookInfo.db文件右击→Save As,将它从模拟器导出到你的计算机的任意位置。然后观察Android Studio的左侧边栏何顶部菜单栏,现在应该多出了一个DB Browser工具,如图所示:

为了打开刚刚导出的数据库文件,我们需要点击这个工具左上角的加号按钮,并选择SQLite选项

然后在弹出窗口的Database配置中选择我们刚才导出的bookInfo.db文件位置,如图所示。

点击“OK”完成配置,这个时候DB Browser中就会显示出bookInfo.db数据库里所有的内容了,如图所示。

想要查询哪张表的内容,只需要双击这张表就可以了,这里我们双击Book表,直接点击窗口下方的“NoFilter”按钮即可:

更新数据

调用了SQLiteDatabase的update()方法执行具体的更新操作,可以看到,这里使用了第三、第四个参数来指定具体更新哪几行。第三个参数对应的是SQL语句的where部分,表示更新所有name等于?的行,而?是一个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容,arrayOf()方法是Kotlin提供的一种用于便捷创建数组的内置方法。

    butSqliteUpdate = findViewById(R.id.but_Sqlite_updata)butSqliteUpdate.setOnClickListener {//构建了一个ContentValues对象,并且只给它指定了一组数据val db = dbHelper.writableDatabaseval values = ContentValues()values.put("author", "古天乐")values.put("name", "是兄弟就来砍我")values.put("pages", 430)values.put("price", 33.28)//arrayOf()方法是Kotlin提供的一种用于便捷创建数组的内置方法db.update("Book", values, "name = ?", arrayOf("安徒生童话"))Toast.makeText(this, "更新完成", Toast.LENGTH_SHORT).show()}

删除数据

SQLiteDatabase中提供了一个delete()方法,专门用于删除数据。这个方法接收3个参数:第一个参数仍然是表名,这个没什么好说的;第二、第三个参数用于约束删除某一行或某几行的数据,不指定的话默认会删除所有行。

    //删除数据butSqliteDelete = findViewById(R.id.but_Sqlite_delete)butSqliteDelete.setOnClickListener {val db = dbHelper.writableDatabasedb.delete("Book", "pages > ?", arrayOf("400"))Toast.makeText(this, "删除完成", Toast.LENGTH_SHORT).show()}

读取数据

查询调用了SQLiteDatabase的query()方法查询数据。这里的query()方法非常简单,只使用了第一个参数指明查询Book表,后面的参数全部为null。这就表示希望查询这张表中的所有数据,虽然这张表中目前只剩下一条数据了。

查询完之后就得到了一个Cursor对象,接着我们调用它的moveToFirst()方法,将数据的指针移动到第一行的位置,然后进入一个循环当中,去遍历查询到的每一行数据。在这个循环中可以通过Cursor的getColumnIndex()方法获取某一列在表中对应的位置索引,然后将这个索引传入相应的取值方法中,就可以得到从数据库中读取到的数据了。接着我们使用Log将取出的数据打印出来,借此检查读取工作有没有成功完成。最后调用close()方法来关闭Cursor。

    butSqliteRead = findViewById(R.id.but_Sqlite_read)butSqliteRead.setOnClickListener {val db = dbHelper.writableDatabase//语法展示、与直接使用SQL//val queryValue = db.query("Book",null,null,null,null,null,null)val queryValue = db.rawQuery("select * from Book", null)if (queryValue.moveToFirst()) {do {val name = queryValue.getString(queryValue.getColumnIndex("name"))Log.i("TAG", "name: $name")} while (queryValue.moveToNext())}queryValue.close()}

在真正的项目中,可能会遇到比这要复杂得多的查询功能 ,这需要自己慢慢摸索。


知识点4:BuildConfig分包

使用BuildConfig实现多渠道打包,为下一个知识点做储备;

plugins {id 'com.android.application'id 'kotlin-android'
}android {compileSdkVersion 30buildToolsVersion "30.0.3"defaultConfig {applicationId "com.example.kotlin_demo"minSdkVersion 16targetSdkVersion 30versionCode 1versionName "1.0.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"multiDexEnabled true //分包flavorDimensions "versionCode"//使用了productFlavors多渠道必须添加}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}//各个环境的配置productFlavors {dev {applicationId "peaceJay.Kotlin"resValue('string', 'app_name', 'Kotlin-开发版')buildConfigField "String", "SERVER_BASE", '"http://xxxx"'}beta {applicationId "peaceJay.Kotlin"resValue('string', 'app_name', 'Kotlin-测试版')buildConfigField "String", "SERVER_BASE", '"http://xxxx"'}prod {applicationId "peaceJay.Kotlin"resValue('string', 'app_name', 'Kotlin')buildConfigField "String", "SERVER_BASE", '"http://xxxx"'}}android.applicationVariants.all { variant ->variant.outputs.all { output ->if (variant.productFlavors[0].name.endsWith("dev")) {outputFileName = "app_dev_v${defaultConfig.versionName}_${defaultConfig.versionCode}_${buildTime()}.apk"} else if (variant.productFlavors[0].name.endsWith("beta")) {outputFileName = "app_beta_v${defaultConfig.versionName}_${defaultConfig.versionCode}_${buildTime()}.apk"} else if (variant.productFlavors[0].name.endsWith("prod")) {outputFileName = "app_release_v${defaultConfig.versionName}_${defaultConfig.versionCode}_${buildTime()}.apk"}}}compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}kotlinOptions {jvmTarget = '1.8'}sourceSets {main {jniLibs.srcDirs = ['libs']}}
}static def buildTime() {return new Date().format("yyyy-MM-dd-HH-mm-ss", TimeZone.getTimeZone("GMT+08:00"))
}dependencies {implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"implementation 'androidx.core:core-ktx:1.3.1'implementation 'androidx.appcompat:appcompat:1.2.0'implementation 'com.google.android.material:material:1.3.0'implementation 'androidx.constraintlayout:constraintlayout:2.0.4'testImplementation 'junit:junit:4.+'androidTestImplementation 'androidx.test.ext:junit:1.1.2'androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'/* 灵活的RecyclerView框架 */implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'/* JSON解析 */implementation 'com.google.code.gson:gson:2.8.6'
}

applicationId 会改变日志目录,如图:

resValue是应用包名,如图:

outputFileName是打包自动生成的文件名,如图:


知识点5:实战封装高性能存储

磨刀不误砍柴工,我们在开始一个完整的项目之前,首先需要的是一个趁手的工具和框架。新建java class文件命名Prefs,实现了磁盘、内存缓存,多运用与存储多种类型数据,与JSON数据。

public class Prefs {// 数据缓存器private static Map<Object, Object> dataMap;// 储存对象private static SharedPreferences prefer;/*** 初始化** @param context {@link Application}*/public static void init(Application context) {Prefs.dataMap = new HashMap<>();Prefs.prefer = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);}/*** 对象数据缓存** @param key  键* @param data 数据对象*/public static void put(String key, Object data) {Prefs.put(key, data, true);}/*** 对象数据缓存** @param key       键* @param data      数据对象* @param diskCache 是否缓存到磁盘*/public static void put(String key, Object data, boolean diskCache) {// 内存缓存Prefs.dataMap.put(key, data);if (diskCache && data != null) {// 磁盘缓存if (data instanceof Integer) {// intPrefs.prefer.edit().putInt(key, (Integer) data).apply();} else if (data instanceof Long) {// longPrefs.prefer.edit().putLong(key, (Long) data).apply();} else if (data instanceof Float) {// floatPrefs.prefer.edit().putFloat(key, (Float) data).apply();} else if (data instanceof Boolean) {// booleanPrefs.prefer.edit().putBoolean(key, (Boolean) data).apply();} else if (data instanceof String) {// StringPrefs.prefer.edit().putString(key, (String) data).apply();} else {// ObjectPrefs.prefer.edit().putString(key, JSON.toJson(data)).apply();}}}/*** 获取对象数据缓存** @param key 键* @param cls 数据类型* @param <T> 泛型* @return 返回指定类型的数据对象或null*/@SuppressWarnings("unchecked")public static <T> T object(String key, Class<T> cls) {if (Prefs.dataMap.containsKey(key)) {return (T) Prefs.dataMap.get(key);}String result = Prefs.prefer.getString(key, null);if (result != null) {T data = JSON.toObject(result, cls);Prefs.dataMap.put(key, data);return data;}return null;}/*** 获取String类型的数据** @param key 键* @param def 默认* @return 返回获取到的数据或默认值*/public static String get(String key, String def) {if (Prefs.dataMap.containsKey(key)) {return (String) Prefs.dataMap.get(key);}String result = Prefs.prefer.getString(key, def);Prefs.dataMap.put(key, result);return result;}/*** 获取Boolean类型的数据** @param key 键* @param def 默认* @return 返回获取到的数据或默认值*/public static boolean get(String key, boolean def) {if (Prefs.dataMap.containsKey(key)) {return (boolean) Prefs.dataMap.get(key);}boolean result = Prefs.prefer.getBoolean(key, def);Prefs.dataMap.put(key, result);return result;}/*** 获取Float类型的数据** @param key 键* @param def 默认* @return 返回获取到的数据或默认值*/public static float get(String key, float def) {if (Prefs.dataMap.containsKey(key)) {return (float) Prefs.dataMap.get(key);}float result = Prefs.prefer.getFloat(key, def);Prefs.dataMap.put(key, result);return result;}/*** 获取Int类型的数据** @param key 键* @param def 默认* @return 返回获取到的数据或默认值*/public static int get(String key, int def) {if (Prefs.dataMap.containsKey(key)) {return (int) Prefs.dataMap.get(key);}int result = Prefs.prefer.getInt(key, def);Prefs.dataMap.put(key, result);return result;}/*** 获取Long类型的数据** @param key 键* @param def 默认* @return 返回获取到的数据或默认值*/public static long get(String key, long def) {if (Prefs.dataMap.containsKey(key)) {return (long) Prefs.dataMap.get(key);}long result = Prefs.prefer.getLong(key, def);Prefs.dataMap.put(key, result);return result;}/*** 清理内存缓存** @param key 键*/public static void remove(String key) {Prefs.dataMap.remove(key);}/*** 清理内存缓存并删除磁盘缓存** @param key 键*/public static void delete(String key) {Prefs.remove(key);Prefs.prefer.edit().remove(key).apply();}/*** 清空内存中的数据*/public static void clean() {Prefs.dataMap.clear();}
}

使用前需要初始化,最好是在<application     android:name=".App">中

Prefs.init(this);  初始化缓存

这里演示了储存单个类型数据并读取,与实体数据保存与读取:

    //①储存单个类型数据Prefs.put("key.one", "String数据")Prefs.put("key.two", 200, false)//获取单个数据类型val one = Prefs.get("key.one", "")Log.i(tag, "key.one: $one")val two = Prefs.get("key.two", 0)Log.i(tag, "key.two: $two")//②储存JSON数据val student = PeopleBean("刘德华", 17320002222)Prefs.put("key.Json", student)//获取JSON数据val bean = Prefs.`object`("key.Json",PeopleBean::class.java)Log.i(tag, "key.Json: " + JSON.toJson(bean))
class PeopleBean(val name: String, val phone: Long)

效果展示:


总结

今天主要对Android常用的数据持久化方式进行了详细的讲解,虽然目前已经掌握了这几种数据持久化方式的用法,但需根据项目的实际需求来选择使用。

Kotlin开发第六天,数据存储,持久化相关推荐

  1. Android应用开发:数据存储和界面展现-1

    1. 相对布局RelativeLayout 特点:相对布局所有组件可以叠加在一起:各个组件的布局是独立的,互不影响:所有组件的默认位置都是在左上角(顶部.左部对齐) 属性 功能描述 android:l ...

  2. Windows 8 应用开发 - 本地数据存储

    原文:Windows 8 应用开发 - 本地数据存储 在应用中通常会遇到用户主动或被动存储信息的情况,当应用关闭后这些数据仍然会存储在本地设备上,用户下次重新激活应用时会自动加载这些数据.下面将通过一 ...

  3. 安卓androidstudio访问本地接口_安卓开发之数据存储在本地的四种方式

    ​ 安卓开发之数据存储在本地的四种方式 本地数据存储,在安卓开发过程中是不可避免的一个话题.这些本地的数据可能是用户的设置,程序的设置,用户的数据图片, 也可能是网络传输的一些缓冲数据. 基本上我们有 ...

  4. iOS开发之数据存储

    概览 在iOS开发中数据存储的方式可以归纳为两类:一类是存储为文件,另一类是存储到数据库.例如前面IOS开发系列-Objective-C之Foundation框架的文章中提到归档.plist文件存储, ...

  5. Kotlin中的数据存储

    数据存储 1 持久化技术简介 数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机的情况下,这些数据仍然不会丢失. 保存在内存中的数据是处于瞬时状态的,而保存在存储设备中 ...

  6. 7_数据存储持久化技术

    持久化技术 持久化技术就是将那些在内存中的瞬时数据存储到存储设备中,使其成为持久数据 文件存储 SharedPregerences存储 数据库存储 文件存储 数据存储到文件中 Context类中提供了 ...

  7. Flutter开发之数据存储-2-文件存储(33)

    数据存储部分在移动开发中是非常重要的部分,无论是一些轻量级的数据(如用户信息.APP配置信息等)还是把需要长期存储的数据写入本地文件或者Sqlite3.都离不开数据存储,上一篇SharedPrefer ...

  8. Flutter开发之数据存储-1-SharedPreferences(32)

    前面讲了很多控件的文章,网络部分也讲了3篇了,图片部分也已经讲过.数据存储部分在移动开发中是非常重要的部分,无论是一些轻量级的数据(如用户信息.APP配置信息等)还是把需要长期存储的数据写入本地文件或 ...

  9. 游戏开发玩家数据存储处理(个人记录)

    结合个人参与的游戏项目开发,谈一下游戏开发玩家数据保存的处理 玩家的数据基本上分为两份,一份是玩家下线或者永久保存的数据,通常保存至数据库(mysql或者mongodb)中,一份是保存在内存中. 一般 ...

最新文章

  1. 万物根源-一分钟教你发布npm包
  2. VC\JS Base64转码
  3. linux虚拟机时间不准的问题
  4. js中apply和join
  5. 嵌入式软件架构设计分层思路
  6. 我的2015年读书计划,每两周读完一本书!
  7. 《Cocos2D权威指南》——3.9 本章小结
  8. c++ 字符串合并_Python基础字符串处理
  9. AIoT、DevOPS、数据平台、开源,你不可不知的微软 Azure 黑科技大公开
  10. 2018年AI如何发展?普华永道做出了8点预测 | 报告下载
  11. 直流电机控制原理与TB6612FNG(初识编码器)
  12. “三权分立”模型的概述
  13. Mina中的zkApp交易snark
  14. 王者荣耀android换ios,2021王者荣耀安卓账号可以转苹果吗 2021年安卓账号转移到ios方法...
  15. HTTP流量复制引流工具(web压测及线上问题复现利器)--Gor(GoReplay)
  16. oracle 每个月求本年该月及之前的合计
  17. 【汇编语言学习】汇编语言基础(一)
  18. 以电子签名形式订立的合同具有证据效力吗?
  19. sharepoint 2013 文档库 资源管理器打开报错 在文件资源管理器中打开此位置时遇到问题,将此网站添加到受信任站点列表,然后重试。
  20. 5种邮件模板分享(含新品开发信、客户人事变动回复、与新采购)

热门文章

  1. js原生后代选择器_js 后代选择器
  2. 自定义Launcher桌面图标无法加载的问题
  3. 沉痛悼念互联网[云原生领域]技术大牛----左耳朵耗子(陈皓老师)
  4. 怎么关闭惠普暗影精灵OMEN 8的主机灯
  5. 【只摘金句】Linux 开发模式带给创业者的启示
  6. 【五社联动】 助力文明城市创建 共同缔造宜居家园
  7. ROLAP vs MOLAP vs HOLAP
  8. SIM7600X常用指令
  9. 高考选日语可以学计算机吗,如果高考选日语,大学选专业有什么限制
  10. python可以做数据库功能吗_python可以用哪些数据库