一、前提概要

《第一行代码—Android》是Android初学者的最佳入门书。全书由浅入深、系统全面地讲解了Android软件开发的方方面面。这本书是2014年8月上市,到现在我写下这篇博客为止,已经过了两年,目前,它还是很畅销。可见,在一批新的android开发者眼中,它还是一本启蒙书的存在。这本书的作者郭霖,Android软件开发工程师。从事Android开发工作,有着丰富的项目实战经验,负责及参与开发过多款移动应用与游戏,对Android系统架构及应用层开发有着深入的理解,活跃于CSDN,在CSDN上发表Android技术相关博文,受到大量好评。

接下来,我会记录关于阅读《第一行代码》的学习见解与体会,内容框架并不是按照书上目录而来,如有不妥错误之处,可以评论指正以及补充。

二、四大组件之活动-Activity

(一)Intent

Intent 是 Android 程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent 一般可被用于启动活动、启动服务、以及发送广播等场景。Intent 的用法大致可以分为两种,显式 Intent 和隐式 Intent,我们先来看一下显式 Intent如何使用。

1.显式Intent

Intent 有多个构造函数的重载,其中一个是 Intent(Context packageContext, Class<?> cls)。这个构造函数接收两个参数,第一个参数 Context 要求提供一个启动活动的上下文,第二个参数 Class 则是指定想要启动的目标活动, 通过这个构造函数就可以构建出 Intent 的 “意图” 。
然后我们应该怎么使用这个 Intent 呢?Activity 类中提供了一个 startActivity()方法, 这个方法是专门用于启动活动的, 它接收一个 Intent参数, 这里我们将构建好的 Intent传入 startActivity()方法就可以启动目标活动了。

Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivity(intent);

2.隐式Intent

相比于显式 Intent,隐式 Intent 则含蓄了许多,它并不明确指出我们想要启动哪一个活动, 而是指定了一系列更为抽象的 action 和 category 等信息, 然后交由系统去分析这个 Intent,并帮我们找出合适的活动去启动。

通过在标签下配置的内容,可以指定当前活动能够响应的 action和 category,打开AndroidManifest.xml,添加如下代码:

<activity android:name=".SecondActivity" >
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

action 标签中我们指明了当前活动可以响应com.example.bin.helloworld.ACTION_START 这个action,而category标签则包含了一些附加信息,更精确地指明了当前的活动能够响应的Intent 中还可能带有的category。只有actioncategory中的内容同时能够匹配上Intent 中指定的action 和category 时,这个活动才能响应该Intent。

每个Intent 中只能指定一个action,但却能指定多个category。

Intent intent = new Intent("com.example.activitytest.ACTION_START");
startActivity(intent);

android.intent.category.DEFAULT 是一种默认category, 在调用startActivity()方法的时候会自动将这个category 添加到Intent 中。可以调用Intent 中的addCategory()方法来添加一个category

3.更多隐式Intent用法

使用隐式Intent,我们不仅可以启动自己程序内的活动,还可以启动其他程序的活动,这使得Android 多个应用程序之间的功能共享成为了可能。比如调用系统的浏览器打开网页。

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
startActivity(intent);

首先指定了 Intent 的 action 是 Intent.ACTION_VIEW, 这是一个 Android 系统内置的动作,其常量值为android.intent.action.VIEW。然后通过 Uri.parse()方法,将一个网址字符串解析成一个 Uri 对象,再调用 Intent 的 setData()方法将这个 Uri 对象传递进去。我们还可以在<intent-filter>标签中再配置一个<data>标签,用于更精确地指定当前活动能够响应什么类型的数据。<data>标签中主要可以配置以下内容。

  1. android:scheme
    用于指定数据的协议部分,如上例中的 http 部分。
  2. android:host
    用于指定数据的主机名部分,如上例中的 www.baidu.com 部分。
  3. android:port
    用于指定数据的端口部分,一般紧随在主机名之后。
  4. android:path
    用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容。
  5. android:mimeType

除了http 协议外,我们还可以指定很多其他协议,比如geo 表示显示地理位置、tel 表示拨打电话。调用系统拨号界面。

 Intent intent = new Intent(Intent.ACTION_DIAL);intent.setData(Uri.parse("tel:10086"));startActivity(intent);

4.向下一个活动传递数据

Intent 中提供了一系列putExtra()方法的重载,可以把我们想要传递的数据暂存在Intent 中,启动了另一个活动后,只需要把这些数据再从Intent 中取出就可以了。putExtra()方法接收两个参数,第一个参数是键,用于后面从Intent中取值,第二个参数才是真正要传递的数据。

 String data = "I am from FirstActivity";Intent intent = new Intent(FirstActivity.this,SecondActivity.class);intent.putExtra("extra_data",data);
startActivity(intent);
 //获得传递过来的意图Intent intent = getIntent(); //获得意图中的信息,getStringExtra获得字符串类型,getBooleanExtra获得布尔类型。String data = intent.getStringExtra("extra_data");

另外,通过intent来传递对象有两种方式Serializable方式和Parcelable方式,这里暂且不提,有兴趣得可以Googel用法。

5.返回数据给上一个活动

Activity 中还有一个 startActivityForResult()方法也是用于启动活动的,但这个方法期望在活动销毁的时候能够返回一个结果给上一个活动。毫无疑问,这就是我们所需要的。
startActivityForResult()方法接收两个参数,第一个参数还是 Intent,第二个参数是请求码,用于在之后的回调中判断数据的来源。

Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
//第一个参数还是Intent
//第二个参数是请求码,用于在之后的回调中判断数据的来源
startActivityForResult(intent,1);
Intent intent = new Intent();
intent.putExtra("data_return","hello FirstActivity");
//setResult()方法接收两个参数
//第一个参数用于向上一个活动返回处理结果, 一般只使用RESULT_OK 或 RESULT_CANCELED(按Back键后返回)
//第二个参数则是把带有数据的Intent 传递回去,
setResult(RESULT_OK,intent);
finish();

