安卓基础知识系列旨在简明扼要地提供面试或工作中常用的基础知识,让对安卓还不太熟悉的小伙伴更快地入门。同时自己在工作中,也没法完全记住所有的基础细节,写这样的系列文章,可以让自己形成一个更完备的知识体系,同时给自己日后留个知识参考。

开始的开始

安卓四大组件中,最常用的组件莫过于我们的Activity组件。安卓程序员每天都在直接或间接地接触着Activity,所以Activity基础知识的重要性不言而喻。

正文

一. 正常情况下的生命周期

正常情况下,新建一个Activity A会顺序经历如下几个生命周期:

  1. onCreate:A正在被创建,这个方法中,我们可以做一些Activity的初始化操作。例如布局文件的加载与事件的绑定(setContentView,findViewById,setOnClickListener等)。
  2. onStart: A 正在被启动,A 由不可见变为可见时调用,此时 A 还无法与用户交互。此时可以做一些数据的初始化操作(开启线程去拉本地数据库数据,或从后台拉数据)。
  3. onResume: A 已经可见,并出现在前台,该Activity位于返回栈栈顶,可以响应用户的操作,即可以与用户交互了。

如果此时用户拉起另一个Activity B, Activity A会顺序经历如下几个生命周期:

  1. onPause: 表示 A 正在停止,准备从前台返回至后台,此时可以做一些停止动画,数据存储等工作。值得注意的是,在onPause生命周期进行的工作不能太耗时,不然会影响 B 的显示。(Activity A的onPause执行完后,Activity B的onResume才会执行)。

  2. onStop: 在 A 完全不可见时调用,紧随着onPause执行,表 A 即将停止,此时 A 已经不在前台,可以做一些稍微重量级的回收工作,但同样不能太耗时,(如果此时新打开的Activity B是对话框式的Activity,背景存在一定区域是透明的,则Activity A的onStop不会调用)。

  3. onDestroy:表示 A 即将被销毁,在这里可以进行资源的回收、释放工作。一般是经过用户按下back键或者系统资源紧张时,将Activity A释放掉以获得更多的内存时调用。

Activity B经历了onResume生命周期后已经显示在前台,如果此时按下back返回键,从 B 页面返回,而 A 还停留在onStop,没有经过onDestroy生命周期的话,A 会经历如下几个生命周期后重新显示:

  1. onRestart: A 由onStop停止状态,转为运行状态时调用,表 A 正在被重新启动。
  2. onStart
  3. onResume

可以看到,排除Activity退到后台的情况,Activity从创建到销毁,总共会经过6个生命周期,分别是onCreate,onStart,onResume,onPause,onStop,onDestroy。从这几个生命周期发生时的特性来看,会发现onCreate与onDestroy、onStart与onStop、onResume与onPause是一种相反的状态。

如果该Activity有事件或服务需要注册(register),一般会在onCreate中进行,而对应的解注册操作(unregister),最好在与onCreate对应的onDestroy中完成,释放资源,这是一种良好的编程习惯。

将上述几个生命周期总结成一张图:

通过上面的文字描述,看这个图应该已经很清楚了,不过,未提到的是,上图中onPause()还有个箭头指向了onResume(),这是一种极端情况。即考虑当Activity A 跳转到Activity B 的情况,此时 A 还在执行onPause() , B 还未显示出来。快速地从B回到A,此时会直接执行 A 的onResume()而不会走onRestart()。不过一般很难复现这种操作,大家留个心眼就行。

在将上文的细节提炼一下:

  1. onStart、onResume、onPause、onStop看起来回调调用的时机差不多,它们俩区别在哪呢?

    onStart和onStop是从Activity是否可见的角度来回调的,而onResume和onPause则是从Activity是否位于前台、是否可以与用户交互的角度来回调的,除了这方面的差别,在时机使用过程中,它们没有其他明显区别。

  2. 从Activity A 跳转到Activity B,是先执行 A 的onPause(),还是先执行 B 的onResume()呢?

    这部分设计Activity跳转的源码,源码逻辑太深、太复杂就不先在基础篇讨论了,大家目前 先记住结论就好:A 的onPause()会先执行,然后才执行 B 的onResume(),这个细节也是面试中可能会问到的点。

  3. 在onPause中不能进行耗时的操作,否则会影响新Activity的显示,稍微重一点的操作可以放在onStop中,但依然不能太耗时。

