《Android》Chap.7 数据持久化
持久化技术简介
数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机的情况下,这些数据仍然不会丢失。
保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的。
持久化技术提供了一种机制,可以让数据在瞬时状态和持久状态之间进行转换。
文件存储
文件存储是Android中最基本的数据存储方式,它不对存储的内容进行任何格式化处理,所有数据都是原封不动地保存到文件当中的,因而它比较适合存储一些简单的文本数据或二进制数据。如果你想使用文件存储的方式来保存一些较为复杂的结构化数据,就需要定义一套自己的格式规范,方便之后将数据从文件中重新解析出来。
将数据存储到文件
实现方法
Context
类中提供了一个openFileOutput()
方法,可以用于将数据存储到指定的文件中。这个方法接收两个参数:
第一个参数是文件名,在文件创建的时候使用,注意这里指定的文件名不可以包含路径,因为所有的文件都默认存储到/data/data/<package name>/files/
目录下。
第二个参数是文件的操作模式,主要有MODE_PRIVATE
和MODE_APPEND
两种模式可选
名称 | 用途 |
---|---|
MODE_PRIVATE | 表示当指定相同文件名的时候,所写入的内容将会覆盖原文件中的内容 |
MODE_APPEND | 表示如果该文件已存在, 就往文件里面追加内容,不存在就创建新文件 |
默认是 MODE_PRIVATE
代码示例
fun save(inputText: String){try {val output = openFileOutput("data",Context.MODE_PRIVATE)val writer = BufferedWriter(OutputStreamWriter(output))writer.use { it.write(inputText)}}catch (e: IOException){e.printStackTrace()}
}
openFileOutput()
方法返回的是一个FileOutputStream
对象,得到这个对象之后就可以使用Java流的方式将数据写入文件。use
函数是kotlin内置扩展函数,它会保证在Lambda表达式中的代码全部执行完之后自动将外层的流关闭,这样就不需要再编写一个finally
语句,手动去关闭流了,
完整使用
首先在activity_main.xml
中添加一个id
为editText
的EditText
,用于输入文本。代码略
然后修改MainActivity
中的代码
class MainActivity : AppCompatActivity() {private lateinit var mainBinding : ActivityMainBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)mainBinding = ActivityMainBinding.inflate(layoutInflater)setContentView(mainBinding.root)}//保证在Activity销毁前一定会调用这个方法override fun onDestroy() {super.onDestroy()val inputText = mainBinding.editText.text.toString()save(inputText)}private fun save(inputText: String){try {val output = openFileOutput("data",Context.MODE_PRIVATE)val writer = BufferedWriter(OutputStreamWriter(output))writer.use {it.write(inputText)}}catch (e: IOException){e.printStackTrace()}}}
在EditText
中输入一串字符
按下返回键退出后,在文件中查找:
借助蓝色箭头指向的工具
参照红色方框标记的路径
data
文件中的字符就是上一步在文本框中输入的字符
从文件中读取数据
代码示例
fun load(): String{val content = StringBuilder()try {val input = openFileInput("data")val reader = BufferedReader(InputStreamReader(input))reader.use { reader.forEachLine { content.append(it)}}}catch (e: IOException){e.printStackTrace()}return content.toString()
}
完整使用
修改刚刚的主函数
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)mainBinding = ActivityMainBinding.inflate(layoutInflater)setContentView(mainBinding.root)val inputText = load()if(inputText.isNotEmpty()){ //获取文件中的内容显示在框内mainBinding.editText.setText(inputText)mainBinding.editText.extendSelection(inputText.length)Toast.makeText(this,"Restoring succeeded",Toast.LENGTH_SHORT).show()}
}
SharedPreferences存储
SharedPreferences是使用键值对的方式来存储数据的。
当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把相应的值取出来。而且SharedPreferences还支持多种不同的数据类型存储,如果存储的数据类型是整型,那么读取出来的数据也是整型的;如果存储的数据是一个字符串,那么读取出来的数据仍然是字符串。
将数据存储到SharedPreferences中
要想使用SharedPreferences存储数据,首先需要获取SharedPreferences对象。
Context类中的getSharedPreferences()方法
此方法接收两个参数:
第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在/data/data/<package name>/shared_prefs/
目录下的;
第二个参数用于指定操作模式,目前只有默认的MODE_PRIVATE
这⼀种模式可选,它和直接传入0的效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。
Activity类中的getPreferences()方法
此方法只接收一个操作模式参数,因为使用这个方法时会自动将当前Activity的类名作为SharedPreferences的文件名。
得到了SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了,主要可以分为3步实现。
- 调用SharedPreferences对象的
edit()
方法获取一个SharedPreferences.Editor
对象。 - 向
SharedPreferences.Editor
对象中添加数据,比如添加一个布尔型数据就使用putBoolean()
方法,添加一个字符串则使用putString()
方法,以此类推。 - 调用
apply()
方法将添加的数据提交,从而完成数据存储操作。
使用实例
在activity_main.xml
中添加一个id为saveButton
的按钮
然后在MainActivity
中编写代码
class MainActivity : AppCompatActivity() {private lateinit var mainBinding: ActivityMainBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)mainBinding = ActivityMainBinding.inflate(layoutInflater)setContentView(mainBinding.root)mainBinding.saveButton.setOnClickListener {//获取SharedPreferences.Editor对象val editor = getSharedPreferences("data",Context.MODE_PRIVATE).edit()editor.putString("name","ZLS") //添加String类型数据editor.putInt("age",18)editor.putBoolean("married",false)editor.apply() //提交数据}}
}
点击按钮后,在/data/data/com.example.sharedPreferencestest/shared_prefs/
目录下找到新生成的data.xml
文件,其中保存了刚刚所有的数据。
从SharedPreferences中读取数据
SharedPreferences对象中提供了一系列的get方法,用于读取存储的数据,每种get方法都对应了SharedPreferences.Editor
中的一种put
方法,比如读取一个布尔型数据就使用getBoolean()
方法,读取一个字符串就使用getString()
方法。
这些get
方法都接收两个参数:第一个参数是键,传入存储数据时使用的键就可以得到相应的值了;第二个参数是默认值,即表当传入的键找不到对应的值时会以什么样的默认值进行返回。
使用实例
在activity_main.xml
中再添加一个id为printButton
的按钮
然后在MainActivity
中添加代码
mainBinding.printButton.setOnClickListener{val prefs = getSharedPreferences("data",Context.MODE_PRIVATE)val name = prefs.getString("name","")val age = prefs.getInt("age",0)val married =prefs.getBoolean("married",false)Log.d("MainActivity","name is $name")Log.d("MainActivity","age is $age")Log.d("MainActivity","married is $married")
}
点击第二个button就会得到:
实现记住密码功能
编写activity_login.xml
登录界面
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context=".LoginActivity"><LinearLayoutandroid:orientation="horizontal"android:layout_width="match_parent"android:layout_height="60dp"><TextViewandroid:layout_width="90dp"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:textSize="18sp"android:text="Account:" /><EditTextandroid:id="@+id/accountEdit"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:layout_gravity="center_vertical" /></LinearLayout><LinearLayoutandroid:orientation="horizontal"android:layout_width="match_parent"android:layout_height="60dp"><TextViewandroid:layout_width="90dp"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:textSize="18sp"android:text="Password:" /><EditTextandroid:id="@+id/passwordEdit"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:layout_gravity="center_vertical" /></LinearLayout><LinearLayoutandroid:orientation="horizontal"android:layout_width="match_parent"android:layout_height="wrap_content"><CheckBoxandroid:id="@+id/rememberPass"android:layout_width="wrap_content"android:layout_height="wrap_content"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="18sp"android:text="remember password"/>/></LinearLayout><Buttonandroid:id="@+id/login"android:layout_width="200dp"android:layout_height="60dp"android:layout_gravity="center_horizontal"android:text="login" /></LinearLayout>
在LoginActivity
中编写代码
class LoginActivity : BaseActivity() {private lateinit var loginBinding: ActivityLoginBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)loginBinding = ActivityLoginBinding.inflate(layoutInflater)setContentView(loginBinding.root)//获取SharedPreferences对象val prefs = getPreferences(MODE_PRIVATE)val isRemember = prefs.getBoolean("remember_password",false)if(isRemember){//将账号和密码都设置在文本框中val account = prefs.getString("account","")val password = prefs.getString("password","")loginBinding.accountEdit.setText(account)loginBinding.passwordEdit.setText(password)loginBinding.rememberPass.isChecked = true}loginBinding.login.setOnClickListener {val account = loginBinding.accountEdit.text.toString()val password = loginBinding.passwordEdit.text.toString()if (account == "admin" && password == "123456"){val editor = prefs.edit()if (loginBinding.rememberPass.isChecked){//如果选中就将输入的账号密码存入文件中editor.putBoolean("remember_password",true)editor.putString("account",account)editor.putString("password",password)}else{//如果没有选中则清空文件中的数据editor.clear()}editor.apply()val intent = Intent(this,MainActivity::class.java)startActivity(intent)finish()}else{Toast.makeText(this,"account or password is invalid",Toast.LENGTH_SHORT).show()}}}
}
当输入正确的账号密码并选择保存后,关闭应用,再次打开时,账号和密码已经自动填充。
SQLite数据库存储
SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百KB的内存就足够了,因而特别适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务,所以只要以前使⽤过其他的关系型数据库,就可以很快地上手SQLite。而SQLite又比一般的数据库要简单得多,它甚至不用设置用户名和密码就可以使用。
创建数据库
Android为了让用户能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper
帮助类。
SQLiteOpenHelper
是一个抽象类,这意味着如果想要使用它,就需要创建一个自己的帮助类去继承它。SQLiteOpenHelper
中有两个抽象方法:onCreate()
和onUpgrade()
。必须在自己的帮助类里重写这两个方法,然后分别在这两个方法中实现创建和升级数据库的逻辑。
SQLiteOpenHelper
中还有两个非常重要的实例方法:getReadableDatabase()
和getWritableDatabase()
。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则要创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。 不同的是,当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()
方法返回的对象将以只读的方式打开数据库, 而getWritableDatabase()
方法则将出现异常。
SQLiteOpenHelper
中有两个构造方法可供重写,一般使用参数少一点的那个构造方法即可。这个构造方法中接收4个参数:第一个参数是Context
,这个没什么好说的,必须有它才能对数据库进行操作;第二个参数是数据库名,创建数据库时使用的就是这里指定的名称;第三个参数允许我们在查询数据的时候返回一个自定义的Cursor
,一般传入null
即可;第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。构建出SQLiteOpenHelper
的实例之后,再调用它的getReadableDatabase()
或getWritableDatabase()
方法就能够创建数据库了,数据库文件会存放在/data/data/<packagename>/databases/
目录下。此时,重写的onCreate()
方法也会得到执行,所以通常会在这里处理一些创建表的逻辑。
完整使用
新建MyDatabaseHelper
类继承自SQLiteOpenHelper
,在代码中执行SQL语句,完成创建表的操作
class MyDatabaseHelper(val context: Context, name: String, version: Int) :SQLiteOpenHelper(context, name, null, version){//建表语句定义成字符串变量private val createBook = "create table Book (" +"id integer primary key autoincrement," +"author text," +"price real," +"pages integer," +"name text)"override fun onCreate(db: SQLiteDatabase) {db.execSQL(createBook) //执行建表语句Toast.makeText(context, "Create succeeded", Toast.LENGTH_SHORT).show()}override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}}
在activity_main.xml
中再添加一个id为createDatabase
的按钮
然后在MainActivity
中添加代码
class MainActivity : AppCompatActivity() {private lateinit var mainActivity: ActivityMainBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)mainActivity = ActivityMainBinding.inflate(layoutInflater)setContentView(mainActivity.root)//数据库名为BookStore.db,版本号为1val dbHelper = MyDatabaseHelper(this,"BookStore.db",1)mainActivity.createDatabase.setOnClickListener {dbHelper.writableDatabase}}
}
第一次点击button后,弹出toast,表明数据库文件已经新建完成。
查看数据表
借助一个叫作Database Navigator的插件工具可以查看数据表。
从Android Studio导航栏中打开Preferences→Plugins,就可以进入插件管理界面了:
点击Install
后重启Android Studio。
打开Device File Explorer
,然后进入/data/data/com.example.databasetest/databases/
目录下,可以看到已经存在了一个BookStore.db
文件,右击后选择Save As
,将它放置在任意你方便查找的位置。
然后在Android Studio的左侧边栏找到DB Browser
工具。
为了打开刚刚导出的数据库文件,需要点击这个工具左上角的加号按钮,并选择SQLite
选项
然后在弹出窗口的Database
配置中选择我们刚才导出的BookStore.db
文件
可以看到BookStore.db
数据库中确实存在了一张Book
表,并且Book
表中的列也和前面使用的建表语句完全匹配,由此证明BookStore.db
数据库和Book
表确实已经创建成功了。
升级数据库
onUpgrade()
方法是用于对数据库进行升级的。
在val dbHelper = MyDatabaseHelper(this,"BookStore.db",1)
语句中,最后一个参数代表版本号,当版本号为比1大的数
时,就执行onUpgrade()
方法。
添加数据
调用SQLiteOpenHelper
的getReadableDatabase()
或getWritableDatabase()
方法是可以用于创建和升级数据库的,不仅如此,这两个方法还都会返回⼀个SQLiteDatabase
对象,借助这个对象就可以对数据进行添加、查询、更新、删除操作了。
SQLiteDatabase
中提供了一个insert()
方法,专门用于添加数据。
它接收3个参数:第一个参数是表名,希望向哪张表里添加数据,这里就传入该表的名字;第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL
,一般用不到这个功能,直接传入null
即可;第三个参数是一个ContentValues
对象,它提供了一系列的put()
方法重载,用于向ContentValues
中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。
完整使用
在activity_main.xml
中再添加一个id为addData
的按钮
然后在MainActivity
中的添加代码
mainActivity.addData.setOnClickListener {val db = dbHelper.writableDatabaseval value1 = ContentValues().apply {//组装数据put("name","The Da Vinci Code")put("author","Dan Brown")put("pages", 454)put("price", 16.96)}db.insert("Book",null,value1) //插入数据val values2 = ContentValues().apply {put("name", "The Lost Symbol")put("author", "Dan Brown")put("pages", 510)put("price", 19.95)}db.insert("Book", null, values2)
}
再次运行时,点击ADD DATA
按钮后,再次使用DB Browser
打开BookStore.db
双击Book
表格
在弹出设置查询条件的窗口中点击No Filter
,表示不需要设置任何查询条件,然后就能看到表中的数据了。
更新数据
SQLiteDatabase
中提供了一个非常好用的update()
方法,用于对数据进行更新。
它接收4个参数:第一个参数和insert()
方法一样,也是表名,指定更新哪张表里的数据;第⼆个参数是ContentValues
对象,要把更新数据在这里组装进去;第三、第四个参数用于约束更新某一行或某几行中的数据,不指定的话默认会更新所有行。
完整使用
在activity_main.xml
中再添加一个id为updataData
的按钮
然后在MainActivity
中的添加代码
mainActivity.updateData.setOnClickListener {val db = dbHelper.writableDatabaseval values = ContentValues() //构建ContentValues对象values.put("price",10.99) //将价格这一列的数据改为10.99db.update("Book",values,"name = ?", arrayOf("The Da Vinci Code")) //第三个参数表示更新所有name=?的行,?表示占位符,对应的是第四个参数的内容
}
再次查看表格数据已经更改完成。
删除数据
SQLiteDatabase
中提供了一个delete()
方法,专门用于删除数据。
它接收3个参数:第一个参数仍然是表名;第二、第三个参数用于约束删除某一行或某几行的数据,不指定的话默认会删除所有行。
完整使用
在activity_main.xml
中再添加一个id为deleteData
的按钮
然后在MainActivity
中的添加代码
mainActivity.deleteData.setOnClickListener {val db = dbHelper.writableDatabasedb.delete("Book","pages > ?", arrayOf("500")) //删除页数大于500的
}
再次查看表格。
查询数据
SQLiteDatabase
中还提供了一个query()
方法用于对数据进行查询。
它至少接收7个参数:第一个参数还是表名,表示从哪张表中查询数据;第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列;第三、第四个参数用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据;第五个参数用于指定需要去group by
的列,不指定则表示不对查询结果进行group by
操作;第六个参数用于对group by
之后的数据进行进一步的过滤,不指定则表示不进行过滤;第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。
query() 方法参数
|
对应SQL部分 | 描述 |
---|---|---|
table | from table_name | 指定查询的表名 |
columns | select column1, column2 | 指定查询的列名 |
selection | where column = value |
指定where 的约束条件
|
selectionArgs | - |
为where 中的占位符提供具体的值
|
groupBy | group by column |
指定需要group by 的列
|
having | having column = value |
对group by 后的结果进一步约束
|
orderBy | order by column1, column2 | 指定查询结果的排序方式 |
调用query()
方法后会返回一个Cursor
对象,查询到的所有数据都将从这个对象中取出。
完整使用
在activity_main.xml
中再添加一个id为queryData
的按钮
然后在MainActivity
中的添加代码
mainActivity.queryData.setOnClickListener {val db = dbHelper.writableDatabase//查询Book表的所有数据val cursor = db.query("Book",null,null,null,null,null,null)if (cursor.moveToFirst()){ //将数据指针移到第一位do { //遍历Cursor对象,取出数据并打印val name = cursor.getString(cursor.getColumnIndex("name"))val author = cursor.getString(cursor.getColumnIndex("author"))val pages = cursor.getInt(cursor.getColumnIndex("pages"))val price = cursor.getDouble(cursor.getColumnIndex("price"))Log.d("MainActivity", "book name is $name")Log.d("MainActivity", "book author is $author")Log.d("MainActivity", "book pages is $pages")Log.d("MainActivity", "book price is $price")}while (cursor.moveToNext()) //每次查询完一行数据就将指针移到下一位}cursor.close() //查询完要关闭
}
运行后点击queryData
按钮,就可以看到表中的数据。
最佳实践
使用事务
SQLite数据库是支持事务的,事务的特性可以保证让一系列的操作要么全部完成,要么一个都不完成。
完整使用
在activity_main.xml
中再添加一个id为replaceData
的按钮
然后在MainActivity
中的添加代码
mainActivity.replaceData.setOnClickListener {val db = dbHelper.writableDatabasedb.beginTransaction() //开启事务try {db.delete("Book",null,null) //删除表中数据if(true){ //手动抛出异常让事物失败throw NullPointerException()}//编写新的数据val values = ContentValues().apply {put("name", "Game of Thrones")put("author", "George Martin")put("pages", 720)put("price", 20.85)}db.insert("Book",null,values) //插入新的数据db.setTransactionSuccessful() //事务已经执行成功}catch (e: Exception){e.printStackTrace()}finally {db.endTransaction() //结束事务}
}
运行后点击replaceData
按钮再点击queryData
按钮,就可以看到表中还是之前的数据。表明即使在删除旧数据的操作完成后抛出异常,但添加新数据的代码没有执行到,事务失败,则删除旧数据的操作也失败
但如果删除掉其中的抛出异常的代码再执行,数据就替换成功
升级数据库的最佳写法
在实际的产品发布使用过程中,数据库的每次升级都要保证用户之前的数据不丢失。
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {if(oldVersion <= 1){//... }if (oldVersion <= 2){//...}
}
每当升级一个数据库版本的时候,onUpgrade()
方法里都一定要写一个相应的if
判断语句。这是为了保证App在跨版本升级的时候,每一次的数据库修改都能被全部执⾏。
比如用户当前是从第2版升级到第3版,那么只有第二条判断语句会执行,而如果用户是直接从第1版升级到第3版,那么两条判断语句都会执行。使用这种方式来维护数据库的升级,不管版本怎样更新,都可以保证数据库的表结构是最新的,而且表中的数据完全不会丢失。
《Android》Chap.7 数据持久化相关推荐
- Android数据持久化
Android数据持久化(存储) 1.SharedPreferences SharedPreferences是Android提供的数据持久化的一种手段,适合单进程.小批量的数据存储与访问.Shared ...
- android libbfaac.so,Android中Json数据读取与创建
一: Json的特性和在数据交互中的地位就不用说了,直接看案例. 首先在android studio中创建assets文件目录,用于存放Json数据文件,android studio 1.3 默认项 ...
- Android数据持久化:SharePreference
SharePreference:作为Android数据持久化的一种,具有一定的便捷性,适合存储一些体积小的数据. 存储数据方式:键值对的方式,类似于Map: 利用SharePreference.Edi ...
- Android数据持久化:文件存储
数据持久化: 数据可分为瞬时数据和关键数据.保存在内存之中的数据是瞬时数据,而对于一些关键性数据,后期需要持续使用的,应当保存在存储设备中: 持久化保存方式: 文件存储.SharePreference ...
- Android教程 -09 数据的持久化存储
视频为本篇播客知识点讲解,建议采用超清模式观看, 欢迎点击订阅我的优酷 任何一个程序其实说白了就是在不停地和数据打交道,数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证及时手机关机的情况下 ...
- Android入门(12)| 数据持久化
文章目录 数据持久化 文件存储 将数据存储进文件 实例 从文件中读取数据 实例 SharedPreferences存储 将数据存储进文件 实例 从文件中读取数据 实例 实现记住密码的功能 SQLite ...
- $《第一行代码:Android》读书笔记——第6章 数据持久化
主要讲述了Android数据持久化的三种方式:文件存储.SharedPreference存储.SQLite数据库存储. (一)文件存储 其实Android中文件存储方式和Java的文件操作类似,就是用 ...
- android 清空数组缓存,Android数据持久化之读写SD卡中内容的方法详解
本文实例讲述了Android数据持久化之读写SD卡中内容的方法.分享给大家供大家参考,具体如下: 前面文章里讲的那三个方法:openFileOutput.openFileInput虽然都能通过流对象O ...
- Android数据持久化技术
Android 数据持久化技术 数据持久化 文件存储 将数据存储到文件中 从文件中读取数据 SharedPreferences存储 使用SharedPreferences存储数据 从SharedPre ...
最新文章
- python操作word文档(python-docx)
- linux中科大yum源,CentOS:国内常用的yum源
- Google Brain大牛Jeff dean视频
- 对象存储与块存储、文件存储等对比
- [转]ECMAScript 6 入门 -编程风格
- yolov3-tiny神经网络FPGA(ZYNQ7020)实现
- Android modem 开发(17)---VoLTE Call
- 资源打包Assetbundle .
- saltstack系列2之zabbix-agent自动化部署
- Android Multimedia框架总结(二十一)MediaCodec中创建到start过程(到jni部分)
- 职场中警惕七种危险的同事
- Java图片识别技术原理-只取图片像素。。。
- 0到100之间的阶乘linux算法,零基础学算法-阶乘
- 儿童节html模板,六一儿童节作文350字满分模板
- 华为CANN训练营笔记——应用开发全流程 [5](with 代码版)
- android第三方拨号盘,android M拨号盘开源之旅(二)--- 浅析拨号盘主界面
- 计算机vs2013论文的命题,科学网—2篇论文评为IEEE TBME期刊2013和2014最高引用论文(排名第1,第2) - 张智林的博文...
- 127_不同国家的手机号,除去国家代码后,有可能相同吗?
- java计算机毕业设计冠军体育用品购物网站MyBatis+系统+LW文档+源码+调试部署
- 《运营力——微信公众号 设计 策划 客服 管理 一册通》导读
热门文章
- linux 自动同步时间命令,LINUX系统时间怎么设置同步脚本或命令?
- 波士顿矩阵模型:产品定位
- 登录微软账号的Windows电脑如何远程?
- 在线markdown编辑器_Beegit初探:协作在线Markdown编辑器
- 面试必备,JS常见算法面试题整理
- java 抽象泛型方法,Java泛型方法 - Only_小白的个人空间 - OSCHINA - 中文开源技术交流社区...
- 一张图看懂DNS域名解析全过程
- python中func函数可以没有返回值吗_python让函数不返回结果的方法
- 2023-03-24:音视频mp3和h264混合(muxer)编码为mp4,用go语言编写。
- $.each的循环遍历