由于我们是使用 startActivityForResult()方法来启SecondActivity 的,在 SecondActivity被销毁之后会回调上一个活动的onActivityResult()方法,因此我们需要在 FirstActivity 中重写这个方法来得到返回的数据:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
//第一个参数requestCode,即我们在启动活动时传入的请求码。
//第二个参数resultCode,即我们在返回数据时传入的处理结果。
//第三个参数data,即携带着返回数据的Intentswitch (requestCode) {case 1:if (resultCode == RESULT_OK) {String returnData = data.getStringExtra("data_return");Toast.makeText(FirstActivity.this, "返回的值:" + returnData, Toast.LENGTH_SHORT).show();}break;default:}
}

如果用户点击Back键,也需要返回值,我们可以重写SecondActivity中的onBackPressed()方法。

@Override
public void onBackPressed() {Intent intent = new Intent();intent.putExtra("data_return", "Hello FirstActivity");setResult(RESULT_OK, intent);finish();
}

也可以重写onKeyDown()方法

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {// 是否触发按键为back键if (keyCode == KeyEvent.KEYCODE_BACK) {Intent intent = new Intent();intent.putExtra("data_return", "Hello FirstActivity");setResult(RESULT_OK, intent);this.finish();return true;}else {return super.onKeyDown(keyCode, event);}
}

(二)活动的生命周期

1.返回栈
Android 中的活动是可以层叠的。我们每启动一个新的活动,就会覆盖在原活动之上,然后点击 Back 键会销毁最上面的活动,下面的一个活动就会重新显示出来。其实 Android 是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动的集合,这个栈也被称作返回栈(Back Stack) 。栈是一种后进先出的数据结构,在默认情况下,每当我们启动了一个新的活动,它会在返回栈中入栈,并处于栈顶的位置。而每当我们按下 Back 键或调用 finish()方法去销毁一个活动时,处于栈顶的活动会出栈,这时前一个入栈的活动就会重新处于栈顶的位置。系统总是会显示处于栈顶的活动给用户。

2.活动状态

每个活动在其生命周期中最多可能会有四种状态。

1.运行状态
当一个活动位于返回栈的栈顶时,这时活动就处于运行状态。系统最不愿意回收的就是处于运行状态的活动,因为这会带来非常差的用户体验。

2.暂停状态
当一个活动不再处于栈顶位置,但仍然可见时,这时活动就进入了暂停状态。你可能会觉得既然活动已经不在栈顶了,还怎么会可见呢?这是因为并不是每一个活动都会占满整个屏幕的,比如对话框形式的活动只会占用屏幕中间的部分区域,你很快就会在后面看到这种活动。处于暂停状态的活动仍然是完全存活着的,系统也不愿意去回收这种活动(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种活动。

3.停止状态
当一个活动不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种活动保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的活动有可能会被系统回收。

4.销毁状态
当一个活动从返回栈中移除后就变成了销毁状态。系统会最倾向于回收处于这种状态的活动,从而保证手机的内存充足。

3.活动的生存期

Activity 类中定义了七个回调方法,覆盖了活动生命周期的每一个环节,下面我来一一介绍下这七个方法。

1.onCreate()
这个方法你已经看到过很多次了,每个活动中我们都重写了这个方法,它会在活动第一次被创建的时候调用。你应该在这个方法中完成活动的初始化操作,比如说加载布局、绑定事件等。

2.onStart()
这个方法在活动由不可见变为可见的时候调用。

3.onResume()
这个方法在活动准备好和用户进行交互的时候调用。此时的活动一定位于返回栈的栈顶,并且处于运行状态。

4.onPause()
这个方法在系统准备去启动或者恢复另一个活动的时候调用。我们通常会在这个方法中将一些消耗CPU 的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用。

5.onStop()
这个方法在活动完全不可见的时候调用。它和onPause()方法的主要区别在于,如果启动的新活动是一个对话框式的活动,那么onPause()方法会得到执行,而onStop()方法并不会执行。

6.onDestroy()
这个方法在活动被销毁之前调用,之后活动的状态将变为销毁状态。

7.onRestart()
这个方法在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了。

4.活动被回收了怎么办

打个比方,MainActivity 中有一个文本输入框,现在你输入了一段文字,然后启动NormalActivity,这时MainActivity 由于系统内存不足被回收掉,过了一会你又点击了Back 键回到MainActivity,你会发现刚刚输入的文字全部都没了,因为MainActivity 被重新创建了。
Activity 中还提供了一个onSaveInstanceState()回调方法,这个方法会保证一定在活动被回收之前调用。

@Override
protected void onSaveInstanceState(Bundle outState) {super.onSaveInstanceState(outState);String tempData = "Something you just typed";outState.putString("data_key", tempData);
}

onSaveInstanceState()方法会携带一个Bundle 类型的参数,Bundle 提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整型数据,以此类推。同样可以使用putParcelable()putSerializable()保存对象。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle 中取值,第二个参数是真正要保存的内容。

恢复数据,使用的onCreate()方法有一个Bundle 类型的参数。这个参数在一般情况下都是null,但是当活动被系统回收之前有通过onSaveInstanceState()方法来保存数据的话,这个参数就会带有之前所保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可。修改onCreate()方法,如下所示:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate");
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
if (savedInstanceState != null) {
String tempData = savedInstanceState.getString("data_key");
Log.d(TAG, tempData);
}
……
}

Intent 还可以结合Bundle 一起用于传递数据的,首先可以把需要传递的数据都保存在Bundle 对象中,然后再将Bundle 对象存放在Intent 里。到了目标活动之后先从Intent 中取出Bundle,再从Bundle中一一取出数据。


(三)活动的启动模式

启动模式一共有四种,分别是 standard、singleTop、singleTask 和 singleInstance , 可 以 在 AndroidManifest.xml 中 通 过 给 <activity> 标 签 指 定android:launchMode 属性来选择启动模式。

standard,默认启动模式,系统不管此 Activity 是否已经在返回栈中存在,每次启动 Activity 都会创建该 Activity 的一个新的实例。

这里写图片描述

singleTop,在启动 Activity 时,如果发现返回栈的栈顶是该 Activity,则直接使用她,不会再创建新的实例;如果栈顶不是该 Activity,则会创建新的实例。

这里写图片描述

singleTask,在启动 Activity 时,如果找到返回栈中已经存在该 Activity,则直接使用,并将这个 Activity 之上的所有 Activity 全部出栈;如果没找到,则会创建新的实例。