如何 Activity A 启动一个透明的 Activity B,会经历哪些生命周期呢?

这是我面试遇到的一个问题,因为 B 页面透明, 所以跳转到 B 页面后,A 页面依然可见,因此就不会调用 Activity A 的 onStop 方法。

  1. 一般情况,A/B 均不是透明页面:

    • A 跳转 B 页面会经历的生命周期:A.onPause() -> B.onCreate() -> B.onStart() -> B.onResume() -> A.onStop。
    • 从 B 页面返回 A 页面经历的生命周期:B.onPause() -> A.onRestart() -> A.onStart() -> A.onResume() -> B.onStop()。
  2. B是透明页面的情况:
    • 如果 B 是透明的,A 跳转到 B:A.onPause() -> B.onCreate() -> B.onStart() -> B.onResume()。
    • 从 B 返回 A:B.onPause() -> A.onResume() -> B.onPause()。
二. 异常情况下的生命周期

异常情况就是除开用户自己主动退出Activity的情况。

考虑一种异常情况,Activity C 打开了Actvity D后,C进入了停止状态(调用了onStop()),此时系统内存不足,需要回收 C(调用C的onDestroy()) ,当用户从 D 返回到 C,C 会被重新创建(调用onCreate())。如果原来 C 里边有临时状态存储着,比如TextView中的文字。那么从 D 返回 C 时,C因为重新创建,如果TextView未指定ID,那它原来的文字就会消失,这一定程度影响了用户的体验。

因此为了优化用户体验,Activity提供了一个onSaveInstanceState()回调方法,这个方法可以保证异常情况下,在Activity被回收之前一定会被调用。

onSaveInstanceState()方法会携带一个bundle参数,我们可以通过bundle对象,存储一些简单的状态信息。

Activity重新创建后,系统会调用onRestoreInstanceState(),并把Activity销毁时onSaveInstanceState()方法所保存的Bundle对象作为参数同时传递给onRestoreInstanceState()和onCreate()。

你可以选择这两个方法中任意一个来恢复数据,二者的区别是:onRestoreInstanceState一旦被调用,其bundle对象一定是有值的,不需要额外的判空处理,而onCreate在正常启动Activity的情况下bundle对象是无值的。

override fun onCreate(savedInstanceState: Bundle?) {}override fun onRestoreInstanceState(savedInstanceState: Bundle) {}

调用时机

onSaveInstanceState()onStop()之前调用,onRestoreInstanceState()会在onStart()之后调用。