这里写图片描述

singleInstance,会启用一个新的返回栈来管理这个活动,并保证不再有其他 Activity 的实例进入。

这里写图片描述

三、四大组件之广播接收者-Broadcast Receiver

(一)系统广播

Android 内置了很多系统级别的广播,通过监听这些广播可以得到相关的系统状态信息。例如:

  • 系统启动完成
  • 打开、关闭飞行模式
  • 电量低
  • 内存不足

1.动态注册

新建一个类,让它继承自 BroadcastReceiver,并重写父类的onReceive()方法就行了。 这样当有广播到来时, onReceive()方法就会得到执行,具体的逻辑就可以在这个方法中处理。

class NetworkChangeReceiver extends BroadcastReceiver{@Overridepublic void onReceive(Context context, Intent intent){Toast.makeText(context, "network changes", Toast.LENGTH_SHORT).show();}
}

在 onCreate() 方法中注册,代码如下:

private IntentFilter intentFilter;
private NetworkChangeReceiver networkChangeReceiver;@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);intentFilter = new IntentFilter();intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");networkChangeReceiver = new NetworkChangeReceiver();registerReceiver(networkChangeReceiver, intentFilter);
}

在 onDestroy() 方法中取消注册,代码如下:

@Override
protected void onDestroy(){super.onDestroy();unregisterReceiver(networkChangeReceiver);
}

运行一下,会发现程序启动时会弹出一个提示;按 Home 回到主界面,并尝试开关网络,会发现有提示弹出。

2.静态注册
这里我们准备让程序接收一条开机广播, 当收到这条广播时就可以在 onReceive()方法里执行相应的逻辑,从而实现开机启动的功能。新建一个 BootCompleteReceiver 继承自BroadcastReceiver,代码如下所示:

public class BootCompleteReceiver extends BroadcastReceiver{@Overridepublic void onReceive(Context context, Intent intent){Toast.makeText(context, "Boot complete", Toast.LENGTH_SHORT).show();}
}

修改 AndroidManifest.xml 文件,代码如下所示:

<receiver android:name=".MainActivity$BootCompleteReceiver"><intent-filter><action android:name="android.intent.action.BOOT_COMPLETED"/></intent-filter>
</receiver>

监听系统开机广播需要权限,在 AndroidManifest.xml 中添加权限声明,代码如下:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"></uses-permission>

(二)自定义广播

1.标准广播

在发送广播之前,我们还是需要先定义一个广播接收器来准备接收此广播,新建一个 MyBroadcastReceiver继承自BroadcastReceiver,代码如下所示:

public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in MyBroadcastReceiver",
Toast.LENGTH_SHORT).show();
}
}

然后在 AndroidManifest.xml 中对这个广播接收器进行注册:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest"
android:versionCode="1"
android:versionName="1.0" >
……
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
……
<receiver android:name=".MyBroadcastReceiver">
<intent-filter>
<action android:name="com.example.broadcasttest. MY_BROADCAST"/>
</intent-filter>
</receiver>
</application>
</manifest>

MyBroadcastReceiver 接收一条com.example.broadcasttest.MY_BROADCAST 的广播。给 MainActivity 增加一个按钮,并在 onCreate() 方法中添加点击事件,代码如下:

public class MainActivity extends Activity {
……
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcasttest.
MY_BROADCAST");
sendBroadcast(intent);
}
});
……
}
……
}

此时发出去的广播就是一条标准广播。

2.有序广播

定义一个按钮,设置其点击事件,发送一个有序广播。

 Intent intent = new  Intent();//设置intent的动作为com.example.broadcast,可以任意定义intent.setAction("com.example.broadcast");//发送无序广播//第一个参数:intent//第二个参数:String类型的接收者权限//第三个参数:BroadcastReceiver 指定的接收者//第四个参数:Handler scheduler//第五个参数:int 此次广播的标记 //第六个参数:String 初始数据//第七个参数:Bundle 往Intent中添加的额外数据sendOrderedBroadcast(intent, null, null, null, "这是初始数据", );

定义多个广播接收者,来接收这个广播事件。通过Toast的打印判断是否收到广播。

public class MyReceiver1 extends BroadcastReceiver {public MyReceiver1() {}@Overridepublic void onReceive(Context context, Intent intent) {//获取广播中的数据(即得到 "这是初始数据" 字符串)String message = getResultData();Toast.makeText(context ,message ,Toast.LENGTH_SHORT).show();//修改数据setResultData("这是修改后的数据");}
}
public class MyReceiver2 extends BroadcastReceiver {public MyReceiver2() {}@Overridepublic void onReceive(Context context, Intent intent) {String message = getResultData();Toast.makeText(context ,message ,Toast.LENGTH_SHORT).show();//终止广播abortBroadcast();}
}
public class MyReceiver3 extends BroadcastReceiver {public MyReceiver3() {}@Overridepublic void onReceive(Context context, Intent intent) {String message = getResultData();Toast.makeText(context ,message ,Toast.LENGTH_SHORT).show();}
}

在Manifest.xml中配置该接收者。
并设置优先级:MyReceiver1>MyReceiver2>MyReceiver3。

<!-- 优先级相等的话,写在前面的receiver的优先级大于后面的 -->
<receiver
            android:name=".MyReceiver1" ><!-- 定义广播的优先级 --><intent-filter android:priority="1000">                <!-- 动作设置为发送的广播动作 --><action android:name="com.example.broadcast"/></intent-filter>
</receiver>
<receiver android:name=".MyReceiver2" ><!-- 定义广播的优先级 --><intent-filter  android:priority="0"><!-- 动作设置为发送的广播动作 --><action android:name="com.example.broadcast"/></intent-filter>
</receiver>
<receiver android:name=".MyReceiver3" ><!-- 定义广播的优先级 --><intent-filter  android:priority="-1000"><!-- 动作设置为发送的广播动作 --><action android:name="com.example.broadcast"/></intent-filter>
</receiver>

运行结果:MyReceiver1得到广播数据后打印“这是初始数据”,MyReceiver2接收到广播数据打印“这是修改后的数据”,MyReceiver3没有打印。

(三)本地广播

定义一个 LocalReceiver ,代码如下:

public static class LocalReceiver extends BroadcastReceiver{@Overridepublic void onReceive(Context context, Intent intent){Toast.makeText(context, "received local broadcast", Toast.LENGTH_SHORT).show();}
}

使用 LocalBroadcastManager 来注册接收器、发送广播,代码如下:

private IntentFilter intentFilter;
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);localBroadcastManager = localBroadcastManager.getInstance(this);Button button = (Button)findViewById(R.id.button);button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");localBroadcastManager.sendBroadcast(intent);}});intentFilter = new IntentFilter();intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");localReceiver = new LocalReceiver();localBroadcastManager.registerReceiver(localReceiver, intentFilter);
}

记住取消注册,代码如下:

@Override
protected void onDestroy(){super.onDestroy();localBroadcastManager.unregisterReceiver(localReceiver);
}

本地广播无法通过静态注册的方式接收。

四、四大组件之服务-Service

(一)服务的基本用法

Service 是一个可以在后台执行长时间运行操作而不使用用户界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。 此外,组件可以绑定到服务,以与之进行交互,甚至是执行进程间通信 (IPC)。 例如,服务可以处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序交互,而所有这一切均可在后台进行。

新建一个Android项目,项目名就叫ServiceTest,然后新建一个MyService继承自Service,并重写父类的onCreate()、onStartCommand()和onDestroy()方法,如下所示:

public class MyService extends Service {  public static final String TAG = "MyService";  @Override  public void onCreate() {  super.onCreate();  Log.d(TAG, "onCreate() executed");  }  @Override  public int onStartCommand(Intent intent, int flags, int startId) {  Log.d(TAG, "onStartCommand() executed");  return super.onStartCommand(intent, flags, startId);  }  @Override  public void onDestroy() {  super.onDestroy();  Log.d(TAG, "onDestroy() executed");  }  @Override  public IBinder onBind(Intent intent) {  return null;  }  }  

可以看到,这里我们又重写了 onCreate()、onStartCommand()和 onDestroy()这三个方法,它们是每个服务中最常用到的三个方法了。其中 onCreate()方法会在服务创建的时候调用,onStartCommand()方法会在每次服务启动的时候调用,onDestroy()方法会在服务销毁的时候调用。

通常情况下,如果我们希望服务一旦启动就立刻去执行某个动作,就可以将逻辑写在onStartCommand()方法里。而当服务销毁时,我们又应该在 onDestroy()方法中去回收那些不再使用的资源。

另外需要注意,每一个服务都需要在 AndroidManifest.xml文件中进行注册才能生效,于是我们还应该修改 AndroidManifest.xml文件,代码如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.servicetest"
android:versionCode="1"
android:versionName="1.0" >
……
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
……
<service android:name=".MyService" >
</service>
</application>
</manifest>

我们在布局文件中加入了两个按钮,一个用于启动Service,一个用于停止Service。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  android:layout_width="match_parent"  android:layout_height="match_parent"  android:orientation="vertical" >  <Button  android:id="@+id/start_service"  android:layout_width="match_parent"  android:layout_height="wrap_content"  android:text="Start Service" />  <Button  android:id="@+id/stop_service"  android:layout_width="match_parent"  android:layout_height="wrap_content"  android:text="Stop Service" />  </LinearLayout>  

然后打开或新建MainActivity作为程序的主Activity,在里面加入启动Service和停止Service的逻辑,代码如下所示:

public class MainActivity extends Activity implements OnClickListener {  private Button startService;  private Button stopService;  @Override  protected void onCreate(Bundle savedInstanceState) {  super.onCreate(savedInstanceState);  setContentView(R.layout.activity_main);  startService = (Button) findViewById(R.id.start_service);  stopService = (Button) findViewById(R.id.stop_service);  startService.setOnClickListener(this);  stopService.setOnClickListener(this);  }  @Override  public void onClick(View v) {  switch (v.getId()) {  case R.id.start_service:  Intent startIntent = new Intent(this, MyService.class);  startService(startIntent);  break;  case R.id.stop_service:  Intent stopIntent = new Intent(this, MyService.class);  stopService(stopIntent);  break;  default:  break;  }  }  }  

活动和服务进行通信

观察MyService中的代码,你会发现一直有一个onBind()方法我们都没有使用到,这个方法其实就是用于和Activity建立关联的,修改MyService中的代码,如下所示:

public class MyService extends Service {  public static final String TAG = "MyService";  private MyBinder mBinder = new MyBinder();  @Override  public void onCreate() {  super.onCreate();  Log.d(TAG, "onCreate() executed");  }  @Override  public int onStartCommand(Intent intent, int flags, int startId) {  Log.d(TAG, "onStartCommand() executed");  return super.onStartCommand(intent, flags, startId);  }  @Override  public void onDestroy() {  super.onDestroy();  Log.d(TAG, "onDestroy() executed");  }  @Override  public IBinder onBind(Intent intent) {  return mBinder;  }  class MyBinder extends Binder {  public void startDownload() {  Log.d("TAG", "startDownload() executed");  // 执行具体的下载任务  }  }  }  

可以看到,这里我们新建了一个 DownloadBinder 类,并让它继承自 Binder,然后在它的内部提供了开始下载以及查看下载进度的方法。当然这只是两个模拟方法,并没有实现真正的功能,我们在这两个方法中分别打印了一行日志。
然后修改activity_main.xml中的代码,在布局文件中添加用于绑定Service和取消绑定Service的按钮:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
……
<Button
android:id="@+id/bind_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Bind Service" />
<Button
android:id="@+id/unbind_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Unbind Service" />
</LinearLayout>

这两个按钮分别是用于绑定服务和取消绑定服务的,那到底谁需要去和服务绑定呢?当然就是活动了。 当一个活动和服务绑定了之后, 就可以调用该服务里的 Binder 提供的方法了。
修改 MainActivity 中的代码,如下所示:

public class MainActivity extends Activity implements OnClickListener {
private Button startService;
private Button stopService;
private Button bindService;
private Button unbindService;
private MyService.DownloadBinder downloadBinder;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder = (MyService.DownloadBinder) service;
downloadBinder.startDownload();
downloadBinder.getProgress();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
……
bindService = (Button) findViewById(R.id.bind_service);
unbindService = (Button) findViewById(R.id.unbind_service);
bindService.setOnClickListener(this);
unbindService.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
……
case R.id.bind_service:
Intent bindIntent = new Intent(this, MyService.class);
bindService(bindIntent, connection, BIND_AUTO_CREATE); // 绑定服务
break;
case R.id.unbind_service:
unbindService(connection); // 解绑服务
break;
default:
break;
}
}
}

现在活动和服务其实还没进行绑定,这个功能是在 Bind Service 按钮的点击事件里完成的。可以看到,这里我们仍然是构建出了一个 Intent 对象,然后调用 bindService()方法将 MainActivity 和 MyService 进行绑定。bindService()方法接收三个参数,第一个参数就是刚刚构建出的 Intent 对象,第二个参数是前面创建出的 ServiceConnection 的实例,第三个参数则是一个标志位,这里传入 BIND_AUTO_CREATE 表示在活动和服务进行绑定后自动创建服务。这会使得 MyService 中的 onCreate()方法得到执行,但 onStartCommand()方法不会执行。

如何销毁Service
在Service的基本用法这一部分,我们介绍了销毁Service最简单的一种情况,点击Start Service按钮启动Service,再点击Stop Service按钮停止Service,这样MyService就被销毁了。

那么如果我们是点击的Bind Service按钮呢?由于在绑定Service的时候指定的标志位是BIND_AUTO_CREATE,说明点击Bind Service按钮的时候Service也会被创建,这时应该怎么销毁Service呢?其实也很简单,点击一下Unbind Service按钮,将Activity和Service的关联解除就可以了。

以上这两种销毁的方式都很好理解。那么如果我们既点击了Start Service按钮,又点击了Bind Service按钮会怎么样呢?这个时候你会发现,不管你是单独点击Stop Service按钮还是Unbind Service按钮,Service都不会被销毁,必要将两个按钮都点击一下,Service才会被销毁。也就是说,点击Stop Service按钮只会让Service停止,点击Unbind Service按钮只会让Service和Activity解除关联,一个Service必须要在既没有和任何Activity关联又处理停止状态的时候才会被销毁。


(二)服务的生命周期
与 Activity 类似,服务也拥有生命周期回调方法,您可以实现这些方法来监控服务状态的变化并适时执行工作。

public class ExampleService extends Service {int mStartMode;       // indicates how to behave if the service is killedIBinder mBinder;      // interface for clients that bindboolean mAllowRebind; // indicates whether onRebind should be used@Overridepublic void onCreate() {// The service is being created}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {// The service is starting, due to a call to startService()return mStartMode;}@Overridepublic IBinder onBind(Intent intent) {// A client is binding to the service with bindService()return mBinder;}@Overridepublic boolean onUnbind(Intent intent) {// All clients have unbound with unbindService()return mAllowRebind;}@Overridepublic void onRebind(Intent intent) {// A client is binding to the service with bindService(),// after onUnbind() has already been called}@Overridepublic void onDestroy() {// The service is no longer used and is being destroyed}
}

这里写图片描述

服务的整个生命周期从调用 onCreate() 开始起,到 onDestroy() 返回时结束。与 Activity 类似,服务也在 onCreate() 中完成初始设置,并在 onDestroy() 中释放所有剩余资源。例如,音乐播放服务可以在 onCreate() 中创建用于播放音乐的线程,然后在 onDestroy() 中停止该线程。无论服务是通过 startService() 还是 bindService() 创建,都会为所有服务调用 onCreate()onDestroy() 方法。

服务的有效生命周期从调用 onStartCommand()onBind() 方法开始。每种方法均有 Intent 对象,该对象分别传递到 startService()bindService()

对于启动服务,有效生命周期与整个生命周期同时结束(即便是在 onStartCommand() 返回之后,服务仍然处于活动状态)。对于绑定服务,有效生命周期在 onUnbind() 返回时结束。

五、四大组件之内容提供者-Content Provider

(一)ContentResolver 的基本用法

内容提供者是android应用程序的基本构建块之一,它们封装数据并将封装的数据通过单一的ContentResolver接口提供给应用程序。当你需要在多个应用之间共享数据的时候就需要用到内容提供者。例如,手机中的联系人数据会被多个应用所用到所以必须要用内容提供者存储起来。如果你不需要在多个应用之间共享数据,你可以使用一个数据库,直接通过SQLite数据库。 当通过content resolver发送一个请求时,系统会检查给定的URI并把请求传给有注册授权的ContentproviderUriMatcher类有助于解析uri。

下面activity文件中包含每个基础的生命周期方法。我们添加了两个新的方法,onClickAddName() 和 onClickRetrieveStudents() 来让应用程序处理用户交互。

public class MainActivity extends Activity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {getMenuInflater().inflate(R.menu.menu_main, menu);return true;}public void onClickAddName(View view) {// Add a new student recordContentValues values = new ContentValues();values.put(StudentsProvider.NAME,((EditText)findViewById(R.id.editText2)).getText().toString());values.put(StudentsProvider.GRADE,((EditText)findViewById(R.id.editText3)).getText().toString());Uri uri = getContentResolver().insert(StudentsProvider.CONTENT_URI, values);Toast.makeText(getBaseContext(),uri.toString(), Toast.LENGTH_LONG).show();}public void onClickRetrieveStudents(View view) {// Retrieve student recordsString URL = "content://com.example.provider.College/students";Uri students = Uri.parse(URL);Cursor c = managedQuery(students, null, null, null, "name");if (c.moveToFirst()) {do{Toast.makeText(this,c.getString(c.getColumnIndex(StudentsProvider._ID)) +", " +  c.getString(c.getColumnIndex( StudentsProvider.NAME)) +", " + c.getString(c.getColumnIndex( StudentsProvider.GRADE)),Toast.LENGTH_SHORT).show();} while (c.moveToNext());}}
}

在新建包contentprovider下创建新的文件StudentsProvider.java。以下是StudentsProvider.java的内容。

public class StudentsProvider extends ContentProvider {static final String PROVIDER_NAME = "com.example.provider.College";static final String URL = "content://" + PROVIDER_NAME + "/students";static final Uri CONTENT_URI = Uri.parse(URL);static final String _ID = "_id";static final String NAME = "name";static final String GRADE = "grade";private static HashMap<String, String> STUDENTS_PROJECTION_MAP;static final int STUDENTS = 1;static final int STUDENT_ID = 2;static final UriMatcher uriMatcher;static{uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);uriMatcher.addURI(PROVIDER_NAME, "students", STUDENTS);uriMatcher.addURI(PROVIDER_NAME, "students/#", STUDENT_ID);}/*** 数据库特定常量声明*/private SQLiteDatabase db;static final String DATABASE_NAME = "College";static final String STUDENTS_TABLE_NAME = "students";static final int DATABASE_VERSION = 1;static final String CREATE_DB_TABLE =" CREATE TABLE " + STUDENTS_TABLE_NAME +" (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +" name TEXT NOT NULL, " +" grade TEXT NOT NULL);";/*** 创建和管理提供者内部数据源的帮助类.*/private static class DatabaseHelper extends SQLiteOpenHelper {DatabaseHelper(Context context){super(context, DATABASE_NAME, null, DATABASE_VERSION);}@Overridepublic void onCreate(SQLiteDatabase db){db.execSQL(CREATE_DB_TABLE);}@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {db.execSQL("DROP TABLE IF EXISTS " +  STUDENTS_TABLE_NAME);onCreate(db);}}@Overridepublic boolean onCreate() {Context context = getContext();DatabaseHelper dbHelper = new DatabaseHelper(context);/*** 如果不存在,则创建一个可写的数据库。*/db = dbHelper.getWritableDatabase();return (db == null)? false:true;}@Overridepublic Uri insert(Uri uri, ContentValues values) {/*** 添加新学生记录*/long rowID = db.insert( STUDENTS_TABLE_NAME, "", values);/*** 如果记录添加成功*/if (rowID > 0){Uri _uri = ContentUris.withAppendedId(CONTENT_URI, rowID);getContext().getContentResolver().notifyChange(_uri, null);return _uri;}throw new SQLException("Failed to add a record into " + uri);}@Overridepublic Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder) {SQLiteQueryBuilder qb = new SQLiteQueryBuilder();qb.setTables(STUDENTS_TABLE_NAME);switch (uriMatcher.match(uri)) {case STUDENTS:qb.setProjectionMap(STUDENTS_PROJECTION_MAP);break;case STUDENT_ID:qb.appendWhere( _ID + "=" + uri.getPathSegments().get(1));break;default:throw new IllegalArgumentException("Unknown URI " + uri);}if (sortOrder == null || sortOrder == ""){/*** 默认按照学生姓名排序*/sortOrder = NAME;}Cursor c = qb.query(db, projection, selection, selectionArgs,null, null, sortOrder);/*** 注册内容URI变化的监听器*/c.setNotificationUri(getContext().getContentResolver(), uri);return c;}@Overridepublic int delete(Uri uri, String selection, String[] selectionArgs) {int count = 0;switch (uriMatcher.match(uri)){case STUDENTS:count = db.delete(STUDENTS_TABLE_NAME, selection, selectionArgs);break;case STUDENT_ID:String id = uri.getPathSegments().get(1);count = db.delete( STUDENTS_TABLE_NAME, _ID +  " = " + id +(!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""), selectionArgs);break;default:throw new IllegalArgumentException("Unknown URI " + uri);}getContext().getContentResolver().notifyChange(uri, null);return count;}@Overridepublic int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {int count = 0;switch (uriMatcher.match(uri)){case STUDENTS:count = db.update(STUDENTS_TABLE_NAME, values, selection, selectionArgs);break;case STUDENT_ID:count = db.update(STUDENTS_TABLE_NAME, values, _ID + " = " + uri.getPathSegments().get(1) +(!TextUtils.isEmpty(selection) ? " AND (" +selection + ')' : ""), selectionArgs);break;default:throw new IllegalArgumentException("Unknown URI " + uri );}getContext().getContentResolver().notifyChange(uri, null);return count;}@Overridepublic String getType(Uri uri) {switch (uriMatcher.match(uri)){/*** 获取所有学生记录*/case STUDENTS:return "vnd.android.cursor.dir/vnd.example.students";/*** 获取一个特定的学生*/case STUDENT_ID:return "vnd.android.cursor.item/vnd.example.students";default:throw new IllegalArgumentException("Unsupported URI: " + uri);}}
}