异常情况下,Activity数据的存储和恢复的生命过程都是一样的,常见的异常情况主要有以下两种:

  1. 资源相关的系统配置发生改变导致Activity被杀死并重新创建

    首先说说什么是系统配置信息。

    不同手机设备的分辨率不同,要将图片适配不同大小的手机屏幕,我们通常会在drawable-xhdpi,drawable-xxhdpi,drawable-xxxhdpi等目录中存放对应大小的图片Resource文件。

    当App启动时,系统就会根据当前设备的屏幕情况去加载合适的Resource资源。同一台设备的横屏和竖屏时的屏幕大小也是不一样的,如果当前Activity处于竖屏状态,突然旋转至横屏,那么此时系统的屏幕配置发生了改变。

    默认情况下,Activity会被销毁并重建。因为这种销毁是一种非用户主导的、异常的情况,Activity会调用onSaveInstanceState()方法后销毁,重建时会再调用onRestoreInstanceState()方法,即走一遍异常情况的生命周期。

    如何避免这种因为系统配置更改而导致Activity重建的异常情况?

    如果app在应用配置变更期间无需更新资源,我们可以在AndroidManifest.xml文件中相应的Activity声明,自行处理相关配置的变更,从而阻止系统重建Activity。
    只需指定相关的configChanges属性。比如下面的例子,就阻止了当屏幕发生旋转时Activity的系统自动重建。

    <activity android:name=".MainActivity"android:configChanges="orientation|screenSize" />
    

    当configChanges中指定的配置发生变化时,系统会调用Activity的onConfigurationChanged()方法,如果有需要处理配置变更的话,可以在这个方法手动处理。一般我们在屏幕旋转时,希望Activity能保持原样,不重建就好了,所以空实现该方法即可。

    当然,需要自行处理时,比如检查当前设备的方向,你可以这么写:

    override fun onConfigurationChanged(newConfig: Configuration) {super.onConfigurationChanged(newConfig)// Checks the orientation of the screenif (newConfig.orientation === Configuration.ORIENTATION_LANDSCAPE) {Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show()} else if (newConfig.orientation === Configuration.ORIENTATION_PORTRAIT) {Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show()}
    }
    

    configChanges属性可以指定很多属性,如果你还想指定更多配置,不同配置间用"|"分隔,比如上面那样。

    部分常用的configChanges配置项目如下:

    项目 描述
    keyboard 键盘类型发生变更 — 例如,用户插入外置键盘。
    keyboardHidden 键盘无障碍功能发生变更 — 例如,用户显示硬键盘。
    locale 语言区域发生变更 — 用户已为文本选择新的显示语言。
    orientation 屏幕方向发生变更 — 用户旋转设备。
    请注意:如果应用面向 Android 3.2(API 级别 13)或更高版本的系统,则还应声明 "screenSize" 配置,因为当设备在横向与纵向之间切换时,该配置也会发生变更
    screenLayout 屏幕布局发生变更 — 不同的显示现可能处于活跃状态。
    screenSize 当前可用屏幕尺寸发生变更。
    该值表示当前可用尺寸相对于当前纵横比的变更,当用户在横向与纵向之间切换时,它便会发生变更。(API 13 中新增)

    这个表格我直接照搬安卓开发者官网的,官网有关于系统配置更完整的资料,大家有需要可以自己点开看看,科学上网。

  2. 系统内存不足导致低优先级的Activity被杀死

    这种情况就是我们分析异常情况下的生命周期时举的例子。Activity C 跳转至Activity D,C 处于后台,当系统内存资源不足时,C的优先级较低,会被系统销毁以获得更多的内存,然后再从 D 回到 C ,C 会被重建,走一遍异常时的数据存储和恢复的生命过程。

    Activity按照优先级从高到低,可以分为如下三中情况:

    • 前台Activity,正在与用户交互的Activity,其优先级最高。
    • 可见但非前台Activity,比如Activity中弹出了一个对话框,导致Activity可见但出于后台,无法与用户直接交互。
    • 后台Activity,已经被暂停的Activity,比如执行了onStop,用户看不见,优先级最低。

    当系统内存不足时,系统会按照上述优先级的顺序去杀死Activity所在的进程。并在后续通过onSaveInstanceState()和onRestoreInstanceState()去存储和恢复数据。

    如果一个进程中没有四大组件在执行,那么这个进程将会很快被系统杀死,因此一些后台工作不适合脱离四大组件而单独运行在后台中。比较好的方法是将后台的工作放到Service服务中,从而保证进程有一定的优先级,就不会容易被系统杀死了。

一些细节的补充(课外知识)

关于Activity生命周期的经典知识,看到上面,已经足够日常工作中的使用了。

下面介绍点偏冷门的知识和数据存储和恢复更优美的技术,内容可能有些繁杂,如果你看的吃力,可以跳过这部分,对你理解Activity的生命周期不会有任何影响!

  1. View也有onSaveInstanceState、onRestoreInstanceState()方法

在这两个方法中,系统自动为我们做了一定的恢复工作。当Activity在异常情况下需要重建时,系统为默认为我们保存当前Activity的视图结构,并且在重启后为我们恢复这些数据,比如文本框中用户输入的数据、ListView滚动的位置等等。

// TextView 的onSaveInstanceState()方法
@Override
public Parcelable onSaveInstanceState() {Parcelable superState = super.onSaveInstanceState();// Save state if we are forced tofinal boolean freezesText = getFreezesText();boolean hasSelection = false;int start = -1;int end = -1;if (mText != null) {start = getSelectionStart();end = getSelectionEnd();if (start >= 0 || end >= 0) {// Or save state if there is a selectionhasSelection = true;}}if (freezesText || hasSelection) {SavedState ss = new SavedState(superState);...ss.error = getError();if (mEditor != null) {// 存储editor状态ss.editorState = mEditor.saveInstanceState();}return ss;}return superState;
}// TextView 的onRestoreInstanceState()方法
@Override
public void onRestoreInstanceState(Parcelable state) {if (!(state instanceof SavedState)) {super.onRestoreInstanceState(state);return;}SavedState ss = (SavedState) state;super.onRestoreInstanceState(ss.getSuperState());// XXX restore buffer type too, as well as lots of other stuffif (ss.text != null) {// 调用setText,恢复数据setText(ss.text);}...
}

上面是EditText竖屏和横屏切换时,text数据存储和恢复演示的效果。

View的onSaveInstanceState()调用过程是这样的:首先Activity被重建时,会调用onSaveInstanceState()去保存数据,然后Activity会委托Window去保存数据,接着Window再委托与它关联的顶层容器DecorView去保存数据,最后DecorView再遍历它一个个子View去保存数据。

View的onRestoreInstanceState()也是类似的思想。

值得注意的是,如果你希望View在异常销毁时能顺利调用onSaveInstanceState()方法,你必须得为该View指定一个id,否则View不会走到onSaveInstanceState()流程。

isSaveEnable也会影响onSaveInstanceState()的调用,这个值是个标志位,表示View是否会保存其状态。默认为true,即,默认会保存状态。

如果你不想View保存状态,可以将其设为false

<com.jamgu.hwstatistics.MyEditTextandroid:id="@+id/edit_text"...android:saveEnabled="false"/>

View异常情况下的数据存储与恢复介绍完了,说这个知识点冷门,其实只是我的看法。我之前看到过这个知识点,但我工作了一年多的时间,完全没有涉及过View的数据存储与恢复的内容,所以就淡忘了,因此说其冷门。

今天写这篇内容的时候,才又想起来View也有异常情况的数据恢复过程!不过其实忘掉也没什么关系。在工作中,View销毁了后,我们一般会让它重新走一遍数据加载的过程,不需要执行其默认的数据存储与恢复。

再说它系统默认恢复的状态也是有限的,一般工作中的数据都是从网络拉回来的数据,所以我们更偏向于直接从网络再拉一遍数据回来。

上面的知识可能有点晦涩,View的流程如果你跟着源码看,应该会有更深刻的认识。如果你是个新手,对安卓还不是很熟悉,看上面这一部分内容可能有些吃力,看不太懂。

不过完全没关系,View的数据与恢复,你只需要知道有这个过程就行了,

  1. 竖屏与横屏切换时,状态存储的更优方案

接下来我将为大家介绍Jetpack库的组件ViewModel,它在设备竖屏与横屏切换时的状态存储和恢复,比传统的用onSaveInstanceState()、onRestoreInstanceState()方法更优。

JetPack库是一个由多个库组成的套件,可帮助开发者遵循最佳做法、减少样板代码,降低代码间的耦合度,并让代码在不同Android版本的设备中保持一致的体验。fragment、dataBinding、ViewModel,MVVM等都是JetPack的一部分。

大家现在基本都androidx库,在这之前,你可能还用过android-support-v7,v28库等等,为什么会有support库,就是因为随着安卓版本的迭代,一些旧的控件在新的安卓版本上难以兼容,所以为了保持体验的一致性,安卓每发一个新的版本,就会有一个support库跟着发出来。

最后发布的是android.support.v28系列库,这样的命名格式,随着安卓版本的升级,support库越来越多,谷歌也意识到这不是一个办法。因此发布了一个androidx库来统一,以后发布新的版本,只需要对androidx库进行升级迭代就可以了,就不需要再发布新的库。JetPack库是androidx库的一部分。

所以回到正题,ViewModel为啥更优?主要有以下几个优点:

  1. 可以存储更复杂的临时状态。

    前文提到,Activity的onSaveInstanceState()主要是通过将状态存储到Bundle对象中,Bundle对象中的数据是一个个键值对,一个key对应着一个value,存储基本数据类型,如Int、Float、Boolean等很方便,但如果我们要在bundle中存储一个对象,就会很麻烦。

    首先这个对象需要继承Parcelable接口,并实现几个方法,使对象能够序列化,这需要开发者编写一定的代码。

    其次要从Bundle取出对象,需要经过反序列化的过程,如果你需要反序列化的对象有很多个,那么用户可能需要等待一段时间才能看到之前的状态,不仅消耗了性能,对应用的用户体验也会有一定的影响。

    而ViewModel,它是一个对象,其状态是存储在内存的,存取复杂状态不需要经过序列化和反序列化的过程,性能会比从Bundle高很多。

  2. 将数据,也就是状态,交给ViewModel管理,可以分担Activity的工作,易于解耦,方便后期的维护。

    像Activity和Fragment(后续的文章会提到)之类的组件,主要用于显示界面数据,对用户操作做出响应,进而给予用户UI上的反馈。比如用户按下了一个按钮,按下时按钮颜色变深。

    如果要求Activity也负责从数据库或网络加载数据,这不仅需要在Activity维护加载数据的代码,也需要维护数据加载回来后的一个个异步的数据加载回调,甚至你还得考虑当Activity退出时,异步回调对象的释放工作,这会使Activity类越发膨胀,臃肿。

    给Activity分配过多的责任可能会导致单个类尝试自己处理应用的所有工作,会增加测试和维护的难度。将数据加载和回调的工作交给ViewModel,将数据加载的逻辑工作与Activity的UI显示工作分离,不仅能让逻辑更清晰,同时也可以避免一个类代码量太多,太臃肿,难以维护。