以下是修改后的AndroidManifest.xml文件。这里添加了<provider.../>标签来包含我们的内容提供者:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="cn.uprogrammer.contentprovider"android:versionCode="1"android:versionName="1.0" ><uses-sdk
        android:minSdkVersion="8"android:targetSdkVersion="22" /><application
        android:allowBackup="true"android:icon="@drawable/ic_launcher"android:label="@string/app_name"android:theme="@style/AppTheme" ><activity
            android:name="cn.uprogrammer.contentprovider.MainActivity"android:label="@string/app_name" ><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><provider android:name="StudentsProvider"android:authorities="com.example.provider.College" ></provider></application></manifest>

下面是res/layout/activity_main.xml文件的内容:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"android:paddingRight="@dimen/activity_horizontal_margin"android:paddingTop="@dimen/activity_vertical_margin"android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"><TextViewandroid:id="@+id/textView1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="内容提供者实例"android:layout_alignParentTop="true"android:layout_centerHorizontal="true"android:textSize="30dp" /><TextViewandroid:id="@+id/textView2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="www.uprogrammer.cn"android:textColor="#ff87ff09"android:textSize="30dp"android:layout_below="@+id/textView1"android:layout_centerHorizontal="true" /><ImageButtonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:id="@+id/imageButton"android:src="@drawable/ic_launcher"android:layout_below="@+id/textView2"android:layout_centerHorizontal="true" /><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:id="@+id/button2"android:text="添加"android:layout_below="@+id/editText3"android:layout_alignRight="@+id/textView2"android:layout_alignEnd="@+id/textView2"android:layout_alignLeft="@+id/textView2"android:layout_alignStart="@+id/textView2"android:onClick="onClickAddName"/><EditTextandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:id="@+id/editText"android:layout_below="@+id/imageButton"android:layout_alignRight="@+id/imageButton"android:layout_alignEnd="@+id/imageButton" /><EditTextandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:id="@+id/editText2"android:layout_alignTop="@+id/editText"android:layout_alignLeft="@+id/textView1"android:layout_alignStart="@+id/textView1"android:layout_alignRight="@+id/textView1"android:layout_alignEnd="@+id/textView1"android:hint="姓名"android:textColorHint="@android:color/holo_blue_light" /><EditTextandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:id="@+id/editText3"android:layout_below="@+id/editText"android:layout_alignLeft="@+id/editText2"android:layout_alignStart="@+id/editText2"android:layout_alignRight="@+id/editText2"android:layout_alignEnd="@+id/editText2"android:hint="年级"android:textColorHint="@android:color/holo_blue_bright" /><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="查询"android:id="@+id/button"android:layout_below="@+id/button2"android:layout_alignRight="@+id/editText3"android:layout_alignEnd="@+id/editText3"android:layout_alignLeft="@+id/button2"android:layout_alignStart="@+id/button2"android:onClick="onClickRetrieveStudents"/></RelativeLayout>