ViewModel的生命周期

ViewModel对象存在的时间范围是获取ViewModel时传递给 ViewModelProvider的Lifecycle 。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失。

比如你在Activity中通过ViewModelProvider获取了一个ViewModel,那么这个lifecycle指的就是Activity的生命周期。

我们一般会在Activity执行onCreate()方法时拿到ViewModel对象,系统可能会在activity的整个生命周期内多次调用onCreate(),比如在旋转设备屏幕时。

**ViewModel存在的时间范围是在首次请求ViewModel对象直到Activity完成并销毁。**在一个生命周期内,多次请求ViewModel对象,获取到的都是同一个ViewModel对象。

ViewModel的简单使用

首先要用ViewModel,需要引入它的库。

// ViewModel and LiveData
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

简单演示下加速器的demo,布局文件如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".viwemodel.ViewModelActivity"><androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/tv_number"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="24sp"android:textColor="#000"android:gravity="center"android:layout_centerInParent="true"/><androidx.appcompat.widget.AppCompatButtonandroid:id="@+id/btn_add"android:layout_width="match_parent"android:layout_height="46dp"android:text="ADD NUMBER"android:layout_below="@+id/tv_number"android:layout_marginTop="20dp"/></RelativeLayout>

布局文件比较简单,就不解释了,接下来看看逻辑代码:

class ViewModelActivity : AppCompatActivity() {private lateinit var vNumber: AppCompatTextViewprivate lateinit var mViewModel: UserModeloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_view_model)val viewModelProvider = ViewModelProvider(this)mViewModel = viewModelProvider.get(UserModel::class.java)vNumber = findViewById(R.id.tv_number)findViewById<AppCompatButton>(R.id.btn_add).setOnClickListener {mViewModel.count++setCounter()}setCounter()}private fun setCounter() {vNumber.text = mViewModel.count.toString()}}

运行,看看效果:

可以看到,activity没有指定configchanges属性,在竖屏和横屏切换时,TextView的状态也是没有丢失的。

ViewModel的介绍就到这里啦,ViewModel还有很多用途,例如在同一个Activity间的两个Fragment之间共享数据。如果配合JetPack库的另一个LiveData使用,它将更加强大。

课外知识提到了几个本文没有涉及的概念,如Fragment,Parcelable序列化等,如果你对安卓还不是很熟悉,对上面提到的一些名词感觉很陌生?不用担心!完全没有关系!这些不影响你理解Activity的生命周期,课外知识只是做个科普,如果你觉得这部分太复杂,你甚至可以跳过这部分内容,没有任何关系。这些知识在工作中用的很少,包括ViewModel这些比较新的组件,或许考虑到代码重构的成本,大厂里一些的老项目里也很少用到,你只需要记住Activity生命周期的经典知识就可以了!

文章内容参考
  1. 安卓开发艺术探索,任玉刚
  2. 第一行代码(第三版),郭霖
  3. 安卓开发者官网

结束的结束

下一篇内容,Activity启动模式~

兄dei,如果觉得我写的还不错,麻烦帮个忙呗