一旦你完成数据库记录的添加,是时候向内容提供者要求给回这些记录。点击”查询”按钮,这将通过实现的 query() 方法来获取并显示所有的数据记录。

你可以在 MainActivity.java 中提供回调方法,来编写更新和删除的操作,并修改用户界面来添加更新和删除操作。

你可以通过这种方式使用已有的内容提供者,如通讯录。你也可以通过这种方式来开发一个优秀的面向数据库的应用,你可以像上面介绍的实例那样来执行素有的数据库操作,如读、写、更新和删除。

以上内容或多或少的介绍了android的四大组件,后续会继续探索《第一行代码》的精彩内容,敬请期待…

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

相关文章

  1. SeLU 激活函数

    SeLU最近arXiv出来一篇新文章,102页的Paper,90+页的附录,密密麻麻的公式证明,已经和我暂时绝缘了, 心疼审稿人1秒。虽然看不懂,并不影响我们使用,总结起来就是下图:代码放到Tensorflow里面就下面几行:def selu(x):with ops.name_scope(elu) as scope:alpha = 1.673263…...

    2024/3/28 21:02:56
  2. [用友问题] 系统存在的问题

    存在问题 1. 生产计划部和销售部,可以非常方便的查看到订单内每一个物料对应库存量,而不需要逐条的去查找 2. 在外贸销售中,仓库盘点需要按照合同不同盘点存货,即相同的存货要按照合同的不同而分别盘点出来。 已采用自由项来管理外贸合同号,但是这样处理对一些同时也是…...

    2024/3/7 22:18:11
  3. 片段中未调用onActivityResult

    相机活动返回时,承载此片段的活动将调用其onActivityResult 。 我的片段通过为相机发送照片的意图启动了一个结果活动。 图片应用程序可以正常加载,拍照并返回。 但是,永远不会点击onActivityResult 。 我已经设置了断点,但是什么也没有触发。 片段可以具有onActivityResu…...

    2024/3/7 22:18:10
  4. 信用卡年轻消费群体数据分析和洞察报告

    文 | 帆软数据应用研究院 船长本文源自2017年贵阳数博会《大数据科技引擎助理金融创新》论坛中百融金服林佳琳女士的报告和交流。信用卡年轻人群,是消费金融的主流人群,针对他们的数据分析和洞察让我们信贷业务决策更科学。数据分析和洞察报告背景为什么会做这样的报告?我们…...

    2024/3/7 22:18:09
  5. functools 中的 partial的作用

    看到有一句代码如下:from functools import partial my_dense_layer = partial(tf.layers.dense,activation=tf.nn.elu,kernel_initializer=he_init,kernel_regularizer=l2_regularizer)hidden1 = my_dense_layer(X, n_hidden1) hidden2 = my_dense_layer(hidden1, n_hidden2) …...

    2024/3/29 7:44:24
  6. Elasticsearch史上最全最常用工具清单

    作者:铭毅天下题记工欲善其事必先利其器,ELK Stack的学习和实战更是如此,特将工作中用到的“高效”工具分享给大家。希望能借助“工具”提高开发、运维效率!工具分类概览基础类工具1、Head插件1)功能概述:ES集群状态查看、索引数据查看、ES DSL实现(增、删、改、查操作)…...

    2024/3/26 10:17:17
  7. 网络基础知识梳理(含答案)

    1)什么是链接? 链接是指两个设备之间的连接。它包括用于一个设备能够与另一个设备通信的电缆类型和协议。 2)OSI 参考模型的层次是什么? 有 7 个 OSI 层:物理层,数据链路层,网络层,传输层,会话层,表示层和应用层。 3)什么是骨干网? 骨干网络是集中的基础设施,旨在…...

    2024/3/10 7:11:04
  8. Activiti工作流在开发中的应用

    Activiti框架简介Activiti5是由Alfresco软件在2010年5月17日发布的业务流程管理(BPM)框架,它是覆盖了业务流程管理、工作流、服务协作等领域的一个开源的、灵活的、易扩展的可执行流程语言框架。Activiti基于Apache许可的开源BPM平台,创始人Tom Baeyens是JBoss JBPM的项目架…...

    2024/3/7 22:18:03
  9. 深度学习--梯度下降

    深度学习必备:随机梯度下降(SGD)优化算法及可视化补充在前:实际上在我使用LSTM为流量基线建模时候,发现有效的激活函数是elu、relu、linear、prelu、leaky_relu、softplus,对应的梯度算法是adam、mom、rmsprop、sgd,效果最好的组合是:prelu+rmsprop。我的代码如下:# S…...

    2024/3/7 22:18:03
  10. 程序员女神,是一种什么样的体验?

    小编开始是怀疑人生的,写完这三位程序员女神后,还是跟科技界白富美做朋友先。微软游戏最强女猿---Corrinne Yu 香港出生的著名女程序员。被著名游戏媒体网站Kotaku评为十年来最具影响的十位游戏界女性之一,也是其中唯一的女程序员。微软Halo团队的游戏引擎主程序员,Dire…...

    2024/3/22 15:09:47
  11. 工程项目管理问题那么多,什么软件可以实现工程项目管理自动化

    工程项目管理的那些问题当工程接近完工时,突然发现工地角落还放着一大堆钢材! 当工地工人的流动性增加时,每个人的信息难以匹配,工人如何实现打卡? 当需要查某项资料时,翻箱倒柜找了半天也没找到! ……上述场景反应了工程项目管理中存在很多问题。总结一下包括: 成本管…...

    2024/3/29 7:45:04
  12. NodeJS版本EasyDarwin开源流媒体服务器开发心得

    title: Node版本EasyDarwin开发心得 date: 2018-03-27 22:46:15 tags: 年后着手Node版本EasyDarwin的开发工作,截止到今天2018年03月27日上线了第一个版本,今天小米发布了MIX2S, 致敬! 致敬! 关于RTSP协议 目前这个第一版暂时仅支持RTSP Over TCP, 不过RTSP Over UDP也会很快实…...

    2024/3/4 7:42:32
  13. 机器学习-梯度下降

    深度学习必备:随机梯度下降(SGD)优化算法及可视化补充在前:实际上在我使用LSTM为流量基线建模时候,发现有效的激活函数是elu、relu、linear、prelu、leaky_relu、softplus,对应的梯度算法是adam、mom、rmsprop、sgd,效果最好的组合是:prelu+rmsprop。我的代码如下:# S…...

    2024/3/4 7:42:31
  14. Android过时方法替代

    managedQuery替换为cursorLoaderexample:uri = data.getData();String[] proj = {MediaStore.Images.Media.DATA};Cursor cursor;Cursor cursor = managedQuery(uri, proj, null, null, null);替换:uri = data.getData(); String[] proj = {MediaStore.Images.Media.DATA}; C…...

    2024/3/28 21:03:11
  15. web工时记录管理工具开发(一)

    前言:由于部门内部需要统计工时,原使用的svn添加工时记录不方便多人同时使用和管理,所以学习开发一个web应用来管理添加工时使用语言:html,css,js,python,flask,mysql,由于所有语言基础均为自学研究,所以开发存在很大的困难,在此记录一下整个开发流程和学习过程1、…...

    2024/3/28 20:44:30
  16. EasyDarwin开源社区流媒体视频课程:流媒体传输控制协议(RTSP RTP SDP)详解之RTP

    视频课程及相关文档代码地址:https://github.com/EasyDarwin/Course#course-3RTP协议 实时传输协议RTP(Real-time Transport Protocol)是一个网络传输协议,它是由IETF的多媒体传输工作小组1996年在RFC 1889中公布的,后在RFC3550中进行更新。 国际电信联盟…...

    2024/3/28 21:03:09
  17. 网络知识总结

    转载地址 http://m.blog.csdn.net/double_happiness/article/details/74025156 谈到网络,首先就需要知道计算机网络中的两个参考模型,即OSI参考模型与TCP/IP参考模型。 OSI参考模型 OSI(Open System Interconnect),即开放式系统互联。一般都叫OSI参考模型,是ISO组织在198…...

    2024/3/28 21:03:08
  18. android学习笔记34--------------有用代码集(不断更新)

    转载请注明原文出处:奔跑的蜗牛(袁方的技术博客)点击打开链接 一、当利用textview显示内容时,显示内容过多可能会折行或显示不全,那样效果很不好。今天发现android api中已经给出自动省略的功能。 实现如下: <TextView android:layout_width="fill_parent"…...

    2024/3/28 21:03:06
  19. 李湘被曝将做副台长 旅游卫视称未得到通知(图)

    日前李湘被曝,将要到旅游卫视担任副台长  9月8日晚,中国慈善盛典--第五届“BAZAAR明星慈善夜”在中国大饭店开幕。这是一场集百位中国一线明星、著名企业家和社会名流、顶级国际品牌、顶级时尚杂志影响力为一体的盛大晚宴,共筹善款753.8888万元人民币,将全部捐赠给中华慈…...

    2024/3/28 21:03:06
  20. 干货 | 6 种激活函数核心知识点,请务必掌握!

    来源:AI有道本文约2800字,建议阅读6分钟。本文为大家总结一下常用激活函数 Sigmoid、tanh、ReLU、Leaky ReLU、ELU、Maxout 的关键知识点。我们知道,神经网络模型中,各隐藏层、包括输出层都需要激活函数(Activation Function)。我们比较熟悉的、常用的激活函数也有 ReLU、…...

    2024/3/28 21:03:04