安卓基础知识之Activity篇(一):Activity生命周期相关推荐

  1. 安卓基础知识之View篇(四):View 事件滑动冲突解决方案

    安卓基础知识系列旨在简明扼要地提供面试或工作中常用的基础知识,让对安卓还不太熟悉的小伙伴更快地入门.同时自己在工作中,也没法完全记住所有的基础细节,写这样的系列文章,可以让自己形成一个更完备的知识体系 ...

  2. 安卓基础知识(一) 服务(Service)

    安卓基础知识(一) 服务(Service) 一.基本概念: 1.服务就是长期于后台运行的程序,可以理解为是一个,用于执行长期任务,并且与用户没有交互的组件.每一个服务需要在配置文件AndroidMan ...

  3. 安卓手机来电防火墙_安卓基础知识自动化测试

    安卓基础知识 本章重点探讨AndroidUI自动化测试过程中所涉及到的原理和技术.掌握这些知识是为学习Appium自动化测试框架打下基础. 学习目标 了解API和安卓版本的关系 了解安卓组件,安卓程序 ...

  4. 安卓基础知识-layout布局详解。

    安卓基础知识 1. 目录结构: src存放java源代码. gen存放系统自动生成的配置文件 res存放应用用到的所有资源文件,如图片,布局等等 drawable存放不同分辨率的图片 layout存放 ...

  5. datagrid出现相同两组数据_stata 数据操作基础知识:以一篇论文数据操作为例

    stata 数据操作基础知识:以一篇论文数据操作为例 上节回顾及问题 统计学学习大图景 数据描述 分位数回归 存在的问题: 1.学了就要多使用,哪怕生搬硬套也要多用 2.时间序列的方法,大家可以操作, ...

  6. 计算机数据库管理基本知识,2015年计算机四级考试《数据库技术》基础知识:概念篇...

    2015年计算机四级考试<数据库技术>基础知识:概念篇 信息与数据 1. 信息.物质.能量是组成客观世界并促进社会发展的三大基本要素; 2. 信息(Information)--是客观世界事 ...

  7. 反相畴的基础知识和一篇论文

    校历第十三周计划(11.18-11.24):反相畴的基础知识和一篇论文 上周由于需要尽快和学长交流,因此提前先看了两篇关于反相畴的论文.由于基础知识的匮乏,这周打算补充一些基础知识,主要来源于薄膜生长 ...

  8. 计算机等级考试上网怎么做,计算机基础知识上网设置篇

    计算机基础知识上网设置篇 分类:计算机等级 | 更新时间:2016-07-08| 来源:转载 现在让我们看看如何进行上网的设置. 首先,让我们查看一下在你的Windows95或是Windows98里是 ...

  9. 安卓APP_ Fragment(3)—— Fragment的生命周期

    摘自:安卓APP_ Fragment(3)-- Fragment的生命周期 作者:丶PURSUING 发布时间: 2021-04-16 22:32:12 网址:https://blog.csdn.ne ...

最新文章

  1. iOS 视频捕获系列Swift之AVFoundation(一)
  2. TensorFlow练习9: 生成妹子图(PixelCNN)
  3. Office 365系列之十:批量部署O365客户端
  4. 用一个实际例子理解Docker volume工作原理
  5. html5的FileReader文件读取
  6. 【文档】软件版本发布说明
  7. Asp.net几大内置对象
  8. 微信小程序设置底部导航栏目方法
  9. 搭建ssh框架的步骤
  10. JavaScript中的交互式网页/事件处理
  11. python网络爬虫 抓取金融分析师名单
  12. Linux SElinux
  13. linux下mysql 8 忘记密码
  14. java us ascii_java – 为什么US-ASCII编码接受非US-ASCII字符?
  15. centos freeradius mysql_CentOS 5.7安装FreeRADIUS 1.1.3+MySQL 5.0.77结合RouteOS
  16. oracle数据库实例改名,如何修改数据库实例及数据库名
  17. MongoDB应用记录
  18. 功能测试数据测试之因果图分析方法
  19. 什么是软件危机?它有哪些典型表现?为什么会出现软件危机?
  20. 封装的PHP爬虫类(一) 单量抓取

热门文章

  1. 华为交换机端口不配置access_华为交换机的配置及:access、trunk、hybird端口详解...
  2. 三种命名规则之-----匈牙利命名规则
  3. 电商类网站需要办理icp许可证吗
  4. 这份Redis6.0集群搭建教程,项目肯定用得上
  5. 码分多址CDMA通信
  6. 软件工程师/软件测试工程师
  7. R shiny 关于全球地震分布
  8. 北京2008奥运体育场馆展望
  9. H3C交换tftp上传下载文件
  10. scroll lock键有什么作用?