最新文章

  1. .Net 对象与对象之间的映射转换的6中方式以及性能对比

    我们在.Net开发的过程中&#xff0c;经常会遇到一个实体对象与另一个实体对象之间的映射转换&#xff0c;接下来我们将依次去实现6个对象间映射转换的方式&#xff0c;并对他们进行性能测试&#xff0c;找出其中效率最高的方式。 通过对象Copy&#xff0c;通过new一个新的实体对…...

    2024/3/29 8:18:10
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. 【Golang星辰图】Go语言中的数学和科学计算:从基础算法到高级工具的完整探索

    加速数学和科学计算&#xff1a;使用Go语言的优秀库和示例代码 前言&#xff1a; 在当今数据驱动的世界中&#xff0c;数学和科学计算是解决各种问题的关键。而Go语言作为一门简单、高效和强大的编程语言&#xff0c;也提供了许多优秀的数学和科学计算库。本文将介绍几个流行…...

    2024/3/28 14:59:37
  4. perl:打开文件夹,选择视频文件,并播放

    在Windows10系统中Perl安装Tk模块 运行 cmd cpan install Tk 编写 openvideo.pl 如下 #!/usr/bin/perl use strict; use warnings; use File::Basename; use Tk;my $mw MainWindow->new or die cannot create Widget;my $types [[AVI, .avi], [MP4, .mp4]];my $file $…...

    2024/3/29 7:52:23
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/3/27 10:21:24
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/3/24 20:11:25
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/3/29 2:45:46
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/3/24 20:11:23
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/3/29 5:19:52
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/3/28 17:01:12
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/3/24 5:55:47
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/3/29 1:13:26
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/3/26 23:04:51
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/3/29 7:41:19
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/3/24 20:11:18
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/3/28 9:10:53
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/3/29 0:49:46
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/3/24 20:11:15
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/3/27 7:12:50
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/3/24 20:11:13
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/3/26 11:21:23
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/3/28 18:26:34
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/3/28 12:42:28
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/3/28 20:09:10
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57