-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.json
1 lines (1 loc) · 625 KB
/
index.json
1
[{"categories":["Andorid基础"],"content":"1.概述 ","date":"2023-09-12","objectID":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/:1:0","tags":["Android"],"title":"SELinux配置和概念","uri":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/"},{"categories":["Andorid基础"],"content":"1.1 概念 作为 Android 安全模型的一部分,Android 使用安全增强型 Linux (SELinux) 对所有进程强制执行强制访问控制 (MAC),甚至包括以 Root/超级用户权限运行的进程(Linux 功能)。借助 SELinux,Android 可以更好地保护和限制系统服务、控制对应用数据和系统日志的访问、降低恶意软件的影响,并保护用户免遭移动设备上的代码可能存在的缺陷的影响。 SELinux 按照默认拒绝的原则运行:任何未经明确允许的行为都会被拒绝。SELinux 可按两种全局模式运行: 宽容模式:权限拒绝事件会被记录下来,但不会被强制执行。 强制模式:权限拒绝事件会被记录下来并强制执行。 可以通过 adb shell set enforece 0 ,将其设置为宽容模式 adb shell set enforece 1 ,将其设置为强制模式 ","date":"2023-09-12","objectID":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/:1:1","tags":["Android"],"title":"SELinux配置和概念","uri":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/"},{"categories":["Andorid基础"],"content":"1.2 MAC 和 DAC DEC系统(自主访问控制),其存在所有权的概念,即特定的资源所有者可以控制该资源关联的访问权限。这种系统通常比较粗放。并且容易出现无意中提权的问题。MAC系统则会在每次收到访问请求时都先咨询核心机构,再做出决定。 ","date":"2023-09-12","objectID":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/:1:2","tags":["Android"],"title":"SELinux配置和概念","uri":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/"},{"categories":["Andorid基础"],"content":"1.3 类型,属性,别名和规则 Android 依靠 SELinux 的类型强制执行 (TE) 组件来实施其政策。 这表示所有对象(例如文件、进程或套接字)都具有相关联的类型。 例如,默认情况下,应用的类型为 untrusted_app。对于进程而言,其类型也称为域。可以使用一个或多个属性为类型添加注解。属性可用于同时指代多种类型。 对象会映射到类(例如文件、目录、符号链接、socket套接字),并且每个类的不同访问权限类型由权限表示。 例如,file类存在权限open。虽然类型和属性作为Android SELinux政策的一部分会进行定期更新,但权限和类是静态定义的,并且作为新Linux版本的一部分也很少进行更新。 政策规则采用以下格式:allow source target:class permissions;,其中: source - 规则主题的类型(或属性)。谁正在请求访问权限? 目标 - 对象的类型(或属性)。对哪些内容提出了访问权限请求? 类 - 要访问的对象(例如,文件、套接字)的类型 权限 - 要执行的操作(或一组操作,例如读取、写入) 例如 我们可以在logcat内对tag:avc进行过滤,来看到不被允许的操作 denied { set } for property=persist.vendor.vt.video_conference_support pid=12424 uid=1000 gid=1000 scontext=u:r:system_app:s0 tcontext=u:object_r:vendor_mtk_vendor_vt_prop:s0 tclass=property_service permissive=0' 可以看到上述avc输出: source : u:r:system_app:s0 tcontext : u:object_r:vendor_mtk_vendor_vt_prop:s0 tclass: property_service 当前的SELinux的状态 permissive = 0 强制模式 1.3.1 类型声明 区别客体类别。这里所说的类型指的是域类型和客体类型。 语法:type 类型名(例如system_prop) type 类型名 [属性集,] [属性集,]….. SELinux没有预定的类型,所以我们需要声明我们所需的类型。 1.3.2 属性 规则默认是拒绝所有访问的,每一个访问都需要被声明。 如果我们想要让程序访问所有的文件,首先我们需要创建一个对应的域类型(system_app),并允许它访问任何类型的文件。 type system_app; //声明域类型 allow system_app xxx_prop_t : file {read write open close}; //允许该域类型对xxx_prop具有读写打开关闭操作。(单个权限不需要{}) allow system_app http_file_t : file {read write open close}; 然而system_app需要对多文件进行访问权限操作,这样我们就需要编写很多allow语句,所以我们需要使用属性。 attribute直接翻译过来叫属性, 主要用于在声明type的时候进行关联。 属性类似一个组,type关联属性之后, 新增的type就可以加入到这个属性组中,这个属性(组)拥有的权限, 新增的type也就自然拥有了这个权限),举例: type xxx_prop_t , coredomain, domain; type http_file_t , domain, coredomain; { xxx_prop_t , http_file_t }都关联(加入)了domain属性组 这样我们需要让system_app对所有文件进行{read,write,open,close}操作的时候 可以直接使用如下语法: allow system_app domain: file {read write open close}; 1.3.3 宏 特别是对于文件访问权限,有很多种权限需要考虑。例如,read 权限不足以打开相应文件或对其调用 stat。为了简化规则定义,Android 提供了一组宏来处理最常见的情况。例如,若要添加 open 等缺少的权限,可以将上述规则改写为: allow appdomain app_data_file:file rw_file_perms; 如需查看实用宏的更多示例,请参阅 global_macros 和 te_macros ","date":"2023-09-12","objectID":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/:1:3","tags":["Android"],"title":"SELinux配置和概念","uri":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/"},{"categories":["Andorid基础"],"content":"1.4 安全上下文 调试SELinux政策或为文件添加标签时(通过file_contexts或运行ls -Z),可能会遇到安全上下文(也称为标签) 安全上下文格式:user:role:type:sensitivity[:categories] user: 用户,目前SEAndroid上只定义一个SELinux用户,值为u role:角色,r(适用于主题)和 object_r(适用于对象) type: 主体/客体类型,一般定义在具体的te文件中 sensitivity[:categories]:安全级别 sensitivity:敏感度 category:类别。 ","date":"2023-09-12","objectID":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/:1:4","tags":["Android"],"title":"SELinux配置和概念","uri":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/"},{"categories":["Andorid基础"],"content":"1.5 te文件内容的语法规则 rule_name source_type target_type : class permission_set rule_name: 赋予权限的规则, 包含allow:允许 dontaudit:不记录某项操作失败 auditallow:记录操作 neverallow:检查安全策略中是否存在违反neverallow规则的allow语句。 source_type: 访问target_type的主体或主体集合(域),可自定义 target_type:接受主体访问的客体或客体集合(域),可自定义 class : 客体资源类型,不同的资源类型具有不同访问权限,可自定义、可继承 perm_set:客体予以主体的权限说明。是class中具有的权限的子集 其中source_type和target_type可以按照上述的attribute配置集合。 ","date":"2023-09-12","objectID":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/:1:5","tags":["Android"],"title":"SELinux配置和概念","uri":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/"},{"categories":["Andorid基础"],"content":"2.SELinux的实现 ","date":"2023-09-12","objectID":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/:2:0","tags":["Android"],"title":"SELinux配置和概念","uri":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/"},{"categories":["Andorid基础"],"content":"2.1 SELinux的核心文件 如果需要集成启动SELinux,需要下载最新Android内核,并对其中的system/sepolicy目录下的文件进行修改。这些修改后的文件再编译后会包含SELinux内核安全政策,并涵盖上游Android系统。 常情况下,您不能直接修改 system/sepolicy 文件,但您可以添加或修改自己的设备专用政策文件(位于 /device/manufacturer/device-name/sepolicy 目录中)。在 Android 8.0 及更高版本中,您对这些文件所做的更改只会影响供应商目录中的政策 如需详细了解 Android 8.0 及更高版本中的公共 sepolicy 分离,请参阅在 Android 8.0 及更高版本中自定义 SEPolicy。 在Android8.0及其更高版本中,sepolicy位于AOSP中的以下位置: system/sepolicy/public: 此目录可以处理privat和vendor之间的接口的所有内容。因此需要对其加以限制。 system/sepolicy/private:此目录下不可以访问vendor下的任何安全策略,其中的内容包含所有system的所有私有内容。 system/sepolicy/vendor: 此目录下不可访问private下的任何安全政策。属于供应商的安全政策。 device/manufacturer/device-name/sepolicy。包含设备专用策略,已经对sepolicy的自定义。 上下文描述文件: 详细上下文参考 查看上下文描述文件 file_contexts //系统中所有file_contexts安全上下文 seapp_contexts //app安全上下文 property_contexts //属性的安全上下文 service_contexts //service文件安全上下文 genfs_contexts //虚拟文件系统安全上下文 ","date":"2023-09-12","objectID":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/:2:1","tags":["Android"],"title":"SELinux配置和概念","uri":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/"},{"categories":["Andorid基础"],"content":"2.1 构建和编译sepolicy 前提需要下载Android内核代码: 最新Android内核 参考: 构建sepolicy 实现sepolicy ","date":"2023-09-12","objectID":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/:2:2","tags":["Android"],"title":"SELinux配置和概念","uri":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/"},{"categories":["Andorid基础"],"content":"3,调试验证 SELinux 日志消息中包含“avc:”字样,因此可使用 grep 轻松找到。 您可以通过运行 cat /proc/kmsg 来获取当前的拒绝事件日志,也可以通过运行 cat /sys/fs/pstore/console-ramoops 来获取上次启动时的拒绝事件日志 。 你也可以通过过滤tag:avc 在logcat上查看输出。 avc: denied { connectto } for pid=2671 comm=\"ping\" path=\"/dev/socket/dnsproxyd\" scontext=u:r:shell:s0 tcontext=u:r:netd:s0 tclass=unix_stream_socket 该输出的解读如下: 上方的 { connectto } 表示执行的操作。根据它和末尾的 tclass (unix_stream_socket),您可以大致了解是对什么对象执行什么操作。在此例中,是操作方正在试图连接到 UNIX 信息流套接字。 scontext (u:r:shell:s0) 表示发起相应操作的环境,在此例中是 shell 中运行的某个程序。 tcontext (u:r:netd:s0) 表示操作目标的环境,在此例中是归 netd 所有的某个 unix_stream_socket。 顶部的 comm=“ping” 可帮助您了解拒绝事件发生时正在运行的程序。在此示例中,给出的信息非常清晰明了。 具体调试策略:请参考google sepolicy调试 ","date":"2023-09-12","objectID":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/:3:0","tags":["Android"],"title":"SELinux配置和概念","uri":"/selinux%E9%85%8D%E7%BD%AE%E5%92%8C%E6%A6%82%E5%BF%B5/"},{"categories":["Andorid基础"],"content":"1.OKHttp详解 ","date":"2023-09-09","objectID":"/okhttp%E8%AF%A6%E8%A7%A3/:0:0","tags":["Android"],"title":"Okhttp详解","uri":"/okhttp%E8%AF%A6%E8%A7%A3/"},{"categories":["Andorid基础"],"content":"1.1 概述 OkHttp 是一套处理 HTTP 网络请求的依赖库,由 Square 公司设计研发并开源,目前可以在 Java 和 Kotlin 中使用 ","date":"2023-09-09","objectID":"/okhttp%E8%AF%A6%E8%A7%A3/:1:0","tags":["Android"],"title":"Okhttp详解","uri":"/okhttp%E8%AF%A6%E8%A7%A3/"},{"categories":["Andorid基础"],"content":"1.2 使用 request uri: http://guolin.tech/api/china //在gradle.build内添加okhttp //implementation 'com.squareup.okhttp3:okhttp:4.10.0' //创建OKHttp client OkHttpClient client = new OkHttpClient(); //创建request Request request = new Request.Builder().url(uri).get().build(); //为client和request创建一个call对象 Call call = client.newCall(request); //为call添加callback并重写onFailure和onResponse方法 call.enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e){ } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { } }); // 调用runOnUiThread(new Runable{})更新ui ","date":"2023-09-09","objectID":"/okhttp%E8%AF%A6%E8%A7%A3/:2:0","tags":["Android"],"title":"Okhttp详解","uri":"/okhttp%E8%AF%A6%E8%A7%A3/"},{"categories":["Andorid基础"],"content":"1.3 问题点 ","date":"2023-09-09","objectID":"/okhttp%E8%AF%A6%E8%A7%A3/:3:0","tags":["Android"],"title":"Okhttp详解","uri":"/okhttp%E8%AF%A6%E8%A7%A3/"},{"categories":["Andorid基础"],"content":"1.3.1 谷歌要求使用默认加密连接,即Https 报错:java.net.UnknownServiceException: CLEARTEXT communication to guolin.tech not permitted by network security policy 原因: 为保证用户数据和设备的安全,Google针对下一代 Android 系统(Android P) 的应用程序,将要求默认使用加密连接,这意味着 Android P 将禁止 App 使用所有未加密的连接,因此运行 Android P 系统的安卓设备无论是接收或者发送流量,未来都不能明码传输。 具体可参考谷歌网络安全配置 解决方法: APP改用 https 请求 targetSdkVersion 降到27以下 更改网络安全配置 方案3的更改网络配置具体操作 在res/xml下创建一个network_security_config.xml,内容如下: \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e \u003cnetwork-security-config\u003e \u003cbase-config cleartextTrafficPermitted=\"true\" /\u003e \u003c/network-security-config\u003e 在 AndroidManifest.xml 文件下面配置network_security_config.xml即可 \u003capplication .... android:networkSecurityConfig=\"@xml/network_security_config\" ... /\u003e ","date":"2023-09-09","objectID":"/okhttp%E8%AF%A6%E8%A7%A3/:3:1","tags":["Android"],"title":"Okhttp详解","uri":"/okhttp%E8%AF%A6%E8%A7%A3/"},{"categories":["Andorid基础"],"content":"1.3.2 FATAL EXCEPTION: OkHttp Dispatcher 原因: OkHttp 的Response response.body().string()方法,只能调用一次。按照源码来看,在被调用读取buff内的字符之后,该资源会被关闭,如果二次调用string()就会导致异常。 ","date":"2023-09-09","objectID":"/okhttp%E8%AF%A6%E8%A7%A3/:3:2","tags":["Android"],"title":"Okhttp详解","uri":"/okhttp%E8%AF%A6%E8%A7%A3/"},{"categories":["Andorid基础"],"content":"1.4 显示的结果 获取json数据。 [{\"id\":1,\"name\":\"北京\"},{\"id\":2,\"name\":\"上海\"},{\"id\":3,\"name\":\"天津\"},{\"id\":4,\"name\":\"重庆\"},{\"id\":5,\"name\":\"香港\"},{\"id\":6,\"name\":\"澳门\"},{\"id\":7,\"name\":\"台湾\"},{\"id\":8,\"name\":\"黑龙江\"},{\"id\":9,\"name\":\"吉林\"},{\"id\":10,\"name\":\"辽宁\"},{\"id\":11,\"name\":\"内蒙古\"},{\"id\":12,\"name\":\"河北\"},{\"id\":13,\"name\":\"河南\"},{\"id\":14,\"name\":\"山西\"},{\"id\":15,\"name\":\"山东\"},{\"id\":16,\"name\":\"江苏\"},{\"id\":17,\"name\":\"浙江\"},{\"id\":18,\"name\":\"福建\"},{\"id\":19,\"name\":\"江西\"},{\"id\":20,\"name\":\"安徽\"},{\"id\":21,\"name\":\"湖北\"},{\"id\":22,\"name\":\"湖南\"},{\"id\":23,\"name\":\"广东\"},{\"id\":24,\"name\":\"广西\"},{\"id\":25,\"name\":\"海南\"},{\"id\":26,\"name\":\"贵州\"},{\"id\":27,\"name\":\"云南\"},{\"id\":28,\"name\":\"四川\"},{\"id\":29,\"name\":\"西藏\"},{\"id\":30,\"name\":\"陕西\"},{\"id\":31,\"name\":\"宁夏\"},{\"id\":32,\"name\":\"甘肃\"},{\"id\":33,\"name\":\"青海\"},{\"id\":34,\"name\":\"新疆\"}] ","date":"2023-09-09","objectID":"/okhttp%E8%AF%A6%E8%A7%A3/:4:0","tags":["Android"],"title":"Okhttp详解","uri":"/okhttp%E8%AF%A6%E8%A7%A3/"},{"categories":["C++基础"],"content":"类型转换运算符 ","date":"2023-08-20","objectID":"/runtime_type_identification/:0:0","tags":["C++"],"title":"Runtime_type_identification","uri":"/runtime_type_identification/"},{"categories":["C++基础"],"content":"1. daynamic_cast dynamic_cast不能回答指针指向的是哪种类型,但是可以回答是否可以安全的将对象的地址赋值给特定类型的指针。 dynamic_cast用于类继承层次间的指针或引用转换.如果是单纯的派生类指针或引用转换为基类的指针或引用,这本是就是安全的。使用dynamic_cast略显多余。 如下: #include \u003ciostream\u003e #include \u003ccstdlib\u003e #include \u003cctime\u003e using std::cout; class Grand { private: int hold; public: Grand(int h = 0):hold(h){} virtual void Speak()const{cout \u003c\u003c \"I am a grand class!\\n\";} virtual int Value() const{return hold;} }; class Superb : public Grand { public: Superb(int h = 0):Grand(h){} void Speak()const{cout \u003c\u003c \"I am a superb class!!\\n\";} virtual void Say() const{cout \u003c\u003c \"I hold the superb value of \" \u003c\u003c Value() \u003c\u003c \"!\\n\";} }; class Magnificent : public Superb { private: char ch; public: Magnificent(int h = 0, char c = 'A'):Superb(h),ch(c){} void Speak() const { cout \u003c\u003c \"I am a Magnificent class !!!\\n\";} void Say() const { cout \u003c\u003c \"I hold the character \" \u003c\u003c ch \u003c\u003c \" and the integer \" \u003c\u003c Value() \u003c\u003c \"\\n\";} }; int main() { Grand * pg; Magnificent * ps = new Magnificent(); pg = dynamic_cast\u003cGrand *\u003e(ps); // 正确,但是没有必要,向上转型总是安全的,而且dynamic_cast有开销 pg-\u003eSpeak(); delete ps; return 0; } dynamic_cast 真正的作用是进行安全的向下转型。 基类指针指向派生类,将该指针转化为派生类的指针。成功返回派生类指针。 基类指针指向基类,将该指针转化为派生类指针,失败返回0(NULL) 如下: #include \u003ciostream\u003e #include \u003ccstdlib\u003e #include \u003cctime\u003e using std::cout; using std::endl; class Grand { private: int hold; public: Grand(int h = 0):hold(h){} virtual void Speak()const{cout \u003c\u003c \"I am a grand class!\\n\";} virtual int Value() const{return hold;} }; class Superb : public Grand { public: Superb(int h = 0):Grand(h){} void Speak()const{cout \u003c\u003c \"I am a superb class!!\\n\";} virtual void Say() const{cout \u003c\u003c \"I hold the superb value of \" \u003c\u003c Value() \u003c\u003c \"!\\n\";} }; class Magnificent : public Superb { private: char ch; public: Magnificent(int h = 0, char c = 'A'):Superb(h),ch(c){} void Speak() const { cout \u003c\u003c \"I am a Magnificent class !!!\\n\";} void Say() const { cout \u003c\u003c \"I hold the character \" \u003c\u003c ch \u003c\u003c \" and the integer \" \u003c\u003c Value() \u003c\u003c \"\\n\";} }; int main() { Grand * ps[3]; ps[0] = new Magnificent(); ps[1] = new Superb(); ps[2] = new Grand(); for(int i = 0; i \u003c 3; i++) { Superb * pb; if(pb = dynamic_cast\u003cSuperb*\u003e(ps[i])) { pb-\u003eSay(); } else { cout \u003c\u003c \"错误的转换\" \u003c\u003c endl; } delete ps[i]; } return 0; } 补充: 使用引用进行dynamic_cast转换,正确的转换并不会发生任何事,但是,错误的转换不会返回0,而是抛出bad_cast异常。 ","date":"2023-08-20","objectID":"/runtime_type_identification/:1:0","tags":["C++"],"title":"Runtime_type_identification","uri":"/runtime_type_identification/"},{"categories":["C++基础"],"content":"2.const_cast const_cast用于改变值为const或Volatile. 用法: const_cast\u003ctype_name\u003e(expression) 返回值为新类型。这里我们需要强调的是const_cast主要用于更改指针或引用的const或volatile限定符。其中,type_name必须是指针、引用或者成员指针类型. 如下: #include \u003ciostream\u003e using namespace std; void change(const int * pt, int n); int main() { int pop1 = 38383; const int pop2 = 2000; cout \u003c\u003c \"pop1: \" \u003c\u003c pop1 \u003c\u003c \",address: \"\u003c\u003c \u0026pop1 \u003c\u003c endl; cout \u003c\u003c \"pop2: \" \u003c\u003c pop2 \u003c\u003c \",address: \"\u003c\u003c \u0026pop2 \u003c\u003c endl; cout \u003c\u003c endl; change(\u0026pop1, -103); change(\u0026pop2, -103); cout \u003c\u003c endl; cout \u003c\u003c \"pop1: \" \u003c\u003c pop1 \u003c\u003c \",address: \"\u003c\u003c \u0026pop1 \u003c\u003c endl; cout \u003c\u003c \"pop2: \" \u003c\u003c pop2 \u003c\u003c \",address: \"\u003c\u003c \u0026pop2 \u003c\u003c endl; return 0; } void change(const int * pt,int n) { int * pc; pc = const_cast\u003cint *\u003e(pt); *pc += n; cout \u003c\u003c \"ps: \" \u003c\u003c *pt \u003c\u003c \",address: \" \u003c\u003c pt \u003c\u003c endl; } result: pop1: 38383,address: 0x7ffda82a80e0 pop2: 2000,address: 0x7ffda82a80e4 ps: 38280,address: 0x7ffda82a80e0 ps: 1897,address: 0x7ffda82a80e4 pop1: 38280,address: 0x7ffda82a80e0 pop2: 2000,address: 0x7ffda82a80e4 从运行结果可以看出,pop1和ps的地址相同,pop2和ps的地址相同,在函数内部,该地址所对应的值已经被-103,但是通过函数之后最终pop2却没有修改,为什么会出现这种结果呢? 实际上这就是因为编译器优化结果造成的,因为在声明pop2的时候,其类型是const int,在编译阶段,编译器认为它就是不变的类型,当编译到cout « “pop2: \" « pop2 « “,address: “« \u0026pop2 « endl;时,会将pop2直接替换为常量2000,即cout « “pop2: \" « 2000 « “,address: “« \u0026pop2 « endl;,因此打印出来的就过就是2000。也正是由于该行为是未定义的行为,才导致输出结果与我们的预期不一致。所以,在我们日常使用中,const_cast可以用用来修改最初声明非const的值,而且应该尽量避免常量转换,除非我们真的需要使用它。 const_cast另一个作用 将volatile丢弃,例子如下: #include \u003ciostream\u003e #include \u003ctypeinfo\u003e int main() { using namespace std; int a = 100; volatile int *atemp = \u0026a; cout \u003c\u003c typeid(a).name() \u003c\u003c endl; cout \u003c\u003c typeid(atemp).name() \u003c\u003c endl; cout \u003c\u003c \"将Volatile int *转换为 int * ...\" \u003c\u003c endl; int * aptr = const_cast\u003cint *\u003e(atemp); cout \u003c\u003c typeid(aptr).name() \u003c\u003c endl; return 0; } ","date":"2023-08-20","objectID":"/runtime_type_identification/:2:0","tags":["C++"],"title":"Runtime_type_identification","uri":"/runtime_type_identification/"},{"categories":["C++基础"],"content":"3.static_cast static_cast关键字一般用来将枚举类型转换成整型,或者短整形转换成长整形,又或者整型转换成浮点型。也可以用来将指向父类的指针转换成指向子类的指针。 static_cast使用注意事项: static_cast可以用于基本类型的转换,如short与int、int与float、enum与int之间; static_cast也可以用于类类型的转换,但目标类型必须含有相应的构造函数; static_cast还可以转换对象的指针类型,但它不进行运行时类型检查,所以是不安全的; static_cast甚至可以把任何表达式都转换成void类型; satic_cast不能移除变量的const属性,请参考const_cast操作符; static_cast进行的是简单粗暴的转换(仅仅依靠尖括号中的类型),所以其正确性完全由程序员自己保证。 static_cast是在编译时进行的,这与dynamic_cast正好相反。 ","date":"2023-08-20","objectID":"/runtime_type_identification/:3:0","tags":["C++"],"title":"Runtime_type_identification","uri":"/runtime_type_identification/"},{"categories":["C++基础"],"content":"4.reinterpret_cast 首先从英文字面的意思理解,interpret是“解释,诠释”的意思,加上前缀“re”,就是“重新诠释”的意思;cast在这里可以翻译成“转型”(在侯捷大大翻译的《深度探索C++对象模型》、《Effective C++(第三版)》中,cast都被翻译成了转型),这样整个词顺下来就是“重新诠释的转型”。我们知道变量在内存中是以“…0101…”二进制格式存储的,一个int型变量一般占用32个位,参考下面的代码 #include \u003ciostream\u003e using namespace std; int main(int argc, char** argv) { int num = 0x00636261;//用16进制表示32位int,0x61是字符'a'的ASCII码 int * pnum = \u0026num; char * pstr = reinterpret_cast\u003cchar *\u003e(pnum); cout\u003c\u003c\"pnum指针的值: \"\u003c\u003cpnum\u003c\u003cendl; cout\u003c\u003c\"pstr指针的值: \"\u003c\u003cstatic_cast\u003cvoid *\u003e(pstr)\u003c\u003cendl;//直接输出pstr会输出其指向的字符串,这里的类型转换是为了保证输出pstr的值 cout\u003c\u003c\"pnum指向的内容: \"\u003c\u003chex\u003c\u003c*pnum\u003c\u003cendl; cout\u003c\u003c\"pstr指向的内容: \"\u003c\u003cpstr\u003c\u003cendl; return 0; } result: pnum指针的值: 0x7ffd7246df64 pstr指针的值: 0x7ffd7246df64 pnum指向的内容: 636261 pstr指向的内容: abc 第6行定义了一个整型变量num,并初始化为0x00636261(十六进制表示),然后取num的地址用来初始化整型指针变量pnum。接着到了关键的地方,使用reinterpret_cast运算符把pnum从int转变成char类型并用于初始化pstr。 将pnum和pstr两个指针的值输出,对比发现,两个指针的值是完全相同的,这是因为“reinterpret_cast 运算符并不会改变括号中运算对象的值,而是对该对象从位模式上进行重新解释”。如何理解位模式上的重新解释呢?通过推敲代码11行和12行的输出内容,就可见一斑。 很显然,按照十六进制输出pnum指向的内容,得到636261;但是输出pstr指向的内容,为什么会得到”abc”呢? 可以参考char占一个byte,但是由于int4byte内没有定义’\\0’所以一直输出,直到遇到0。 ","date":"2023-08-20","objectID":"/runtime_type_identification/:4:0","tags":["C++"],"title":"Runtime_type_identification","uri":"/runtime_type_identification/"},{"categories":["Andorid基础"],"content":"Aidl ","date":"2023-07-10","objectID":"/android%E5%AD%A6%E4%B9%A0_aidl/:0:0","tags":["Android"],"title":"Android学习_Aidl练习","uri":"/android%E5%AD%A6%E4%B9%A0_aidl/"},{"categories":["Andorid基础"],"content":"1.什么是Aidl AIDL 意思即 Android Interface Definition Language,翻译过来就是Android接口定义语言,是用于定义服务器和客户端通信接口的一种描述语言,可以拿来生成用于IPC的代码。从某种意义上说AIDL其实是一个模板,因为在使用过程中,实际起作用的并不是AIDL文件,而是据此而生成的一个IInterface的实例代码,AIDL其实是为了避免我们重复编写代码而出现的一个模板 AIDL文件以 .aidl 为后缀名 ","date":"2023-07-10","objectID":"/android%E5%AD%A6%E4%B9%A0_aidl/:1:0","tags":["Android"],"title":"Android学习_Aidl练习","uri":"/android%E5%AD%A6%E4%B9%A0_aidl/"},{"categories":["Andorid基础"],"content":"2.使用及样例 ","date":"2023-07-10","objectID":"/android%E5%AD%A6%E4%B9%A0_aidl/:2:0","tags":["Android"],"title":"Android学习_Aidl练习","uri":"/android%E5%AD%A6%E4%B9%A0_aidl/"},{"categories":["Andorid基础"],"content":"2.1 初期设置 使用Aidl需要在build.gradle(:app)内的android内添加如下 buildFeatures { aidl = true } ","date":"2023-07-10","objectID":"/android%E5%AD%A6%E4%B9%A0_aidl/:2:1","tags":["Android"],"title":"Android学习_Aidl练习","uri":"/android%E5%AD%A6%E4%B9%A0_aidl/"},{"categories":["Andorid基础"],"content":"2.2 样例 1.定义一个IBookManager.aidl接口,管理Aidl通信 2.定义一个Book.aidl,用于IPC传输实体 3.定义一个IOnServerCallBack.aidl,用于在IBookManager中设置回调函数,从而实现服务端向客户端传输数据。在客户端实现 文件结构如下图: IBookManager 如下是aidl // IBookManager.aidl package com.zxl.aidldemo.aidl; import com.zxl.aidldemo.aidl.Book; import com.zxl.aidldemo.aidl.IOnServerCallback; // Declare any non-default types here with import statements interface IBookManager { /** * Demonstrates some basic types that you can use as parameters * and return values in AIDL. */ void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString); //从远端获取BookList int getBookList(); //添加一本书 void addBook(in Book book); //注册回调 void registerCallback(IOnServerCallback callback); void unregisterCallback(IOnServerCallback callback); } 其对应的Java实现类: package com.zxl.aidldemo.aidl; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.util.Log; import java.util.ArrayList; import java.util.List; /** * 图书管理类 */ public class BookManagerImpl extends IBookManager.Stub{ private static final String TAG = \"ZHAOBookManagerImpl\"; private RemoteCallbackList\u003cIOnServerCallback\u003e mListener ; private ArrayList\u003cBook\u003e list ; public BookManagerImpl(RemoteCallbackList\u003cIOnServerCallback\u003e mListener){ this.mListener = mListener; list = new ArrayList\u003c\u003e(); } @Override public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException { } @Override public int getBookList() throws RemoteException { log(\"getBookList\"); return list.size(); } @Override public void addBook(Book book) throws RemoteException { log(\"addBook : \"+book.toString()); list.add(book); if(mListener!=null){ int num = mListener.beginBroadcast(); for (int i = 0; i \u003c num; i++) { IOnServerCallback broadcastItem = mListener.getBroadcastItem(i); broadcastItem.onBookRecived(book); } mListener.finishBroadcast(); } } @Override public void registerCallback(IOnServerCallback callback) throws RemoteException { if(mListener != null){ mListener.register(callback); } } @Override public void unregisterCallback(IOnServerCallback callback) throws RemoteException { mListener.unregister(callback); } private void log(String str){ Log.d(TAG,str); } } Book.aidl Book.aidl,用于IPC传输实体 // Book.aidl package com.zxl.aidldemo.aidl; // Declare any non-default types here with import statements parcelable Book; 其对应的java类 继承了Parcelable package com.zxl.aidldemo.aidl; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; public class Book implements Parcelable { private int BookId; private String BookName; public Book(int BookId,String BookName){ this.BookId = BookId; this.BookName = BookName; } protected Book(Parcel in){ BookId = in.readInt(); BookName = in.readString(); } public static final Creator\u003cBook\u003e CREATOR = new Creator\u003cBook\u003e() { @Override public Book createFromParcel(Parcel in) { return new Book(in); } @Override public Book[] newArray(int size) { return new Book[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(BookId); dest.writeString(BookName); } } IOnServerCallback 作为回调函数,查看Book信息是否传递到服务端。 aidl接口如下 // IOnServerCallback.aidl package com.zxl.aidldemo.aidl; import com.zxl.aidldemo.aidl.Book; // Declare any non-default types here with import statements interface IOnServerCallback { /** * Demonstrates some basic types that you can use as parameters * and return values in AIDL. */ void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString); //接收到书的反应函数 void onBookRecived(in Book book); } MainActivity 定义了四个btn,一个TextView显示服务端的书本数目,原先想将Book的集合回传并显示在TextView上,但是由于还需要重写Parcelable接口,懒得写所以直接回传书本。 其中的重点是IBinder.DeathRecipient的重写,ServiceConn的连接。 package com.zxl.aidldemo; import androidx.appcompat.app.AppCompatActivity; import android.content.ComponentName; import android.content.Intent; import android.content.Serv","date":"2023-07-10","objectID":"/android%E5%AD%A6%E4%B9%A0_aidl/:2:2","tags":["Android"],"title":"Android学习_Aidl练习","uri":"/android%E5%AD%A6%E4%B9%A0_aidl/"},{"categories":["Andorid基础"],"content":"输出效果图 ","date":"2023-07-10","objectID":"/android%E5%AD%A6%E4%B9%A0_aidl/:3:0","tags":["Android"],"title":"Android学习_Aidl练习","uri":"/android%E5%AD%A6%E4%B9%A0_aidl/"},{"categories":["Andorid基础"],"content":" 1.Bookkeeping 实现功能: 主界面显示本月收支,本日收支,本月收支的条目 主界面右下角实现记一笔,菜单功能 菜单 包括账单概览,账单图表化概览,设置,关于 具体实现请参考如下连接 记账本源码仓库 ","date":"2023-07-09","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_bookkeeping/:0:0","tags":["Android"],"title":"Android基础学习_Bookkeeping","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_bookkeeping/"},{"categories":["Andorid基础"],"content":"1.1 文件夹结构 文件夹 Adapter 各类适配器 Bookkeeping Activity.java文件存放地址 DataBase 数据库操作类 Dialog 自定义弹窗 Entity 存储数据类 Utils 工具 View 自定义View AAChartCoreLib AAChart图标 MyApplication.java 自定义Application ","date":"2023-07-09","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_bookkeeping/:1:0","tags":["Android"],"title":"Android基础学习_Bookkeeping","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_bookkeeping/"},{"categories":["Andorid基础"],"content":"1.2 效果图 ","date":"2023-07-09","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_bookkeeping/:2:0","tags":["Android"],"title":"Android基础学习_Bookkeeping","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_bookkeeping/"},{"categories":["Andorid基础"],"content":" 1.AAChartCore AAChartCore,是 AAChartKit 的 Java语言版本,是在流行的开源前端图表框架的基础上,封装的面向对象的,一组简单易用,极其精美的图表绘制控件 AAChartCore(Java版)仓库 AAChartCore(Kotlin版)仓库 AAChartCore(ios版)仓库 AAChartCore(Java版)文档 2.使用 ","date":"2023-07-08","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_aachartcore/:0:0","tags":["Android"],"title":"Android基础学习_AAChartCore","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_aachartcore/"},{"categories":["Andorid基础"],"content":"2.1 安装 ","date":"2023-07-08","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_aachartcore/:1:0","tags":["Android"],"title":"Android基础学习_AAChartCore","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_aachartcore/"},{"categories":["Andorid基础"],"content":"2.1.1 手动安装 1.使用git指令拉去Java版的AAChartCore,找到Demo文件 2.将 Demo 文件下的 assets 文件拉取到项目app/src/main下 2.将 Demo 中的名为 AAChartCoreLib 文件夹拉取到app/src/main/java/下 ","date":"2023-07-08","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_aachartcore/:1:1","tags":["Android"],"title":"Android基础学习_AAChartCore","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_aachartcore/"},{"categories":["Andorid基础"],"content":"2.2 使用 ","date":"2023-07-08","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_aachartcore/:2:0","tags":["Android"],"title":"Android基础学习_AAChartCore","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_aachartcore/"},{"categories":["Andorid基础"],"content":"2.2.1 layout_xml文件 在xml文件内定义一个或者多个AAchartView。 \u003ccom.zxl.anan.AAChartCore.AAChartCoreLib.AAChartCreator.AAChartView android:id=\"@+id/AAChartView\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" /\u003e ","date":"2023-07-08","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_aachartcore/:2:1","tags":["Android"],"title":"Android基础学习_AAChartCore","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_aachartcore/"},{"categories":["Andorid基础"],"content":"2.2.2 Activity chartType有如下多种 String Column = \"column\"; String Bar = \"bar\"; String Area = \"area\"; String AreaSpline = \"areaspline\"; String Line = \"line\"; String Spline = \"spline\"; String Scatter = \"scatter\"; String Pie = \"pie\"; String Bubble = \"bubble\"; String Pyramid = \"pyramid\"; String Funnel = \"funnel\"; String Columnrange = \"columnrange\"; String Arearange = \"arearange\"; String Areasplinerange = \"areasplinerange\"; String Boxplot = \"boxplot\"; String Waterfall = \"waterfall\"; private void initChartView() { aaChartView = findViewById(R.id.AAChartView); //获取当前时间 HashMap\u003cString, Integer\u003e calenderTime = Util.getCalenderTime(); int year = calenderTime.get(\"year\"); int month = calenderTime.get(\"month\"); String date = Util.getYearMonthStr(year,month); //获取数据 for (int i = 0; i \u003c typeNameArr.length; i++) { overview_classification oc = DBManager.queryTypeSumByYearMonthKind(year, month, 0, typeNameArr[i]); if(oc.getTypeName() != null){ ocList.add(oc); } } //获取标签名 String strArray[] = new String[ocList.size()]; Float dataArray[] = new Float[ocList.size()]; for (int i = 0; i \u003c ocList.size(); i++) { strArray[i] = ocList.get(i).getTypeName(); dataArray[i] = ocList.get(i).getTotalMoney(); } // AAChartModel aaChartModel = new AAChartModel() .chartType(AAChartType.Column) .title(\"月度账单总览\")//设置标题 .subtitle(date)//设置副标题 .backgroundColor(\"#FFFFFF\")//设置背景色白色 .categories(strArray) //设置标签 .dataLabelsEnabled(true) //点击显示数据标签 .yAxisGridLineWidth(0f) //数据 .series( new AASeriesElement[]{ new AASeriesElement() .name(\"Out\") .data(dataArray), }); //展示 aaChartView.aa_drawChartWithChartModel(aaChartModel); } ","date":"2023-07-08","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_aachartcore/:2:2","tags":["Android"],"title":"Android基础学习_AAChartCore","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_aachartcore/"},{"categories":["Andorid基础"],"content":" 1.即时网络监听 ","date":"2023-06-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/:0:0","tags":["Android"],"title":"Android基础学习_即时网络监听","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/"},{"categories":["Andorid基础"],"content":"1.1 使用BroadCast监听网络变化 ","date":"2023-06-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/:1:0","tags":["Android"],"title":"Android基础学习_即时网络监听","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/"},{"categories":["Andorid基础"],"content":"1.1.1 创建广播接收类 创建广播接受类并继承BroadcastReceiver 重写onReciver方法 根据intent的action判断是不是自己需要反应的动作(本次反应对象为ConnectivityManager.CONNECTIVITY_ACTION) public class NetworkChangeBroadCast extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if(action.equals(ConnectivityManager.CONNECTIVITY_ACTION)){ Toast.makeText(context,\"CONNECTIVITY_ACTION_CHANGED\",Toast.LENGTH_SHORT).show(); Log.d(\"ZHAO\",\"CONNECTIVITY_ACTION_CHANGED\"); } } } ","date":"2023-06-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/:1:1","tags":["Android"],"title":"Android基础学习_即时网络监听","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/"},{"categories":["Andorid基础"],"content":"1.1.2 在MainActivity内部注册广播 在onCreate内部注册广播 在ondestroy内部注销广播 public class BroadCastActivity extends AppCompatActivity { private NetworkChangeBroadCast networkChangeBroadCast; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); //注册过滤器 IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); //广播接受者 networkChangeBroadCast = new NetworkChangeBroadCast(); //注册广播 registerReceiver(networkChangeBroadCast,intentFilter); } @Override protected void onDestroy() { super.onDestroy(); //解除广播 unregisterReceiver(networkChangeBroadCast); } } ","date":"2023-06-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/:1:2","tags":["Android"],"title":"Android基础学习_即时网络监听","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/"},{"categories":["Andorid基础"],"content":"1.2 使用ConnectivityManager.NetworkCallback监控网络 官方文档 ","date":"2023-06-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/:2:0","tags":["Android"],"title":"Android基础学习_即时网络监听","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/"},{"categories":["Andorid基础"],"content":"1.2.1 新建NetworkCallbackImpl类 新建NetworkCallbackImpl类继承ConnectivityManager.NetworkCallback 重写onAvailable、onLost、onCapabilitiesChanged public class NetworkCallbackImpl extends ConnectivityManager.NetworkCallback { private static final String TAG = \"ZHAO\"; //网络可用时 @Override public void onAvailable(@NonNull Network network) { super.onAvailable(network); Log.d(\"ZHAO\",\"onAvailable: 网络已连接\"); } //网络不可用时 @Override public void onLost(@NonNull Network network) { super.onLost(network); Log.d(\"ZHAO\",\"onLost: 网络已断开\"); } @Override public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) { super.onCapabilitiesChanged(network, networkCapabilities); if(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)){ if(networkCapabilities.hasTransport(networkCapabilities.TRANSPORT_WIFI)){ Log.d(TAG, \"onCapabilitiesChanged: 网络类型为wifi\"); }else if (networkCapabilities.hasTransport(networkCapabilities.TRANSPORT_CELLULAR)){ Log.d(TAG, \"onCapabilitiesChanged: 蜂窝网络\"); }else { Log.d(TAG, \"其他网络网络\"); } } } } ","date":"2023-06-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/:2:1","tags":["Android"],"title":"Android基础学习_即时网络监听","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/"},{"categories":["Andorid基础"],"content":"1.2.2 注册NetworkCallbackImpl NetworkCallbackImpl networkCallbackImpl = new NetworkCallbackImpl(); NetworkRequest.Builder builder = new NetworkRequest.Builder(); NetworkRequest request = builder.build(); ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); if(connectivityManager!=null){ connectivityManager.registerNetworkCallback(request,networkCallbackImpl); } ","date":"2023-06-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/:2:2","tags":["Android"],"title":"Android基础学习_即时网络监听","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E5%8D%B3%E6%97%B6%E7%BD%91%E7%BB%9C%E7%9B%91%E5%90%AC/"},{"categories":["Andorid基础"],"content":" 1.RecycleView ","date":"2023-06-18","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_recycleview/:0:0","tags":["Android"],"title":"Android基础学习_RecycleView简单使用","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_recycleview/"},{"categories":["Andorid基础"],"content":"1.1 简介 RecyclerView 可以让您轻松高效地显示大量数据。您提供数据并定义每个列表项的外观,而 RecyclerView 库会根据需要动态创建元素。 顾名思义,RecyclerView 会回收这些单个的元素。当列表项滚动出屏幕时,RecyclerView 不会销毁其视图。相反,RecyclerView 会对屏幕上滚动的新列表项重用该视图。这种重用可以显著提高性能,改善应用响应能力并降低功耗。 ","date":"2023-06-18","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_recycleview/:1:0","tags":["Android"],"title":"Android基础学习_RecycleView简单使用","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_recycleview/"},{"categories":["Andorid基础"],"content":"1.2 实现方法 ","date":"2023-06-18","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_recycleview/:2:0","tags":["Android"],"title":"Android基础学习_RecycleView简单使用","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_recycleview/"},{"categories":["Andorid基础"],"content":"1.2.1 布局(省略) 使用LinearLayout包含一个RecyclerView即可,设置RecyclerView的id即可 ","date":"2023-06-18","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_recycleview/:2:1","tags":["Android"],"title":"Android基础学习_RecycleView简单使用","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_recycleview/"},{"categories":["Andorid基础"],"content":"1.2.2 实现Adapter和ViewHolder 适配器Adapter,可以让开发者自定义recyclerview绑定的数据,自定义的Item的xml文件自定义每行的格式。 创建 创建一个Adapter类(名字什么都可以,这里是RecyclerViewAdapter) Adapter类继承RecyclerView.Adapter RecyclerView.Adapter内部的泛型定义为Adapter的内部类ViewHolder即可 内部添加私有成员变量Context和存储数据的List,并添加构造方法 具体步骤如下:(下列例子所使用的的item格式,其内部仅包含一个TextView) public class RecycleViewAdapter extends RecyclerView.Adapter\u003cRecycleViewAdapter.ViewHolder\u003e { private Context mContext; private ArrayList\u003cString\u003e fruitList; //构造方法 public RecycleViewAdapter(Context context, ArrayList\u003cString\u003e list){ this.mContext = context; this.fruitList = list; } @NonNull @Override public RecycleViewAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return null; } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { return null } @Override public int getItemCount() { return null } //自定义内部类ViewHolder public class ViewHolder extends RecyclerView.ViewHolder { public ViewHolder(@NonNull View itemView) { super(itemView); } } } onCreateViewHolder() 每当 RecyclerView 需要创建新的 ViewHolder 时,它都会调用此方法。此方法会创建并初始化 ViewHolder 及其关联的 View,但不会填充视图的内容,因为 ViewHolder 此时尚未绑定到具体数据。 @NonNull @Override public RecycleViewAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View root = LayoutInflater.from(mContext).inflate(R.layout.recycle_list_item,parent,false); RecycleViewAdapter.ViewHolder mHolder = new ViewHolder(root); return mHolder; } onBindViewHolder() RecyclerView 调用此方法将 ViewHolder 与数据相关联。此方法会提取适当的数据,并使用该数据填充 ViewHolder 的布局。例如,如果 RecyclerView 显示的是一个名称列表,该方法可能会在列表中查找适当的名称,并填充 ViewHolder 的 TextView。 @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.tv_rv.setText(fruitList.get(position)); } getItemCount() RecyclerView 调用此方法来获取数据集的大小。例如,在通讯簿应用中,这可能是地址总数。RecyclerView 使用此方法来确定什么时候没有更多的列表项可以显示。 @Override public int getItemCount() { return fruitList.size(); } ViewHolder public class ViewHolder extends RecyclerView.ViewHolder { TextView tv_rv; public ViewHolder(@NonNull View itemView) { super(itemView); tv_rv = itemView.findViewById(R.id.tv_rv); } } 使用 在MainActivity内部创建一个存储数据的List,并通过RecyclerViewAdapter()的构造方法传入。 获取当前的布局管理器,并设置到RecyclerView中 设置RecyclerView的Adapter 如果需要进行线段分割可以使用RecyclerView.serItemDecoration进行分割。 super.onCreate(savedInstanceState); setContentView(R.layout.activity_base); fruitList = new ArrayList\u003c\u003e(); recyclerView = findViewById(R.id.first_rv); addFruit(); // LinearLayoutManager layoutManager = new LinearLayoutManager(getApplicationContext()); //设置纵向或者横向 layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); //添加切割线 recyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.HORIZONTAL)); //设置布局 recyclerView.setLayoutManager(layoutManager); //适配器 adapter = new RecycleViewAdapter(getApplicationContext(),fruitList); recyclerView.setAdapter(adapter); ","date":"2023-06-18","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_recycleview/:2:2","tags":["Android"],"title":"Android基础学习_RecycleView简单使用","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_recycleview/"},{"categories":["Andorid基础"],"content":"补充 1.关于LayoutManager RecyclerView 中的列表项由 LayoutManager 类负责排列。 RecyclerView 库提供了三种布局管理器,用于处理最常见的布局情况: LinearLayoutManager 将各个项排列在一维列表中。 GridLayoutManager 将所有项排列在二维网格中: 如果网格垂直排列,GridLayoutManager 会尽量使每行中所有元素的宽度和高度相同,但不同的行可以有不同的高度。 如果网格水平排列,GridLayoutManager 会尽量使每列中所有元素的宽度和高度相同,但不同的列可以有不同的宽度。 StaggeredGridLayoutManager 与 GridLayoutManager 类似,但不要求同一行中的列表项具有相同的高度(垂直网格有此要求)或同一列中的列表项具有相同的宽度(水平网格有此要求)。其结果是,同一行或同一列中的列表项可能会错落不齐。 关于LayoutInflater 参考地址 Layout是什么鬼? 一个用于加载布局的系统服务,就是实例化与Layout XML文件对应的View对象,不能直接使用, 需要通过getLayoutInflater( )方法或getSystemService( )方法来获得与当前Context绑定的 LayoutInflater实例! LayoutInflater的用法 获取LayoutInflater实例的三种方法: LayoutInflater inflater1 = LayoutInflater.from(this); LayoutInflater inflater2 = getLayoutInflater(); LayoutInflater inflater3 = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); PS:后面两个其实底层走的都是第一种方法~ 加载布局的方法: public View inflate (int resource, ViewGroup root, boolean attachToRoot) 该方法的三个参数依次为: ①要加载的布局对应的资源id ②为该布局的外部再嵌套一层父布局,如果不需要的话,写null就可以了! ③是否为加载的布局文件的最外层套一层root布局,不设置该参数的话, 如果root不为null的话,则默认为true 如果root为null的话,attachToRoot就没有作用了! root不为null,attachToRoot为true的话,会在加载的布局文件最外层嵌套一层root布局; 为false的话,则root失去作用! 简单理解就是:是否为加载的布局添加一个root的外层容器~! 通过LayoutInflater.LayoutParams来设置相关的属性: 比如RelativeLayout还可以通过addRule方法添加规则,就是设置位置:是参考父容器呢? 还是参考子控件?又或者设置margin等等,这个由你决定~ ","date":"2023-06-18","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_recycleview/:2:3","tags":["Android"],"title":"Android基础学习_RecycleView简单使用","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_recycleview/"},{"categories":["Andorid基础"],"content":" 1.SmartRefreshLayout 简介:下拉刷新、上拉加载、二级刷新、淘宝二楼、RefreshLayout、OverScroll,Android智能下拉刷新框架,支持越界回弹、越界拖动,具有极强的扩展性,集成了几十种炫酷的Header和 Footer。 项目源地址 2.使用 ","date":"2023-06-18","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_smartrefreshlayout/:0:0","tags":["Android"],"title":"Android基础学习_SmartRefreshLayout","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_smartrefreshlayout/"},{"categories":["Andorid基础"],"content":"2.1 在build.gradle中添加如下 compile 'com.android.support:appcompat-v7:25.3.1' //必须 25.3.1 以上 // 注意:分包之后不会有默认的Header和Footer需要手动添加!还是原来的三种方法! implementation 'io.github.scwang90:refresh-layout-kernel:2.0.6' //核心必须依赖 implementation 'io.github.scwang90:refresh-header-classics:2.0.6' //经典刷新头 implementation 'io.github.scwang90:refresh-header-radar:2.0.6' //雷达刷新头 implementation 'io.github.scwang90:refresh-header-falsify:2.0.6' //虚拟刷新头 implementation 'io.github.scwang90:refresh-header-material:2.0.6' //谷歌刷新头 implementation 'io.github.scwang90:refresh-header-two-level:2.0.6' //二级刷新头 implementation 'io.github.scwang90:refresh-footer-ball:2.0.6' //球脉冲加载 implementation 'io.github.scwang90:refresh-footer-classics:2.0.6' //经典加载 android.useAndroidX=true android.enableJetifier=true 需要依赖 androidx.appcompat implementation 'androidx.appcompat:appcompat:1.0.0' //必须 1.0.0 以上 ","date":"2023-06-18","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_smartrefreshlayout/:0:1","tags":["Android"],"title":"Android基础学习_SmartRefreshLayout","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_smartrefreshlayout/"},{"categories":["Andorid基础"],"content":"2.2 在XML布局文件中添加 SmartRefreshLayout \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e \u003cLinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:orientation=\"vertical\"\u003e \u003c!--在Xml布局中引入SmartRefreshLayout--\u003e \u003ccom.scwang.smart.refresh.layout.SmartRefreshLayout android:id=\"@+id/refreshLayout\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\"\u003e \u003ccom.scwang.smart.refresh.header.ClassicsHeader android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\"/\u003e \u003cListView android:id=\"@+id/List_view\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:overScrollMode=\"never\" android:background=\"#fff\" /\u003e \u003ccom.scwang.smart.refresh.footer.ClassicsFooter android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\"/\u003e \u003c/com.scwang.smart.refresh.layout.SmartRefreshLayout\u003e \u003c/LinearLayout\u003e ","date":"2023-06-18","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_smartrefreshlayout/:0:2","tags":["Android"],"title":"Android基础学习_SmartRefreshLayout","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_smartrefreshlayout/"},{"categories":["Andorid基础"],"content":"2.3 在 Activity 或者 Fragment 中添加代码 RefreshLayout refreshLayout = (RefreshLayout)findViewById(R.id.refreshLayout); refreshLayout.setRefreshHeader(new ClassicsHeader(this)); refreshLayout.setRefreshFooter(new ClassicsFooter(this)); refreshLayout.setOnRefreshListener(new OnRefreshListener() { @Override public void onRefresh(RefreshLayout refreshlayout) { refreshlayout.finishRefresh(2000/*,false*/);//传入false表示刷新失败 } }); refreshLayout.setOnLoadMoreListener(new OnLoadMoreListener() { @Override public void onLoadMore(RefreshLayout refreshlayout) { refreshlayout.finishLoadMore(2000/*,false*/);//传入false表示加载失败 } }); ","date":"2023-06-18","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_smartrefreshlayout/:0:3","tags":["Android"],"title":"Android基础学习_SmartRefreshLayout","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_smartrefreshlayout/"},{"categories":["Andorid基础"],"content":"2.4 使用指定的 Header 和 Footer 1.全局设置 public class App extends Application { //static 代码段可以防止内存泄露 static { //设置全局的Header构建器 SmartRefreshLayout.setDefaultRefreshHeaderCreator(new DefaultRefreshHeaderCreator() { @Override public RefreshHeader createRefreshHeader(Context context, RefreshLayout layout) { layout.setPrimaryColorsId(R.color.colorPrimary, android.R.color.white);//全局设置主题颜色 return new ClassicsHeader(context);//.setTimeFormat(new DynamicTimeFormat(\"更新于 %s\"));//指定为经典Header,默认是 贝塞尔雷达Header } }); //设置全局的Footer构建器 SmartRefreshLayout.setDefaultRefreshFooterCreator(new DefaultRefreshFooterCreator() { @Override public RefreshFooter createRefreshFooter(Context context, RefreshLayout layout) { //指定为经典Footer,默认是 BallPulseFooter return new ClassicsFooter(context).setDrawableSize(20); } }); } } 注意:方法一 设置的Header和Footer的优先级是最低的,如果同时还使用了方法二、三,将会被其它方法取代 2.XML布局文件指定 \u003ccom.scwang.smart.refresh.layout.SmartRefreshLayout xmlns:app=\"http://schemas.android.com/apk/res-auto\" android:id=\"@+id/refreshLayout\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:background=\"#444444\" app:srlPrimaryColor=\"#444444\" app:srlAccentColor=\"@android:color/white\" app:srlEnablePreviewInEditMode=\"true\"\u003e \u003c!--srlAccentColor srlPrimaryColor 将会改变 Header 和 Footer 的主题颜色--\u003e \u003c!--srlEnablePreviewInEditMode 可以开启和关闭预览功能--\u003e \u003ccom.scwang.smart.refresh.header.ClassicsHeader android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\"/\u003e \u003cTextView android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:padding=\"@dimen/dimenPaddingCommon\" android:background=\"@android:color/white\" android:text=\"@string/description_define_in_xml\"/\u003e \u003ccom.scwang.smart.refresh.footer.ClassicsFooter android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\"/\u003e \u003c/com.scwang.smart.refresh.layout.SmartRefreshLayout\u003e 注意:方法二 XML设置的Header和Footer的优先级是中等的,会被方法三覆盖。而且使用本方法的时候,Android Studio 会有预览效果 3.Java代码设置 final RefreshLayout refreshLayout = (RefreshLayout) findViewById(R.id.refreshLayout); //设置 Header 为 贝塞尔雷达 样式 refreshLayout.setRefreshHeader(new BezierRadarHeader(this).setEnableHorizontalDrag(true)); //设置 Footer 为 球脉冲 样式 refreshLayout.setRefreshFooter(new BallPulseFooter(this).setSpinnerStyle(SpinnerStyle.Scale)); 3.具体配置参数 参考一下地址,官方页面还有许多有趣的UI加载页面,可以尝试一下。 [github样例地址](SmartRefreshLayout/art/md_property.md at master · scwang90/SmartRefreshLayout · GitHub) 4.上拉刷新下拉加载 具体操作可以通过重写SmartRefreshLayout的onrefresh()或者onLoadMore() refreshLayout.setOnRefreshListener(new OnRefreshListener() { @Override public void onRefresh(@NonNull RefreshLayout refreshLayout) { Log.d(TAG,\"onRefresh\"); //ListView或者RecycleView内元素加载或者刷新操作 refreshData(); //传入值为UI加载画面持续时间 refreshLayout.finishRefresh(1000);//传入false表示刷新失败 } }); refreshLayout.setOnLoadMoreListener(new OnLoadMoreListener(){ @Override public void onLoadMore(@NonNull RefreshLayout refreshLayout) { Log.d(TAG,\"onLoadMore\"); //ListView或者RecycleView内元素加载或者刷新操作 addData(); //传入值为UI加载画面持续时间 refreshLayout.finishLoadMore(1000);//传入false表示刷新失败 } }); ","date":"2023-06-18","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_smartrefreshlayout/:0:4","tags":["Android"],"title":"Android基础学习_SmartRefreshLayout","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_smartrefreshlayout/"},{"categories":["Andorid基础"],"content":"基于原生安卓的音乐播放器 ","date":"2023-06-10","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/:0:0","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)利用Message","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/"},{"categories":["Andorid基础"],"content":"1.BaseFragment public class BaseFragment extends Fragment { private static final String TAG = \"ZHAO_BaseFragment\"; private static final Boolean DEBUG = true; private static final String MUSIC_BG = \"music_bg\"; private static final String MUSIC_SRC = \"music_src\"; private Integer mMusic_bg; private Integer mMusic_src; private View root; public BaseFragment() { } public static BaseFragment newInstance(Integer mMusic_bg, Integer mMusic_src) { BaseFragment fragment = new BaseFragment(); Bundle args = new Bundle(); args.putInt(MUSIC_BG, mMusic_bg); args.putInt(MUSIC_SRC, mMusic_src); fragment.setArguments(args); return fragment; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); log(\"onAttach\"); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { this.mMusic_bg = getArguments().getInt(MUSIC_BG); this.mMusic_src = getArguments().getInt(MUSIC_SRC); } log(\"onCreate\"+mMusic_bg); } @Override public void onStart() { log(\"onStart\"+mMusic_bg); super.onStart(); } @Override public void onResume() { log(\"onResume\"+mMusic_bg); super.onResume(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { log(\"onCreateView\"+mMusic_bg); if(root == null){ root = inflater.inflate(R.layout.fragment_base,container,false); root.findViewById(R.id.ll_main).setBackgroundResource(getMusic_bg()); } return root; } public int getMusic_src(){ return mMusic_src; } public int getMusic_bg(){ return mMusic_bg; } public View getRoot(){ return root; } @Override public void onPause() { log(\"onPause\"+mMusic_bg); super.onPause(); } @Override public void onStop() { log(\"onStop\"+mMusic_bg); super.onStop(); } @Override public void onDestroy() { log(\"onDestroy\"+mMusic_bg); super.onDestroy(); } private void log(String str){ if(DEBUG){ Log.d(TAG,str); } } } fragment_xml部分 \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e \u003cFrameLayout 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\" tools:context=\".BaseFragment\"\u003e \u003cLinearLayout android:id=\"@+id/ll_main\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:background=\"@drawable/bg\" android:gravity=\"center\" android:orientation=\"vertical\"\u003e \u003cImageView android:id=\"@+id/im_music\" android:layout_width=\"240dp\" android:layout_height=\"240dp\" android:layout_gravity=\"center_horizontal\" android:layout_margin=\"15dp\" android:src=\"@drawable/music_disk\" /\u003e \u003cSeekBar android:id=\"@+id/sb_music\" android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:progressBackgroundTint=\"#fff\"/\u003e \u003cRelativeLayout android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:paddingLeft=\"8dp\" android:paddingRight=\"8dp\" \u003e \u003cTextView android:id=\"@+id/tv_progress\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:textColor=\"#fff\" android:text=\"00:00\" /\u003e \u003cTextView android:id=\"@+id/tv_total\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:layout_alignParentRight=\"true\" android:textColor=\"#fff\" android:text=\"00:00\" /\u003e \u003c/RelativeLayout\u003e \u003cLinearLayout android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:orientation=\"horizontal\" \u003e \u003cButton android:id=\"@+id/btn_play\" android:layout_width=\"0dp\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:paddingLeft=\"10dp\" android:paddingRight=\"10dp\" android:text=\"播放音乐\"/\u003e \u003cButton android:id=\"@+id/btn_pause\" android:layout_width=\"0dp\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:paddingLeft=\"10dp\" android:paddingRight=\"10dp\" android:text=\"暂停播放\"/\u003e \u003cButton android:id=\"@+id/btn_continue\" android:layout_width=\"0dp\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:paddingLeft=\"10dp\" android:paddingRight=\"10dp\" android:text=\"继续播放\"/\u003e \u003cButton","date":"2023-06-10","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/:1:0","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)利用Message","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/"},{"categories":["Andorid基础"],"content":"Ui界面 \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e \u003cLinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\" android:id=\"@+id/ll_main\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:gravity=\"center\" android:orientation=\"vertical\"\u003e \u003ccom.google.android.material.tabs.TabLayout android:id=\"@+id/tab_layout\" android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" /\u003e \u003candroidx.viewpager2.widget.ViewPager2 android:id=\"@+id/viewpager2\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" /\u003e \u003c/LinearLayout\u003e ","date":"2023-06-10","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/:2:0","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)利用Message","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/"},{"categories":["Andorid基础"],"content":"MainActivity部分 public class MainActivity extends AppCompatActivity { private static final String TAG = \"ZHAO_MainActivity\"; private static final Boolean DEBUG = true; private static final int EVENT_PLAY = 1000; private static final int EVENT_PAUSE = 1001; private static final int EVENT_RESUME = 1002; private static final int EVENT_EXIT = 1003; private static final int EVENT_UPDATE_MUSIC_INFO = 1004; private static final int EVENT_SEEKBAR_CHANGED = 1005; private static final int EVENT_UPDATE_MUSIC_SRC = 1006; private ViewPager2 viewPager; private TabLayout tab_layout; private ArrayList\u003cBaseFragment\u003e fragmentList; private ArrayList\u003cString\u003e tab_label; private LinearLayout ll_main; private ImageView im_music; private SeekBar sb_music; private TextView tv_progress; private TextView tv_total; private Button btn_play,btn_pause,btn_resume,btn_exit; private ObjectAnimator animator; private BaseFragment currentFragment; private Messenger mMessenger ; private Handler mHandler = new Handler(){ @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); switch (msg.what){ case EVENT_UPDATE_MUSIC_INFO: log(\"EVENT_UPDATE_MUSIC_INFO\"); Bundle data = msg.getData(); int progress = data.getInt(\"progress\"); int duration = data.getInt(\"duration\"); sb_music.setMax(duration); sb_music.setProgress(progress); tv_progress.setText(msToSeekBar(progress)); tv_total.setText(msToSeekBar(duration)); sb_music.setProgress(progress); break; } } }; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mMessenger = new Messenger(service); } @Override public void onServiceDisconnected(ComponentName name) { mMessenger = null; } }; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); log(\"onCreate\"); init(); bindService(); } @Override protected void onResume() { super.onResume(); log(\"onResume\"); } //初始化用 private void init(){ viewPager = findViewById(R.id.viewpager2); tab_layout = findViewById(R.id.tab_layout); //生成ViewPager fragmentList = new ArrayList\u003c\u003e(); fragmentList.add(BaseFragment.newInstance(R.drawable.bg_jiaohuanyusheng,R.raw.jjay_jiaohuanyusheng)); fragmentList.add(BaseFragment.newInstance(R.drawable.bg_shengsheng,R.raw.jjay_shengsheng)); fragmentList.add(BaseFragment.newInstance(R.drawable.bg_xuebuhui,R.raw.jjay_xuebuhui)); MyFragmentAdapter myFragmentAdapter = new MyFragmentAdapter(this,fragmentList); viewPager.setAdapter(myFragmentAdapter); //初始化tabLabel tab_label = new ArrayList\u003c\u003e(); tab_label.add(\"交换余生\"); tab_label.add(\"生生\"); tab_label.add(\"学不会\"); //创建滑动动画联动ViewPager和TabLayout new TabLayoutMediator(tab_layout, viewPager,true,false ,new TabLayoutMediator.TabConfigurationStrategy() { @Override public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) { log(\"table_layout position : \"+position+\",label name : \"+tab_label.get(position)); tab.setText(tab_label.get(position)); } }).attach(); //需要设置TabLayout滑动监听来查看当前的Fragment //获取当前的Fragment //由于按照ViewPager默认生成的Fragment的Tag的命名是\"f\" + position 我们可以使用如下方法获取当前正在运行的Fragment tab_layout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { initSelectedTabItem(); } @Override public void onTabUnselected(TabLayout.Tab tab) { } @Override public void onTabReselected(TabLayout.Tab tab) { } }); } private void initSelectedTabItem(){ log(tab_layout.getTabAt(viewPager.getCurrentItem()).getText()+\"\"); currentFragment = (BaseFragment) getSupportFragmentManager().findFragmentByTag(\"f\"+viewPager.getCurrentItem()); log(currentFragment.getTag()); //并且切换有页面需要发送音乐源文件的SRC Message msg = mHandler.obtainMessage(EVENT_UPDATE_MUSIC_SRC); msg.obj = currentFragment.getMusic_src(); msg.replyTo = new Messenger(mHandler); try { mMessenger.send(msg); } catch (RemoteException e) { throw new RuntimeException(e); } // 切换的时候控件需要置空 ll_main = null; im_music = null; ","date":"2023-06-10","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/:3:0","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)利用Message","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/"},{"categories":["Andorid基础"],"content":"service部分 public class MusicPlayerService extends Service { private static final String TAG = \"ZHAO_MusicPlayerService\"; private static final boolean DEBUG = true; private static final int EVENT_PLAY = 1000; private static final int EVENT_PAUSE = 1001; private static final int EVENT_RESUME = 1002; private static final int EVENT_EXIT = 1003; private static final int EVENT_UPDATE_MUSIC_INFO = 1004; private static final int EVENT_SEEKBAR_CHANGED = 1005; private static final int EVENT_UPDATE_MUSIC_SRC = 1006; private int mMusic_src; private MediaPlayer mediaPlayer; private Timer timer; private TimerTask timerTask; private Messenger mMessenger; private Messenger mClientMessenger; private Handler mHandler = new Handler(){ @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); switch (msg.what){ case EVENT_PLAY: log(\"EVENT_PLAY\"); play(); break; case EVENT_PAUSE: log(\"EVENT_PAUSE\"); pause(); break; case EVENT_RESUME: log(\"EVENT_RESUME\"); resume(); break; case EVENT_EXIT: log(\"EVENT_EXIT\"); stop(); break; case EVENT_SEEKBAR_CHANGED: log(\"EVENT_SEEKBAR_CHANGED\"); int progress = (int)msg.obj; seekTo(progress); break; case EVENT_UPDATE_MUSIC_SRC: if(mediaPlayer != null){ pause(); } log(\"EVENT_UPDATE_MUSIC_SRC\"); mClientMessenger = msg.replyTo; int music_src = (int)msg.obj; compareMusicSrc(music_src); } } }; @Override public void onCreate() { super.onCreate(); log(\"onCreate\"); mediaPlayer = new MediaPlayer(); mMessenger = new Messenger(mHandler); mMusic_src = R.raw.jjay_jiaohuanyusheng; } @Nullable @Override public IBinder onBind(Intent intent) { log(\"onBind\"); return mMessenger.getBinder(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { log(\"onDestroy\"); super.onDestroy(); } private void addTimer(){ timer = new Timer(); timerTask = new TimerTask() { @Override public void run() { Message msg = mHandler.obtainMessage(EVENT_UPDATE_MUSIC_INFO); Bundle bundle = new Bundle(); int duration = mediaPlayer.getDuration(); int progress = mediaPlayer.getCurrentPosition(); bundle.putInt(\"duration\",duration); bundle.putInt(\"progress\",progress); msg.setData(bundle); try { mClientMessenger.send(msg); } catch (RemoteException e) { throw new RuntimeException(e); } } }; timer.schedule(timerTask,5,1000); } private void removeTimer(){ if(timer !=null \u0026\u0026 timerTask!=null){ timerTask.cancel(); timer.purge(); } } private void compareMusicSrc(int music_src){ log(\"compareMusicSrc\"); if(mMusic_src != music_src){ mMusic_src = music_src; mediaPlayer.reset(); mediaPlayer = MediaPlayer.create(getApplicationContext(),mMusic_src); } } //播放音乐 private void play(){ log(\"play\"); mediaPlayer.start(); addTimer(); } //暂停音乐 private void pause(){ log(\"pause\"); mediaPlayer.pause(); removeTimer(); } //继续音乐 private void resume(){ log(\"resume\"); mediaPlayer.start(); addTimer(); } private void stop(){ mediaPlayer.stop(); mediaPlayer.release(); } private void seekTo(int ms){ log(\"seekTo\"); mediaPlayer.seekTo(ms); } private void log(String str){ if(DEBUG){ Log.d(TAG,str); } } } ","date":"2023-06-10","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/:4:0","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)利用Message","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/"},{"categories":["Andorid基础"],"content":"思路 ","date":"2023-06-10","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/:5:0","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)利用Message","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/"},{"categories":["Andorid基础"],"content":"BaseFragment 提供了一个getInstance静态方法,创建实例。并在内部设置两个变量, music_src:音乐源 music_bg:音乐背景 获取BaseFragment实例的时候,会在onCreate方法中对music_src和music_bg进行赋值 并在onCreateView方法中,对背景进行赋值,设置对应的背景。 ","date":"2023-06-10","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/:5:1","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)利用Message","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/"},{"categories":["Andorid基础"],"content":"主界面 Service和Activity互相通信的方法之Activity向Service通信 在MyService中创建Handler,并以该Handler创建Messenger对象,在OnBind方法中返回Messenger.getBinder方法返回Binder对象。在Activity中创建ServiceConnection,并重写该接口的onServiceConnected和onServiceDisconnected连接方法。在onServiceConnected内利用传入参数Binder创建Messenger对象,该Messenger(信使)指向Service的Handler对象。 Service和Activity互相通信的方法之AService向Activity通信 通过将Activity传向Service的Message内的replyTo设置为由Activity类的Handler生成的Messenger,并在Service内接收到该消息之后,获取msg.replyTo的Messenger对象,即可以向Activity的Handler对象发送信息。 主界面的设置 设置如下几种事件类型来传递消息。 private static final int EVENT_PLAY = 1000; private static final int EVENT_PAUSE = 1001; private static final int EVENT_RESUME = 1002; private static final int EVENT_EXIT = 1003; private static final int EVENT_UPDATE_MUSIC_INFO = 1004; private static final int EVENT_SEEKBAR_CHANGED = 1005; private static final int EVENT_UPDATE_MUSIC_SRC = 1006; 利用ViewPager2和TabLayout,建立页面滑动效果,并在页面切换的时候,传递Music_src,对播放源进行重置。但是由于在Fragment的声明周期后于Avtivity,导致在进入主界面的时候,第一个页面的控件无法赋值和设置点击函数,必须在接触TabLayout的滑动监听效果,去重置赋值。 点击播放按钮,生成EVENT_PLAY消息发送,并每秒接受从service发送的EVENT_UPDATE_MUSIC_INFO更新SeekBar等信息. 点击暂停按钮,生成EVENT_PAUSE消息发送 点击继续按钮,生成EVENT_RESUME消息发送 点击退出按钮,生成EVENT_PAUSE,并在本页面调用finish() 滑动界面,触发addOnTabSelectedListener.onTabSelected 发送EVENT_PAUSE之后,发送EVENT_UPDATE_MUSIC_SRC,接着对控件重新赋值 ","date":"2023-06-10","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/:5:2","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)利用Message","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/"},{"categories":["Andorid基础"],"content":"Service 接收EVENT_PLAY信息,调用play(),mediaPlay重新赋值,添加计时器任务。 接收EVENT_PAUSE,调用pause(),timerTask.cancel(),并让Timer.purge清除被标记为cancel的任务 接收EVENT_RESUME,调用resume(),重新为timer添加timerTask,向Activity传输任务。 接收EVENT_EXIT,调用stop() 接收EVENT_SEEKBAR_CHANGED,调用seekTo,将mediaplayer的进度调到参数位置 接收EVENT_UPDATE_MUSIC_SRC,更新音乐源,并重新创建赋值mediaplayer ","date":"2023-06-10","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/:5:3","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)利用Message","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/"},{"categories":["Andorid基础"],"content":"问题点 1.如何获取第一个Fragment并赋值 2.使用Aidl进行进程间通信。 3.对某些资源的释放。 ","date":"2023-06-10","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/:5:4","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)利用Message","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/"},{"categories":["Andorid基础"],"content":"效果图 ","date":"2023-06-10","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/:5:5","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)利用Message","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer2%E5%AE%9E%E7%8E%B0%E9%80%9A%E4%BF%A1/"},{"categories":["Andorid基础"],"content":"TelephonyManager类笔记 提供对有关设备上的电话服务的信息的访问。应用程序可以使用此类中的方法来确定电话服务和状态,以及访问某些类型的用户信息。应用程序还可以注册一个侦听器以接收电话状态更改的通知。 //TelePhonyManager对象的获取 mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); 需要注意的是,需要在AndroidManifest.xml内部声明权限。 \u003c!--允许读取电话状态SIM的权限--\u003e \u003cuses-permission android:name=\"android.permission.READ_PHONE_STATE\" /\u003e \u003c!-- 这个权限用于进行网络定位 --\u003e \u003cuses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" /\u003e 部分TelephonyManager由于权限问题,无法输出显示。 public class MainActivity extends AppCompatActivity implements View.OnClickListener { private static final String TAG = \"zhao\"; private Button btn_click; private TelephonyManager mTelephonyManager; private StringBuilder sb; private TextView tv_message; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { Log.d(\"MainActivity\", \"MainActivity onCreate\"); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv_message = findViewById(R.id.tv_message); btn_click = findViewById(R.id.btn_click); sb = new StringBuilder(); mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); btn_click.setOnClickListener(this); } private void setMessage(){ //获取手机当前位置 //\u003cuses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/\u003e // String cellLocation = mTelephonyManager.getCellLocation().toString(); // sb.append(\"CellLocation Info : \").append(cellLocation).append(\"\\n\"); /* 获取手机的卡槽个数 */ int phoneCount = mTelephonyManager.getPhoneCount(); sb.append(\"phoneCount : \").append(phoneCount).append(\"\\n\"); /* * 获取数据连接状态 * DATA_CONNECTED 数据连接状态:已连接 * DATA_CONNECTING 数据连接状态:正在连接 * DATA_DISCONNECTED 数据连接状态:断开 * DATA_SUSPENDED 数据连接状态:暂停 */ int dataState = mTelephonyManager.getDataState(); setDataState(dataState); /* * 返回唯一的设备ID * 如果是GSM网络,返回IMEI;如果是CDMA网络,返回MEID;如果设备ID是不可用的返回null */ // String deviceId = mTelephonyManager.getDeviceId(); // sb.append(\"deviceId : \").append(deviceId).append(\"\\n\"); // // /* * 返回设备的软件版本号 * 例如:GSM手机的IMEI/SV码,如果软件版本是返回null,如果不可用返回null */ // String deviceSoftwareVersion = mTelephonyManager.getDeviceSoftwareVersion(); // sb.append(\"deviceSoftwareVersion : \").append(deviceSoftwareVersion).append(\"\\n\"); // /* * 返回手机号码 * 对于GSM网络来说即MSISDN,如果不可用返回null * */ // @SuppressLint(\"MissingPermission\") String line1Number = mTelephonyManager.getLine1Number(); // sb.append(\"line1Number : \").append(line1Number).append(\"\\n\"); // // // /* // * 返回当前设备附近设备的信息 // * getNeighboringCellInfo() 在\u003e=29 被删除 // */ // // /* // * 返回ISO标准的国家码,即国际长途区号 // */ String networkCountryIso = mTelephonyManager.getNetworkCountryIso(); sb.append(\"networkCountryIso : \").append(networkCountryIso).append(\"\\n\"); // // /* // * 返回MCC+MNC代码 (SIM卡运营商国家代码和运营商网络代码)(IMSI) // */ String networkOperator = mTelephonyManager.getNetworkOperator(); sb.append(\"networkOperator : \").append(networkOperator).append(\"\\n\"); // // /* // * 返回移动网络运营商的名字(SPN) // */ String networkOperatorName = mTelephonyManager.getNetworkOperatorName(); sb.append(\"networkOperatorName : \").append(networkOperatorName).append(\"\\n\"); // // // /* // * 获取网络类型 // * NETWORK_TYPE_CDMA 网络类型为CDMA // * NETWORK_TYPE_EDGE 网络类型为EDGE // * NETWORK_TYPE_EVDO_0 网络类型为EVDO0 // * NETWORK_TYPE_EVDO_A 网络类型为EVDOA // * NETWORK_TYPE_GPRS 网络类型为GPRS // * NETWORK_TYPE_HSDPA 网络类型为HSDPA // * NETWORK_TYPE_HSPA 网络类型为HSPA // * NETWORK_TYPE_HSUPA 网络类型为HSUPA // * NETWORK_TYPE_UMTS 网络类型为UMTS // * // * 在中国,联通的3G为UMTS或HSDPA,移动和联通的2G为GPRS或EGDE,电信的2G为CDMA,电信的3G为EVDO // */ // @SuppressLint(\"MissingPermission\") int networkType = mTelephonyManager.getNetworkType(); // setNetWorkType(networkType); // // /* // * 返回设备的类型 // * // * PHONE_TYPE_CDMA 手机制式为CDMA,电信 // * PHONE_TYPE_GSM 手机制式为GSM,移动和联通 // * PHONE_TYPE_NONE 手机制式未知 // */ int phoneType = mTelephonyManager.getPhoneType(); setPhoneType(phoneType); // // /* // * 返回SIM卡提供商的国家代码 // */ String simCountryIso = mTelephonyManager.getSimCountryIso(); sb.append(\"simCountryIso : \").append(simCountryIso).append(\"\\n\"); // // /* // * 返回MCC+MNC代码 (SIM卡运营商国家代码和运营商网络代码)(IMSI) // */ String sim","date":"2023-06-06","objectID":"/telephonymanager/:0:0","tags":["Android"],"title":"TelePhonyManager","uri":"/telephonymanager/"},{"categories":["Andorid基础"],"content":"基于原生安卓的音乐播放器 ","date":"2023-06-04","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/:0:0","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/"},{"categories":["Andorid基础"],"content":"1.Ui界面 \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e \u003cLinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\" android:id=\"@+id/ll_main\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:background=\"@drawable/bg\" android:gravity=\"center\" android:orientation=\"vertical\"\u003e \u003cLinearLayout android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:orientation=\"horizontal\" \u003e \u003cTextView android:layout_width=\"wrap_content\" android:layout_height=\"30dp\" android:layout_weight=\"1\" android:textSize=\"17sp\" android:textColor=\"@color/black\" android:background=\"@color/white\" android:text=\"选取歌曲\"/\u003e \u003cSpinner android:id=\"@+id/spinner\" android:layout_weight=\"1\" android:background=\"@color/white\" android:layout_width=\"wrap_content\" android:layout_height=\"30dp\" /\u003e \u003c/LinearLayout\u003e \u003cImageView android:id=\"@+id/lv_music\" android:layout_width=\"240dp\" android:layout_height=\"240dp\" android:layout_gravity=\"center_horizontal\" android:layout_margin=\"15dp\" android:src=\"@drawable/music_disk\" /\u003e \u003cSeekBar android:id=\"@+id/sb\" android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:progressBackgroundTint=\"#fff\"/\u003e \u003cRelativeLayout android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:paddingLeft=\"8dp\" android:paddingRight=\"8dp\" \u003e \u003cTextView android:id=\"@+id/tv_progress\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:textColor=\"#fff\" android:text=\"00:00\" /\u003e \u003cTextView android:id=\"@+id/tv_total\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:layout_alignParentRight=\"true\" android:textColor=\"#fff\" android:text=\"00:00\" /\u003e \u003c/RelativeLayout\u003e \u003cLinearLayout android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:orientation=\"horizontal\" \u003e \u003cButton android:id=\"@+id/btn_play\" android:layout_width=\"0dp\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:paddingLeft=\"10dp\" android:paddingRight=\"10dp\" android:text=\"播放音乐\"/\u003e \u003cButton android:id=\"@+id/btn_pause\" android:layout_width=\"0dp\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:paddingLeft=\"10dp\" android:paddingRight=\"10dp\" android:text=\"暂停播放\"/\u003e \u003cButton android:id=\"@+id/btn_continue\" android:layout_width=\"0dp\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:paddingLeft=\"10dp\" android:paddingRight=\"10dp\" android:text=\"继续播放\"/\u003e \u003cButton android:id=\"@+id/btn_exit\" android:layout_width=\"0dp\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:paddingLeft=\"10dp\" android:paddingRight=\"10dp\" android:text=\"退出播放\"/\u003e \u003c/LinearLayout\u003e \u003c/LinearLayout\u003e ","date":"2023-06-04","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/:1:0","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/"},{"categories":["Andorid基础"],"content":"MainActivity部分 public class MainActivity extends AppCompatActivity { private static final String TAG = \"MainActivity\"; private static final boolean DEBUG = true; private LinearLayout ll_main ; private Spinner spinner; private ImageView lv_music; private static SeekBar seekBar; private static TextView tv_progress; private static TextView tv_total; private Button btn_play,btn_pause,btn_continue,btn_exit; private ObjectAnimator objectAnimator; private String[] music_name; private HashMap\u003cString,Integer[]\u003e map; private MusicControllerService.musicController musicController; public static Handler mHandler = new Handler(){ @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); Bundle data = msg.getData(); int duration = data.getInt(\"duration\");//获取从子线程发来的音乐总时长 int currentPosition = data.getInt(\"currentPosition\");//获取从子线程发来的播放进度 seekBar.setMax(duration);//设置seekBar的最大歌曲时长 seekBar.setProgress(currentPosition);//设置seekBar的进度 String sDuration = msToMinSec(duration); String currentPos = msToMinSec(currentPosition); tv_progress.setText(currentPos); tv_total.setText(sDuration); } }; private ServiceConnection conn = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { musicController = (MusicControllerService.musicController)service; } @Override public void onServiceDisconnected(ComponentName name) { musicController = null; } }; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init(){ ll_main = findViewById(R.id.ll_main); spinner = findViewById(R.id.spinner); lv_music = findViewById(R.id.lv_music); seekBar = findViewById(R.id.sb); tv_progress = findViewById(R.id.tv_progress); tv_total = findViewById(R.id.tv_total); btn_play =findViewById(R.id.btn_play); btn_pause = findViewById(R.id.btn_pause); btn_continue = findViewById(R.id.btn_continue); btn_exit = findViewById(R.id.btn_exit); music_name = getResources().getStringArray(R.array.music_tag); //构建哈希表 map = new HashMap\u003cString ,Integer[]\u003e(){ { put(music_name[0],new Integer[]{R.raw.jjay_xuebuhui,R.drawable.bg_xuebuhui}); put(music_name[1],new Integer[]{R.raw.jjay_shengsheng,R.drawable.bg_shengsheng}); put(music_name[2],new Integer[]{R.raw.jjay_jiaohuanyusheng,R.drawable.bg_jiaohuanyusheng}); } }; onClick onClick = new onClick(); btn_play.setOnClickListener(onClick); btn_pause.setOnClickListener(onClick); btn_continue.setOnClickListener(onClick); btn_exit.setOnClickListener(onClick); //对象是lv_music,动作是rotation objectAnimator = ObjectAnimator.ofFloat(lv_music,\"rotation\",0,360.0F); //设置持续时间,10s一周 objectAnimator.setDuration(10000); //设置旋转为匀速旋转 objectAnimator.setInterpolator(new LinearInterpolator()); //设置重复次数,-1为持续 objectAnimator.setRepeatCount(-1); //设置spinner //简历adapter并绑定数据源 ArrayAdapter adapter = new ArrayAdapter\u003c\u003e(getApplicationContext(), android.R.layout.simple_spinner_item,music_name); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); //绑定adapter控件 spinner.setAdapter(adapter); //设置点击事件 spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView\u003c?\u003e parent, View view, int position, long id) { log(\"onItemSelected , position:\"+position+\",music src:\"+map.get(music_name[position])[0]); ll_main.setBackgroundResource(map.get(music_name[position])[1]); musicController.setMusicSrc(map.get(music_name[position])[0]); objectAnimator.pause(); } @Override public void onNothingSelected(AdapterView\u003c?\u003e parent) { } }); Intent intent = new Intent(getApplicationContext(), MusicControllerService.class); bindService(intent,conn,BIND_AUTO_CREATE); seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { //当音乐停止 if(progress == seekBar.getMax()){ objectAnimator.pause(); } //判断是不是用户拖动 if(fromUser){ musicControlle","date":"2023-06-04","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/:2:0","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/"},{"categories":["Andorid基础"],"content":"service部分 public class MusicControllerService extends Service { private static final String TAG = \"MusicController\"; private static final boolean DEBUG = true; private MediaPlayer mediaPlayer; private Timer timer; private Integer music_src; public void setMusic_src(Integer music_src) { music_src = music_src; } @Nullable @Override public IBinder onBind(Intent intent) { return new musicController(); } @Override public void onCreate() { super.onCreate(); mediaPlayer = new MediaPlayer(); music_src = R.raw.jjay_xuebuhui; } @Override public int onStartCommand(Intent intent, int flags, int startId) { return super.onStartCommand(intent, flags, startId); } //增加计时器 private void addTimer(){ if(timer == null){ timer = new Timer(); TimerTask timerTask = new TimerTask() { @Override public void run() { int duration = mediaPlayer.getDuration();//获取歌曲总时长 int currentPos = mediaPlayer.getCurrentPosition();//获取当前进度 Message msg = new Message(); //将音乐的总时长和播放进度封装到消息对象中 Bundle bundle = new Bundle(); bundle.putInt(\"duration\",duration); bundle.putInt(\"currentPosition\",currentPos); msg.setData(bundle); //将消息发送到主线程的消息队列 MainActivity.mHandler.sendMessage(msg); } }; //开启计时任务后的5ms,第一次执行task之后,每500ms执行一次 timer.schedule(timerTask,5,500); } } public class musicController extends Binder{ //播放音乐 public void play(){ log(\"musicController play\"); mediaPlayer.reset(); mediaPlayer = MediaPlayer.create(getApplicationContext(),music_src); mediaPlayer.start(); addTimer(); } //暂停 public void pause(){ log(\"musicController pause\"); mediaPlayer.pause(); } //继续 public void resume(){ log(\"musicController resume\"); mediaPlayer.start();//播放 不会被重置 } //停止 public void stop(){ log(\"musicController stop\"); mediaPlayer.stop();//停止音乐 mediaPlayer.release(); try{ timer.cancel(); }catch (Exception e){ e.printStackTrace(); } } //打带 public void seekTo(int ms){ mediaPlayer.seekTo(ms); } public void setMusicSrc(int musicSrc){ music_src = musicSrc; mediaPlayer.stop(); mediaPlayer.release(); mediaPlayer = MediaPlayer.create(getApplicationContext(),musicSrc); log(String.valueOf(musicSrc)); } } @Override public void onDestroy() { super.onDestroy(); } private void log(String str){ if(DEBUG){ Log.d(TAG,str); } } } ","date":"2023-06-04","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/:3:0","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/"},{"categories":["Andorid基础"],"content":"思路 ","date":"2023-06-04","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/:4:0","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/"},{"categories":["Andorid基础"],"content":"UI界面 创建一个黑胶圆盘,提供播放动画效果 创建一个SeekBar,提供音乐进度条效果 创建四个Button,提供播放,暂停,继续,退出的点击事件 ","date":"2023-06-04","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/:4:1","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/"},{"categories":["Andorid基础"],"content":"主界面 使用ObjectAnimetor,设置黑胶圆盘的动画效果 提供一个spinner供选择歌曲 在Spinner选择item的时候,调用onItemSelete切换背景和歌曲源 提供一个Service,提供播放,暂停,继续,退出的方法实现 需要重写ServiceConnetion 并调用bindservice 记住 在onDestory的时候调用unbindService方法 ","date":"2023-06-04","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/:4:2","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/"},{"categories":["Andorid基础"],"content":"Service Service内部维护三个成员变量 1.MediaPlayer 音乐播放器 2.Timer类 3.音乐源节点 并在内部创建一个内部类继承iBInder 内部类内提供调用的播放,暂停,继续,退出的方法。 Timer类作为SeekBar的设置,提供一个TimerTask,在每0.5s的时候构建一个Message,将播放的进度返回,主界面接受消息并设置SeekBar。 ","date":"2023-06-04","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/:4:3","tags":["Android"],"title":"Android基础学习_MediaPlayer(音乐播放器)","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_mediaplayer/"},{"categories":["Andorid基础"],"content":"1.Android开发 数据存储 ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:0:0","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.1 共享参数sharePrefrence 本节介绍Android的键值对存储方式——共享参数SharedPreferences的使用方法,包括:如何将数据保存到共享参数,如何从共享参数读取数据,如何使用共享参数实现登录页面的记住密码功能,如何利用设备浏览器找到共享参数文件。 ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:1:0","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.1.1 共享参数的使用方法 SharedPreferences是Android的一个轻量级存储工具,它采用的存储结构是Key-Value的键值对方式,类似于Java的Properties,二者都是把Key-Value的键值对保存在配置文件中。不同的是,Properties的文件内容形如Key=Value,而SharedPreferences的存储介质是XML文件,且以XML标记保存键值对。保存共享参数键值对信息的文件路径为:/data/data/应用包名/shared_prefs/文件名.xml。下面是一个共享参数的XML文件例子: \u003c?xml version='1.0' encoding='utf-8' standalone='yes' ?\u003e \u003cmap\u003e \u003cint name=\"isMarried\" value=\"0\" /\u003e \u003cstring name=\"name\"\u003e李易\u003c/string\u003e \u003cfloat name=\"weight\" value=\"90.0\" /\u003e \u003cint name=\"age\" value=\"24\" /\u003e \u003clong name=\"height\" value=\"180\" /\u003e \u003c/map\u003e 基于XML格式的特点,共享参数主要用于如下场合: (1)简单且孤立的数据。若是复杂且相互关联的数据,则要保存于关系数据库。 (2)文本形式的数据。若是二进制数据,则要保存至文件。 (3)需要持久化存储的数据。App退出后再次启动时,之前保存的数据仍然有效。 实际开发中,共享参数经常存储的数据包括:App的个性化配置信息、用户使用App的行为信息、临时需要保存的片段信息等。 共享参数对数据的存储和读取操作类似于Map,也有存储数据的put方法,以及读取数据的get方法。调 用getSharedPreferences方法可以获得共享参数实例,获取代码示例如下: // 从share.xml获取共享参数实例 SharedPreferences sp = getSharedPreferences(\"user_info\", MODE_PRIVATE); 由以上代码可知,getSharedPreferences方法的第一个参数是文件名,填share表示共享参数的文件名 是share.xml;第二个参数是操作模式,填MODE_PRIVATE表示私有模式。往共享参数存储数据要借助于Editor类,保存数据的代码示例如下: SharedPreferences.Editor editor = sp.edit(); editor.putString(\"name\",name); editor.putInt(\"age\",Integer.parseInt(age)); editor.putLong(\"height\",Long.parseLong(height)); editor.putFloat(\"weight\",Float.parseFloat(weight)); editor.putInt(\"isMarried\",(ck_isMarried.isChecked()?1:0)); editor.commit(); 从共享参数读取数据相对简单,直接调用共享参数实例的get方法即可读取键值,注意 get方法 的第二个参数表示默认值,读取数据的代码示例如下: StringBuilder sb = new StringBuilder(); sb.append(sp.getString(\"name\",\"\")).append(\"\\n\") .append(sp.getInt(\"age\",0)).append(\"\\n\") .append(sp.getLong(\"height\",0)).append(\"\\n\") .append(sp.getFloat(\"weight\",0.0f)).append(\"\\n\") .append((sp.getInt(\"isMarried\",0)==1)?\"已婚\":\"未婚\"); tv_result.setText(sb.toString()); ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:1:1","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.2 数据库SQLite 本节介绍Android的数据库存储方式—SQLite的使用方法,包括:SQLite用到了哪些SQL语法,如何使用 数据库管理器操纵SQLite,如何使用数据库帮助器简化数据库操作等,以及如何利用SQLite改进登录页 面的记住密码功能。 ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:2:0","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.2.1 SQL的基本语法 此处省略 ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:2:1","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.2.2 数据库管理器SQLiteDatabase SQL语句毕竟只是SQL命令,若要在Java代码中操纵SQLite,还需专门的工具类。SQLiteDatabase便是 Android提供的SQLite数据库管理器,开发者可以在活动页面代码调用openOrCreateDatabase方法获 取数据库实例,参考代码如下: public void onClick(View v) { if(v.getId() == R.id.btn_create_db){ // 创建名为test.db的数据库。数据库如果不存在就创建它,如果存在就打开它 sqLiteDatabase = openOrCreateDatabase( getFilesDir() + File.separator + \"test.db\", Context.MODE_PRIVATE, null); //输出创建成功 String desc = String.format(\"数据库路径:%s,创建%s\",sqLiteDatabase.getPath(),(sqLiteDatabase!=null)?\"成功\":\"失败\"); tv_show.setText(desc); }else if(v.getId() == R.id.btn_delete_db){ String path = sqLiteDatabase.getPath(); boolean flag = deleteDatabase(path); String desc = String.format(\"数据库路径:%s,创建%s\",path,(flag)?\"成功\":\"失败\"); tv_show.setText(desc); } } 获得数据库实例之后,就能对该数据库开展各项操作了。数据库管理器SQLiteDatabase提供了若干操作 数据表的API,常用的方法有3类,列举如下: 管理类,用于数据库层面的操作 openDatabase:打开指定路径的数据库。 isOpen:判断数据库是否已打开。 close:关闭数据库。 getVersion:获取数据库的版本号。 setVersion:设置数据库的版本号。 事务类,用于事务层面的操作 beginTransaction:开始事务。 setTransactionSuccessful:设置事务的成功标志。 endTransaction:结束事务。执行本方法时,系统会判断之前是否调用了 setTransactionSuccessful方法,如果之前已调用该方法就提交事务,如果没有调用该方法就回滚事务。 数据处理类,用于数据表层面的操作 execSQL:执行拼接好的SQL控制语句。一般用于建表、删表、变更表结构。 delete:删除符合条件的记录。 update:更新符合条件的记录信息。 insert:插入一条记录。 query:执行查询操作,并返回结果集的游标。 rawQuery:执行拼接好的SQL查询语句,并返回结果集的游标。 ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:2:2","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.2.3 数据库帮助器SQLiteOpenHelper 由于SQLiteDatabase存在局限性,一不小心就会重复打开数据库,处理数据库的升级也不方便;因此 Android提供了数据库帮助器SQLiteOpenHelper,帮助开发者合理使用SQLite。 SQLiteOpenHelper的具体使用步骤如下: 1.新建一个继承自SQLiteOpenHelper的数据库操作类,按提示重写onCreate和onUpgrade两个方法。其中,onCreate方法只在第一次打开数据库时执行,在此可以创建表结构;而onUpgrade方法在数据库版本升高时执行,在此可以根据新旧版本号变更表结构。 2.为保证数据库安全使用,需要封装几个必要方法,包括获取单例对象、打开数据库连接、关闭 数据库连接,说明如下: 2.1 获取单例对象:确保在App运行过程中数据库只会打开一次,避免重复打开引起错误。 2.2 打开数据库连接:SQLite有锁机制,即读锁和写锁的处理;故而数据库连接也分两种,读连接可调用getReadableDatabase方法获得,写连接可调用getWritableDatabase获得。 2.3 关闭数据库连接:数据库操作完毕,调用数据库实例的close方法关闭连接。 3.提供对表记录增加、删除、修改、查询的操作方法。 能被SQLite直接使用的数据结构是ContentValues类,它类似于映射Map,也提供了put和get方法存取 键值对。区别之处在于:ContentValues的键只能是字符串,不能是其他类型。ContentValues主要用于 增加记录和更新记录,对应数据库的insert和update方法。 记录的查询操作用到了游标类Cursor,调用query和rawQuery方法返回的都是Cursor对象,若要获取全 部的查询结果,则需根据游标的指示一条一条遍历结果集合。Cursor的常用方法可分为3类,说明如 下: 1.2.3.1 游标控制类方法,用于指定游标的状态 close:关闭游标。 isClosed:判断游标是否关闭。 isFirst:判断游标是否在开头。 isLast:判断游标是否在末尾。 1.2.3.2 游标移动类方法,把游标移动到指定位置 moveToFirst:移动游标到开头。 moveToLast:移动游标到末尾。 moveToNext:移动游标到下一条记录。 moveToPrevious:移动游标到上一条记录。 move:往后移动游标若干条记录。 moveToPosition:移动游标到指定位置的记录。 1.2.3.3 获取记录类方法,可获取记录的数量、类型以及取值 getCount:获取结果记录的数量。 getInt:获取指定字段的整型值。 getLong:获取指定字段的长整型值。 getFloat:获取指定字段的浮点数值。 getString:获取指定字段的字符串值。 getType:获取指定字段的字段类型。 整体创建流程见如下代码,效果图不具体展示: layout_file \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e \u003cLinearLayout 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\" tools:context=\".SharePrefrenceActivity\" android:orientation=\"vertical\"\u003e \u003cLinearLayout android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:orientation=\"horizontal\" \u003e \u003cTextView android:layout_width=\"120dp\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:text=\"姓名:\" android:textSize=\"17sp\" android:textColor=\"@color/black\" /\u003e \u003cEditText android:id=\"@+id/et_name\" android:layout_width=\"match_parent\" android:layout_height=\"40dp\" android:layout_weight=\"1\" android:hint=\"请输入您的名字\" android:textSize=\"17sp\" android:inputType=\"text\" android:background=\"@drawable/editext_selector\" /\u003e \u003c/LinearLayout\u003e \u003cLinearLayout android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:orientation=\"horizontal\" \u003e \u003cTextView android:layout_width=\"120dp\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:text=\"年龄:\" android:textSize=\"17sp\" android:textColor=\"@color/black\" /\u003e \u003cEditText android:id=\"@+id/et_age\" android:layout_width=\"match_parent\" android:layout_height=\"40dp\" android:layout_weight=\"1\" android:hint=\"请输入您的年龄\" android:textSize=\"17sp\" android:inputType=\"number\" android:background=\"@drawable/editext_selector\" /\u003e \u003c/LinearLayout\u003e \u003cLinearLayout android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:orientation=\"horizontal\" \u003e \u003cTextView android:layout_width=\"120dp\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:text=\"身高:\" android:textSize=\"17sp\" android:textColor=\"@color/black\" /\u003e \u003cEditText android:id=\"@+id/et_height\" android:layout_width=\"match_parent\" android:layout_height=\"40dp\" android:layout_weight=\"1\" android:hint=\"请输入您的身高\" android:textSize=\"17sp\" android:inputType=\"number\" android:background=\"@drawable/editext_selector\" /\u003e \u003c/LinearLayout\u003e \u003cLinearLayout android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:orientation=\"horizontal\" \u003e \u003cTextView android:layout_width=\"120dp\" android:layout_height=\"wrap_content\" android:layout_weight=\"1\" android:text=\"体重:\" android:textSize=\"17sp\" android:textColor=\"@color/black\" /\u003e \u003cEditText android:id=\"@+id/et_weight\" android:layout_width=\"match_parent\" android:layout_height=\"40dp\" android:layout_weight=\"1\" android:hint=\"请输入您的体重\" android:textSize=\"17sp\" android:inputType=\"number\" android:background=\"@drawable/editext_selector\" /\u003e \u003c/LinearLayout\u003e \u003cCheckBox android:id=\"@+id/ck_isMarried\" android:layout_width=\"match_parent\" an","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:2:3","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.3 存储卡的文件操作 本节介绍Android的文件存储方式—在存储卡上读写文件,包括:公有存储空间与私有存储空间有什么 区别、如何利用存储卡读写文本文件、如何利用存储卡读写图片文件等。 ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:3:0","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.3.1 私有存储空间与公共存储空间 为了更规范地管理手机存储空间,Android从7.0开始将存储卡划分为私有存储和公共存储两大部分,也就是分区存储方式,系统给每个App都分配了默认的私有存储空间。App在私有空间上读写文件无何授权,但是若想在公共空间读写文件,则要在AndroidManifest.xml里面添加下述的权限配置。 \u003c!-- 存储卡读写 --\u003e \u003cuses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/\u003e \u003cuses-permission android:name=\"android.permission.READ_EXTERNAL_STORAG\" /\u003e 但是即使App声明了完整的存储卡操作权限,系统仍然默认禁止该App访问公共空间。打开手机的系统设置界面,进入到具体应用的管理页面,会发现该应用的存储访问权限被禁止了,如图2所示。 当然图示的禁止访问只是不让访问存储卡的公共空间,App自身的私有空间依旧可以正常读写。这缘于 Android把存储卡分成了两块区域,一块是所有应用均可访问的公共空间,另一块是只有应用自己才可 访问的专享空间。虽然Android给每个应用都分配了单独的安装目录,但是安装目录的空间很紧张,所 以Android在存储卡的“Android/data”目录下给每个应用又单独建了一个文件目录,用来保存应用自己 需要处理的临时文件。这个目录只有当前应用才能够读写文件,其他应用是不允许读写的。由于私有空 间本身已经加了访问权限控制,因此它不受系统禁止访问的影响,应用操作自己的文件目录自然不成问 题。因为私有的文件目录只有属主应用才能访问,所以一旦属主应用被卸载,那么对应的目录也会被删 掉。 既然存储卡分为公共空间和私有空间两部分,它们的空间路径获取也就有所不同。若想获取公共空间的 存储路径,调用的是Environment.getExternalStoragePublicDirectory方法;若想获取应用私有空间的 存储路径,调用的是getExternalFilesDir方法。下面是分别获取两个空间路径的代码例子: //获取系统公共存储路径 String publicPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString(); //当前app的私有路径 String privatePath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(); boolean isLegacy = true; if (Build.VERSION.SDK_INT \u003e= Build.VERSION_CODES.Q) { // Android10的存储空间默认采取分区方式,此处判断是传统方式还是分区方式 isLegacy = Environment.isExternalStorageLegacy(); } String desc = \"系统的公共存储路径位于\" + publicPath + \"\\n\\n当前App的私有存储路径位于\" + privatePath + \"\\n\\nAndroid7.0之后默认禁止访问公共存储目录\" + \"\\n\\n当前App的存储空间采取\" + (isLegacy?\"传统方式\":\"分区方式\"); tv_show.setText(desc); ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:3:1","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.3.2 在存储卡上读写文本文件 公共区域和私有区域的读写,均依赖于IO流的读写。下面代码定义了读写的FILE工具类: public class FileUtil { //把字符串保存到指定路径的文本文件 public static void saveText(String path,String text){ BufferedWriter bw = null; try{ bw = new BufferedWriter(new FileWriter(path)); bw.write(text); }catch (Exception e){ e.printStackTrace(); }finally { if(bw!=null){ try { bw.close(); } catch (IOException e) { throw new RuntimeException(e); } } } } //从指定路径抽出文本 public static String readText(String path){ BufferedReader br = null; StringBuilder sb = new StringBuilder(); try{ br = new BufferedReader(new FileReader(path)); String str = null; while((str = br.readLine())!=null){ sb.append(str); } }catch (Exception e){ e.printStackTrace(); }finally { if(br!=null){ try { br.close(); } catch (IOException e) { throw new RuntimeException(e); } } } return sb.toString(); } } 需要在公共区域写入文件时,可以传入path String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString() + File.separatorChar + filename; 需要在私有区域写入文件时,可以传入path String path = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() File.separatorChar + filename; ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:3:2","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.3.3 在存储卡上读取图片 文本文件读写可以转换为对字符串的读写,而图片文件保存的是图像数据,需要专门的位图工具Bitmap 处理。位图对象依据来源不同又分成3种获取方式,分别对应位图工厂BitmapFactory的下列3种方法: decodeResource:从指定的资源文件中获取位图数据 decodeFile:从指定路径的文件中获取位图数据。注意从Android 10开始,该方法只适用于私有目 录下的图片,不适用公共空间下的图片。 decodeStream:从指定的输入流中获取位图数据。比如使用IO流打开图片文件,此时文件输入流 对象即可作为decodeStream方法的入参。 //从文件读取位图信息 public static Bitmap readImage(String path){ Bitmap b1 = null; BufferedInputStream bis = null; try{ bis = new BufferedInputStream(new FileInputStream(path)); b1 = BitmapFactory.decodeStream(bis); }catch (Exception e){ e.printStackTrace(); }finally { if(bis != null){ try { bis.close(); } catch (IOException e) { throw new RuntimeException(e); } } } return b1; } 得到位图对象之后,就能在图像视图上显示位图。图像视图ImageView提供了下列方法显示各种来源的 图片: setImageResource:设置图像视图的图片资源,该方法的入参为资源图片的编号,形如“R.drawable.去掉扩展名的图片名称”。 setImageBitmap:设置图像视图的位图对象,该方法的入参为Bitmap类型。 setImageURI:设置图像视图的路径对象,该方法的入参为Uri类型。字符串格式的文件路径可通过代码“Uri.parse(file_path)”转换成路径对象。 读取图片文件的花样倒是挺多,把位图数据写入图片文件却只有一种,即通过位图对象的compress方法 将位图数据压缩到文件输出流。具体的图片写入代码如下所示: //将图片写入文件 public static void saveImage(String path, Bitmap bitmap){ BufferedOutputStream bos = null; try { bos = new BufferedOutputStream(new FileOutputStream(path)); bitmap.compress(Bitmap.CompressFormat.JPEG,100,bos); }catch (Exception e){ e.printStackTrace(); }finally { if(bos!=null){ try { bos.close(); } catch (IOException e) { throw new RuntimeException(e); } } } } ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:3:3","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.4 应用组件Application 本节介绍Android重要组件Application的基本概念和常见用法。首先说明Application的生命周期贯穿了 App的整个运行过程,接着利用Application实现App全局变量的读写,然后阐述了如何借助App实例来 操作Room数据库框架。 ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:4:0","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.4.1 Application的生命周期 Application是Android的一大组件,在App运行过程中有且仅有一个Application对象贯穿应用的整个生 命周期。打开AndroidManifest.xml,发现activity节点的上级正是application节点,不过该节点并未指 定name属性,此时App采用默认的Application实例。 注意到每个activity节点都指定了name属性,譬如常见的name属性值为.MainActivity,让人知晓该 activity的入口代码是MainActivity.java。现在尝试给application节点加上name属性,看看其庐山真面 目,具体步骤说明如下: (1)打开AndroidManifest.xml,给application节点加上name属性,表示application的入口代码是 MainApplication.java。修改后的application节点示例如下: \u003capplication android:name=\".MainApplication\" android:icon=\"@mipmap/ic_launcher\" android:label=\"@string/app_name\" android:theme=\"@style/AppTheme\"\u003e (2)在Java代码的包名目录下创建MainApplication.java,要求该类继承Application,继承之后可供重 写的方法主要有以下3个。 onCreate:在App启动时调用。 onTerminate:在App终止时调用(按字面意思)。 onConfigurationChanged:在配置改变时调用,例如从竖屏变为横屏。 光看字面意思的话,与生命周期有关的方法是onCreate和onTerminate,那么重写这两个方法,并在重 写后的方法中打印日志,修改后的Java代码如下所示: @Override public void onCreate() { super.onCreate(); Log.d(\"zhao\",\"onCreate\"); } @Override public void onTerminate() { super.onTerminate(); Log.d(\"zhao\",\"onTerminate\"); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); Log.d(\"zhao\",\"onConfigurationChanged\"); } (3)运行测试App,在logcat窗口观察应用日志。但是只在启动一开始看到MainApplication的 onCreate日志(该日志先于MainActivity的onCreate日志),却始终无法看到它的onTerminate日志, 无论是自行退出App还是强行杀掉App,日志都不会打印onTerminate。 无论你怎么折腾,这个onTerminate日志都不会出来。Android明明提供了这个方法,同时提供了关于 该方法的解释,说明文字如下:This method is for use in emulated process environments.It will never be called on a production Android device, where processes are removed by simply killing them; no user code (including this callback) is executed when doing so。这段话的意思是:该方法 供模拟环境使用,它在真机上永远不会被调用,无论是直接杀进程还是代码退出;执行该操作时,不会 执行任何用户代码。 现在很明确了,onTerminate方法就是个摆设,中看不中用。如果读者想在App退出前回收系统资源, 就不能指望onTerminate方法的回调了。 ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:4:1","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.4.2 利用Application操作全局变量 C/C++有全局变量的概念,因为全局变量保存在内存中,所以操作全局变量就是操作内存,显然内存的 读写速度远比读写数据库或读写文件快得多。所谓全局,指的是其他代码都可以引用该变量,因此全局 变量是共享数据和消息传递的好帮手。不过Java没有全局变量的概念,与之比较接近的是类里面的静态 成员变量,该变量不但能被外部直接引用,而且它在不同地方引用的值是一样的(前提是在引用期间不 能改动变量值),所以借助静态成员变量也能实现类似全局变量的功能。 根据上一小节的介绍可知,Application的生命周期覆盖了App运行的全过程。不像短暂的Activity生命周 期,一旦退出该页面,Activity实例就被销毁。因此,利用Application的全生命特性,能够在 Application实例中保存全局变量。 适合在Application中保存的全局变量主要有下面3类数据: (1)会频繁读取的信息,例如用户名、手机号码等。 (2)不方便由意图传递的数据,例如位图对象、非字符串类型的集合对象等。 (3)容易因频繁分配内存而导致内存泄漏的对象,例如Handler处理器实例等。 要想通过Application实现全局内存的读写,得完成以下3项工作: (1)编写一个继承自Application的新类MainApplication。该类采用单例模式,内部先声明自身类的一 个静态成员对象,在创建App时把自身赋值给这个静态对象,然后提供该对象的获取方法getInstance。 具体实现代码示例如下: public class MyApplication extends Application { private static MainApplication mApp; // 声明一个当前应用的静态实例 // 声明一个公共的信息映射对象,可当作全局变量使用 public HashMap\u003cString, String\u003e infoMap = new HashMap\u003cString, String\u003e(); // 利用单例模式获取当前应用的唯一实例 public static MainApplication getInstance() { return mApp; } @Override public void onCreate() { super.onCreate(); Log.d(\"zhao\",\"onCreate\"); } @Override public void onTerminate() { super.onTerminate(); Log.d(\"zhao\",\"onTerminate\"); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); Log.d(\"zhao\",\"onConfigurationChanged\"); } } 在活动页面代码中调用MainApplication的getInstance方法,获得它的一个静态对象,再通过该对 象访问MainApplication的公共变量和公共方法。 不要忘了在AndroidManifest.xml中注册新定义的Application类名,也就是给application节点增 加android:name属性,其值为.MainApplication。 接下来演示如何读写内存中的全局变量,首先分别创建写内存页面和读内存页面,其中写内存页面把用 户的注册信息保存到全局变量infoMap ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:4:2","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.4.3 利用Room简化数据库操作 虽然Android提供了数据库帮助器,但是开发者在进行数据库编程时仍有诸多不便,比如每次增加一张 新表,开发者都得手工实现以下代码逻辑: (1)重写数据库帮助器的onCreate方法,添加该表的建表语句。 (2)在插入记录之时,必须将数据实例的属性值逐一赋给该表的各字段。 (3)在查询记录之时,必须遍历结果集游标,把各字段值逐一赋给数据实例。 (4)每次读写操作之前,都要先开启数据库连接;读写操作之后,又要关闭数据库连接。 上述的处理操作无疑存在不少重复劳动,数年来引得开发者叫苦连连。为此各类数据库处理框架纷纷涌 现,包括GreenDao、OrmLite、Realm等,可谓百花齐放。眼见SQLite渐渐乏人问津,谷歌公司干脆整 了个自己的数据库框架—Room,该框架同样基于SQLite,但它通过注解技术极大地简化了数据库操 作,减少了原来相当一部分编码工作量。 由于Room并未集成到SDK中,而是作为第三方框架提供,因此要修改模块的build.gradle文件,往 dependencies节点添加下面两行配置,表示导入指定版本的Room库: def room_version = \"2.5.1\"//目前最新的稳定版本 implementation \"androidx.room:room-runtime:$room_version\" annotationProcessor \"androidx.room:room-compiler:$room_version\" 添加依赖之后。我们需要进行如下五个步骤 1.编写信息表对应的实体类,该类添加\"@Entitiy\"注解 2.编写信息表对应的持久化类,该类添加\"@Dao\"注解 3.编写信息表对应的数据库类,该类继承RoomDatabase,并添加\"@Database\"注解 4.在自定义的application类中声明数据库的唯一实例 5.在操作信息表的地方获取数据表的持久化对象 1.编写信息表的Entity类 package com.example.chapter06review.Entity; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; @Entity public class BookInfo { @PrimaryKey(autoGenerate = true) private int id; @ColumnInfo(name = \"name\") private String name; @ColumnInfo(name = \"author\") private String author; @ColumnInfo(name = \"press\") private String press; @ColumnInfo(name = \"price\") private double price; @Override public String toString() { return \"BookInfo{\" + \"id=\" + id + \", name='\" + name + '\\'' + \", author='\" + author + '\\'' + \", press='\" + press + '\\'' + \", price=\" + price + '}'; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public String getPress() { return press; } public void setPress(String press) { this.press = press; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } } 2.编写信息表对应的持久化类 @Dao public interface BookInfoDao { @Insert void insert(BookInfo... bookInfo); @Delete void delete(BookInfo... bookInfo); @Update void update(BookInfo... bookInfo); //根据名字查询 @Query(\"SELECT * FROM BookInfo WHERE name = :name ORDER BY uid DESC limit 1\") BookInfo queryByName(String name ); //查询所有信息 @Query(\"SELECT * FROM BOOKINFO\") List\u003cBookInfo\u003e queryAll(); } ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:4:3","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"3.编写信息表对应的Database类 //entities表示该数据库有哪些表,version表示数据库的版本号 //exportSchema表示是否导出数据库信息的json串,建议设为false,若设为true还需指定json文件的保存路径 @Database(entities = {BookInfo.class},version = 1, exportSchema = false) public abstract class BookDatabase extends RoomDatabase { // 获取该数据库中某张表的持久化对象 public abstract BookDao bookDao(); } ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:4:4","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"4.在自定义的Application类中声明图书数据库的唯一实例 为了避免重复打开数据库造成的内存泄漏问题,每个数据库在App运行过程中理应只有一个实例,此时 要求开发者自定义新的Application类,在该类中声明并获取图书数据库的实例,并将自定义的 Application类设为单例模式,保证App运行之时有且仅有一个应用实例。下面是自定义Application类的 代码例子: public class MyApplication extends Application { private static MyApplication mApp; public static MyApplication getInstance(){ return mApp; } //声明一个书籍数据库对象 private BookInfoDatabase bookInfoDatabase; @Override public void onCreate() { super.onCreate(); mApp = this; Log.d(\"zhao\",\"onCreate\"); //构建数据库实例 bookInfoDatabase = Room.databaseBuilder(this, BookInfoDatabase.class,\"book\") //允许迁移数据库(数据库发生变更是,Room默认删除源数据库再创建新数据库,如此一来原来的记录就会丢失,故而要改用迁移方式以保存原有记录 .addMigrations() //允许在主线程中操作数据库 .allowMainThreadQueries() .build(); } //获取数据库实例 public BookInfoDatabase getBookInfoDatabase(){ return bookInfoDatabase; } } 5.在操作图书信息表的地方获取数据表的持久化对象 //构建 bookInfoDao = MyApplication.getInstance().getBookInfoDatabase().bookInfoDao(); 完成以上5个编码步骤之后,接着调用持久化对象的queryXXX、insertXXX、updateXXX、deleteXXX等 方法,就能实现图书信息的增删改查操作了。 ","date":"2023-05-05","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/:4:5","tags":["Android"],"title":"Android基础学习_数据存储","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"},{"categories":["Andorid基础"],"content":"1.Android开发 中级控件 ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:0:0","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.1 图形定制 ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:1:0","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.1.1 图形Drawable Android把所有能够显示的图形都抽象为Drawable类(可绘制的)。这里的图形不止是图片,还包括色块、画板、背景等。包含图片在内的图形文件放在res目录的各个drawable目录下,其中drawable目录一般保存描述性的 XML文件,而图片文件一般放在具体分辨率的drawable目录下。 例如: drawable-ldpi里面存放低分辨率的图片(如240×320),现在基本没有这样的智能手机了。 drawable-mdpi里面存放中等分辨率的图片(如320×480),这样的智能手机已经很少了。 drawable-hdpi里面存放高分辨率的图片(如480×800),一般对应4英寸~4.5英寸的手机(但不绝对,同尺寸的手机有可能分辨率不同,手机分辨率就高不就低,因为分辨率低了屏幕会有模糊的感觉)。 drawable-xhdpi里面存放加高分辨率的图片(如720×1280),一般对应5英寸~5.5英寸的手机。 drawable-xxhdpi里面存放超高分辨率的图片(如1080×1920),一般对应6英寸~6.5英寸的手机。 drawable-xxxhdpi里面存放超超高分辨率的图片(如1440×2560),一般对应7英寸以上的平板电脑。 基本上,分辨率每加大一级,宽度和高度就要增加二分之一或三分之一像素。如果各目录存在同名图 片,Android就会根据手机的分辨率分别适配对应文件夹里的图片。在开发App时,为了兼容不同的手机 屏幕,在各目录存放不同分辨率的图片,才能达到最合适的显示效果。例如,在drawable-hdpi放了一 张背景图片bg.png(分辨率为480×800),其他目录没放,使用分辨率为480×800的手机查看该App界 面没有问题,但是使用分辨率为720×1280的手机查看该App会发现背景图片有点模糊,原因是Android 为了让bg.png适配高分辨率的屏幕,强行把bg.png拉伸到了720×1280,拉伸的后果是图片变模糊了。 在XML布局文件中引用图形文件可使用“@drawable/不含扩展名的文件名称”这种形式,如各视图的 background属性、ImageView和ImageButton的src属性、TextView和Button四个方向的drawable*** 系列属性都可以引用图形文件。 ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:1:1","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.1.2 shape视图 Shape图形又称形状图形,它用来描述常见的几何形状,包括矩形、圆角矩形、圆形、椭圆等。用好形状图形可以让App页面不再呆板,还可以节省美工不少工作量。 形状图形的定义文件放在drawable目录下,它是以shape标签为根节点的XML描述文件。根节点下定义 了6个节点,分别是:size(尺寸)、stroke(描边)、corners(圆角)、solid(填充)、padding (间隔)、gradient(渐变),各节点的属性值主要是长宽、半径、角度以及颜色等。下面是形状图形 各个节点及其属性的简要说明。 1.1.2.1 shape(形状) shape是形状图形文件的根节点,它描述了当前是哪种几何图形。下面是shape节点的常用属性说明。 shape:字符串类型,表示图形的形状。形状类型的取值说明见下表。 形状类型 说明 rectangle 矩形,默认值 ring 圆形 oval 椭圆,此时corner节点会失效 line 直线,必须设置stroke,否则会报错 1.1.2.2 size size是shape的下级节点,它描述了形状图形的宽高尺寸。若无size节点,则表示宽高与宿主视图一样大小。下面是size节点的常用属性说明。 height:像素类型,图形高度。 width:像素类型,图形宽度。 1.1.2.3 stroke(描边) stroke是shape的下级节点,它描述了形状图形的描边规格。若无stroke节点,则表示不存在描边。下面是stroke节点的常用属性说明。 color:颜色类型,描边的颜色。 dashGap:像素类型,每段虚线之间的间隔。 dashWidth:像素类型,每段虚线的宽度。若dashGap和dashWidth有一个值为0,则描边为实 线。 width:像素类型,描边的厚度。 1.1.2.4 corners(圆角) corners是shape的下级节点,它描述了形状图形的圆角大小。若无corners节点,则表示没有圆角。下 面是corners节点的常用属性说明。 bottomLeftRadius:像素类型,左下圆角的半径。 bottomRightRadius:像素类型,右下圆角的半径。 topLeftRadius:像素类型,左上圆角的半径。 topRightRadius:像素类型,右上圆角的半径。 radius:像素类型,4个圆角的半径(若有上面4个圆角半径的定义,则不需要radius定义)。 1.1.2.5 solid(填充) solid是shape的下级节点,它描述了形状图形的填充色彩。若无solid节点,则表示无填充颜色。下面是 solid节点的常用属性说明。 color:颜色类型,内部填充的颜色。 1.1.2.6 padding(间隔) padding是shape的下级节点,它描述了形状图形与周围边界的间隔。若无padding节点,则表示四周不 设间隔。下面是padding节点的常用属性说明。 top:像素类型,与上方的间隔。 bottom:像素类型,与下方的间隔。 left:像素类型,与左边的间隔。 right:像素类型,与右边的间隔。 1.1.2.7 gradient(渐变) gradient是shape的下级节点,它描述了形状图形的颜色渐变。若无gradient节点,则表示没有渐变效 果。下面是gradient节点的常用属性说明。 angle:整型,渐变的起始角度。为0时表示时钟的9点位置,值增大表示往逆时针方向旋转。例 如,值为90表示6点位置,值为180表示3点位置,值为270表示0点/12点位置 type:字符串类型,渐变类型。渐变类型的取值说明见下表 |渐变类型|说明| |linear|线性渐变,默认值| |radial|放射渐变,起始颜色就是圆心颜色| |sweep|滚动渐变,即一个线段以某个端点为圆心做360°旋转| centerX:浮点型,圆心的X坐标。当android:type=“linear\"时不可用。 centerY:浮点型,圆心的Y坐标。当android:type=“linear\"时不可用。 gradientRadius:整型,渐变的半径。当android:type=“radial\"时需要设置该属性。 centerColor:颜色类型,渐变的中间颜色。 startColor:颜色类型,渐变的起始颜色。 endColor:颜色类型,渐变的终止颜色。 useLevel:布尔类型,设置为true为无渐变色、false为有渐变色。 ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:1:2","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.1.3 九宫格图片 将某张图片设置成视图背景时,如果图片尺寸太小,则系统会自动拉伸图片使之填满背景。可是一旦图 片拉得过大,其画面容易变得模糊,如图5-3所示,上面按钮的背景图片被拉得很宽,此时左右两边的边 缘线既变宽又变模糊了。 为了解决这个问题,Android专门设计了点九图片。点九图片的扩展名是png,文件名后面常带有“.9”字 样。因为该图片划分了3×3的九宫格区域,所以得名点九图片,也叫九宫格图片。如果背景是一个形状 图形,其stroke节点的width属性已经设置了固定数值(如1dp),那么无论该图形被拉到多大,描边宽 度始终是1dp。点九图片的实现原理与之类似,即拉伸图形时,只拉伸内部区域,不拉伸边缘线条。 NinePhoto 图1的左侧窗口是图片加工区域,右侧窗口是图片预览区域,从上到下依次是纵向拉伸预览、横向 拉伸预览、两方向同时拉伸预览。在左侧窗口图片四周的马赛克处单击会出现一个黑点,把黑点左右或 上下拖动会拖出一段黑线,不同方向上的黑线表示不同的效果。 界面上边的黑线指的是水平方向的拉伸区域。水平方向拉伸图片时,只有黑线区域内的图像会拉伸,黑线以外的图像保持原状,从而保证左右两侧的边框厚度不变。 界面左边的黑线指的是垂直方向的拉伸区域。垂直方向拉伸图片时,只有黑线区域内的图像会拉伸,黑线以外的图像保持原状,从而保证上下两侧的边框厚度不变。 界面下边的黑线指的是该图片作为控件背景时,控件内部的文字左右边界只能放在黑线区域内。这里Horizontal Padding的效果就相当于android:paddingLeft与android:paddingRight 界面右边的黑线指的是该图片作为控件背景时,控件内部的文字上下边界只能放在黑线区域内。这里Vertical Padding的效果就相当于android:paddingTop与android:paddingBottom 尤其注意,如果点九图片被设置为视图背景,且该图片指定了Horizontal Padding和Vertical Padding, 那么视图内部将一直与视图边缘保持固定间距,无论怎么调整XML文件和Java代码都无法缩小间隔,缘由是点九图片早已在水平和垂直方向都设置了padding ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:1:3","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.1.4 状态列表图形 常见的图形文件一般为静态图形,但有时会用到动态图形,比如按钮控件的背景在正常情况下是凸起 的,在按下时是凹陷的,从按下到弹起的过程,用户便晓得点击了该按钮。根据不同的触摸情况变更图 形状态,这种情况用到了Drawable的一个子类StateListDrawable(状态列表图形),它在XML文件中 规定了不同状态时候所呈现的图形列表。 接下来演示一下状态列表图形的界面效果,右击drawable目录,并依次选择右键菜单的 New→Drawable resource file,在弹窗中输入文件名称再单击OK按钮,即可自动生成一个XML描述文 件。往该文件填入下面的状态列表图形定义: 接下来演示一下状态列表图形的界面效果,右击drawable目录,并依次选择右键菜单的 New→Drawable resource file,在弹窗中输入文件名称再单击OK按钮,即可自动生成一个XML描述文 件。往该文件填入下面的状态列表图形定义: \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e \u003cselector xmlns:android=\"http://schemas.android.com/apk/res/android\"\u003e \u003citem android:state_pressed=\"true\" android:drawable=\"@drawable/button_pressed\" /\u003e \u003citem android:drawable=\"@drawable/button_normal\" /\u003e \u003c/selector\u003e 上述XML文件的关键点是state_pressed属性,该属性表示按下状态,值为true表示按下时显示 button_pressed图像,其余情况显示button_normal图像。 效果图如下: 状态列表图形不仅用于按钮控件,还可用于其他拥有多种状态的控件,这取决于开发者在XML文件中指 定了哪种状态类型,各种状态类型的取值说明详见下表. 状态类型的属性名称 说明 使用的控件 state_pressed 是否按下 按钮 state_checked 是否勾选 复选框CheckBox、单选按钮RadioButton state_focused 是否获取焦点 文本编辑框EditText state_selected 是否选中 各控件均适用 ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:1:4","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.2 选择按钮 几个常用的特殊控制按钮 ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:2:0","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.2.1 复选框CheckBox 在学习复选框之前,先了解一下CompoundButton。在Android体系中,CompoundButton类是抽象的 复合按钮,因为是抽象类,所以它不能直接使用。实际开发中用的是CompoundButton的几个派生类, 主要有复选框CheckBox、单选按钮RadioButton以及开关按钮Switch,这些派生类均可使用 CompoundButton的属性和方法。加之CompoundButton本身继承了Button类,故以上几种按钮同时 具备Button的属性和方法,它们之间的继承关系如图4所示。 CompoundButton在XML文件中主要使用下面两个属性。 checked:指定按钮的勾选状态,true表示勾选,false则表示未勾选。默认为未勾选。 button:指定左侧勾选图标的图形资源,如果不指定就使用系统的默认图标。 CompoundButton在Java代码中主要使用下列4种方法。 setChecked:设置按钮的勾选状态。 setButtonDrawable:设置左侧勾选图标的图形资源。 setOnCheckedChangeListener:设置勾选状态变化的监听器。 isChecked:判断按钮是否勾选。 复选框CheckBox是CompoundButton一个最简单的实现控件,点击复选框将它勾选,再次点击取消勾 选。复选框对象调用setOnCheckedChangeListener方法设置勾选监听器,这样在勾选和取消勾选时就 会触发监听器的勾选事件。 接下来演示复选框的操作过程,首先编写活动页面的XML文件如下所示: \u003cCheckBox android:id=\"@+id/ck_system\" android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:text=\"这是systemStyle CheckBox\" android:textSize=\"20sp\" /\u003e 接着编写对应的Java代码,主要是如何处理勾选监听器,具体代码如下所示: public class CheckBoxReviewActivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_check_box_review); CheckBox ck_system = findViewById(R.id.ck_system); CheckBox ck_custom = findViewById(R.id.ck_custom); ck_system.setOnCheckedChangeListener(this); ck_custom.setOnCheckedChangeListener(this); } @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { String desc = String.format(\"你%s了这个CheckBox\",(isChecked ? \"勾选\" : \"取消勾选\")); buttonView.setText(desc); } } ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:2:1","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.2.2 开关按钮switch Switch是开关按钮,它像一个高级版本的CheckBox,在选中与取消选中时可展现的界面元素比复选框丰 富。Switch控件新添加的XML属性说明如下: textOn:设置右侧开启时的文本。 textOff:设置左侧关闭时的文本。 track:设置开关轨道的背景。 thumb:设置开关标识的图标。 下面介绍类IOSUI的Switch控件的设计。 主要思路是借助状态列表图形,首先创建一个图形专用的XML文件,给状态列表指定选中与未选中时候的开关图标,如下所示: \u003cselector xmlns:android=\"http://schemas.android.com/apk/res/android\"\u003e \u003citem android:state_checked=\"true\" android:drawable=\"@drawable/switch_on\"/\u003e \u003citem android:drawable=\"@drawable/switch_off\"/\u003e \u003c/selector\u003e 然后把CheckBox标签的background属性设置为@drawable/switch_selector,同时将button属性设置 为@null。完整的CheckBox标签内容示例如下: \u003cCheckBox android:id=\"@+id/ck_status\" android:layout_width=\"60dp\" android:layout_height=\"30dp\" android:background=\"@drawable/switch_selector\" android:button=\"@null\" /\u003e ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:2:2","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.2.3 单选按钮RadioButton 所谓单选按钮,指的是在一组按钮中选择其中一项,并且不能多选,这要求有个容器确定这组按钮的范 围,这个容器便是单选组RadioGroup。单选组实质上是个布局,同一组RadioButton都要放在同一个 RadioGroup节点下。RadioGroup提供了orientation属性指定下级控件的排列方向,该属性为horizontal时,单选按钮在水平方向排列;该属性为vertical时,单选按钮在垂直方向排列。 下面是RadioGroup在Java代码中的3个常用方法。 check:选中指定资源编号的单选按钮。 getCheckedRadioButtonId:获取已选中单选按钮的资源编号。 setOnCheckedChangeListener:设置单选按钮勾选变化的监听器。 与CheckBox不同的是,RadioButton默认未选中,点击后显示选中,但是再次点击不会取消选中。只有 点击同组的其他单选按钮时,原来选中的单选按钮才会取消选中。另需注意,单选按钮的选中事件不是 由RadioButton处理,而是由RadioGroup处理。 ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:2:3","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.3 文本输入 ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:3:0","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.3.1 编辑框EditText 编辑框EditText用于接收软键盘输入的文字,例如用户名、密码、评价内容等,它由文本视图派生而 来,除了TextView已有的各种属性和方法,EditText还支持下列XML属性。 inputType:指定输入的文本类型。输入类型的取值说明见下表,若同时使用多种文本类型,则可使用竖线“|”把多种文本类型拼接起来。 maxLength:指定文本允许输入的最大长度。 hint:指定提示文本的内容。 textColorHint:指定提示文本的颜色。 输入类型 说明 text 文本 textPassword 文本密码。显示时用圆点”.“代替显示 number 整型数 numberSigned 带符号的数字。允许在开头带负号 numberDecimal 带小数点的数字 numberPassword 数字密码。显示时用圆点”.“代替显示 datetime 时间日期格式。除了数字外,还允许输入横线、斜杆、空格、冒号 date 日期格式。除了数字外,还允许输入横线“-”和斜杆“/” time 时间格式。除了数字外,还允许输入冒号“:” ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:3:1","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.3.2 焦点变更监听器 虽然编辑框EditText提供了maxLength属性,用来设置可输入文本的最大长度,但是它没提供对应的 minLength属性,也就无法设置可输入文本的最小长度。譬如手机号码为固定的11位数字,用户必须输 满11位才是合法的,然而编辑框不会自动检查手机号码是否达到11位,即使用户少输一位只输入十位数 字,编辑框依然认为这是合法的手机号。 焦点变更监听器来自于接口View.OnFocusChangeListener,若想注册该监听器,就要调用编辑框对象 的setOnFocusChangeListener方法,即可在光标切换之时(获得光标和失去光标)触发焦点变更事 件。下面是给密码框注册焦点变更监听器的代码例子 // 从布局文件中获取名为et_password的编辑框 EditText et_password = findViewById(R.id.et_password); // 给编辑框注册一个焦点变化监听器,一旦焦点发生变化,就触发监听器的onFocusChange方法 et_password.setOnFocusChangeListener(this); 以上代码把焦点变更监听器设置到当前页面,则需让活动页面实现接口 View.OnFocusChangeListener,并重写该接口定义的onFocusChange方法,判断如果是密码框获得焦 点,就检查输入的手机号码是否达到11位。具体的焦点变更处理方法如下所示: // 焦点变更事件的处理方法,hasFocus表示当前控件是否获得焦点。 // 为什么光标进入事件不选onClick?因为要点两下才会触发onClick动作(第一下是切换焦点动作) @Override public void onFocusChange(View v, boolean hasFocus) { // 判断密码编辑框是否获得焦点。hasFocus为true表示获得焦点,为false表示失去焦点 if (v.getId()==R.id.et_password \u0026\u0026 hasFocus) { String phone = et_phone.getText().toString(); if (TextUtils.isEmpty(phone) || phone.length()\u003c11) { // 手机号码不足11位 // 手机号码编辑框请求焦点,也就是把光标移回手机号码编辑框 et_phone.requestFocus(); Toast.makeText(this, \"请输入11位手机号码\", Toast.LENGTH_SHORT).show(); } } } ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:3:2","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.3.3 文本监视器 监控文本的监控操作用到文本监听器接口TextWatcher,该接口提供了3个监控方法,具体说明如下: beforeTextChanged:在文本改变之前触发。 onTextChanged:在文本改变过程中触发。 afterTextChanged:在文本改变之后触发。 具体到编码实现,需要自己写个监听器实现TextWatcher接口,再调用编辑框对象的 addTextChangedListener方法注册文本监听器。监听操作建议在afterTextChanged方法中完成、 ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:3:3","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.4 对话框 本节介绍几种常用的对话框控件,包括:如何使用提醒对话框处理不同的选项,如何使用日期对话框获 取用户选择的日期,如何使用时间对话框获取用户选择的时间。 ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:4:0","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.4.1 提醒对话框AlertDialog AlertDialog名为提醒对话框,它是Android中最常用的对话框,可以完成常见的交互操作,例如提示、 确认、选择等功能。由于AlertDialog没有公开的构造方法,因此必须借助建造器AlertDialog.Builder才 能完成参数设置,AlertDialog.Builder的常用方法说明如下。 setIcon:设置对话框的标题图标。 setTitle:设置对话框的标题文本。 setMessage:设置对话框的内容文本。 setPositiveButton:设置肯定按钮的信息,包括按钮文本和点击监听器。 setNegativeButton:设置否定按钮的信息,包括按钮文本和点击监听器。 setNeutralButton:设置中性按钮的信息,包括按钮文本和点击监听器,该方法比较少用。 首先,我们来了解一下AlertDialog的大体创建顺序。与TextView、Button这些控件稍有不同, AlertDialog并不是初始化(findViewById)之后就直接调用各种方法了。仔细想想AlertDialog的使用场景,它并不像TextView和Button那些控件似的一般都是固定在界面上,而是在某个时机才会触发出来(比如用户点击了某个按钮或者断网了)。所以AlertDialog并不需要到布局文件中创建,而是在代码中通过构造器(AlertDialog.Builder)来构造标题、图标和按钮等内容的。 1.创建构造器AlertDialog.Builder的对象; 2.通过构造器对象调用setTitle、setMessage、setIcon等方法构造对话框的标题、信息和图标等内容; 3.根据需要调用setPositive/Negative/NeutralButton()方法设置正面按钮、负面按钮和中立按钮; 4.调用构造器对象的create方法创建AlertDialog对象; 5.AlertDialog对象调用show方法,让对话框在界面上显示。 注:AlertDialog.Builder自己也有一个show方法,可以显示对话框,所以上面的第4、第5步可以简化为一步。 下面是构建并显示提醒对话框的Java代码例子: public class AlertDialogReviewActivity extends AppCompatActivity implements View.OnClickListener { private TextView tv_show; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_alert_dialog_review); Button delete_soft = findViewById(R.id.delete_soft); delete_soft.setOnClickListener(this); } @Override public void onClick(View v) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(\"尊敬的用户:\"); builder.setMessage(\"您真的要卸载我么\"); builder.setPositiveButton(\"残忍卸载\", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Toast.makeText(AlertDialogReviewActivity.this,\"虽然依依不舍,但是只能离开了\",Toast.LENGTH_SHORT).show(); } }); builder.setNegativeButton(\"我再想想\", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Toast.makeText(AlertDialogReviewActivity.this,\"陪你三百六十五个日夜\",Toast.LENGTH_SHORT).show(); } }); AlertDialog alertDialog = builder.create(); alertDialog.show(); } } ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:4:1","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.4.2 日期对话框DatePickerDialog 虽然EditText提供了inputType=“date\"的日期输入,但是很少有人会手工输入完整日期,况且EditText 还不支持“ 年 ** 月 **日”这样的中文日期,所以系统提供了专门的日期选择器DatePicker,供用户选择 具体的年月日。不过,DatePicker并非弹窗模式,而是在当前页面占据一块区域,并且不会自动关闭。 按习惯来说,日期控件应该弹出对话框,选择完日期就要自动关闭对话框。因此,很少直接在界面上显 示DatePicker,而是利用已经封装好的日期选择对话框DatePickerDialog。 DatePickerDialog相当于在AlertDialog上装载了DatePicker,编码时只需调用构造方法设置当前的年、 月、日,然后调用show方法即可弹出日期对话框。日期选择事件则由监听器OnDateSetListener负责响 应,在该监听器的onDateSet方法中,开发者获取用户选择的具体日期,再做后续处理。特别注意 onDateSet的月份参数,它的起始值不是1而是0。也就是说,一月份对应的参数值为0,十二月份对应的 参数值为11,中间月份的数值以此类推。 在界面上内嵌显示DatePicker的效果如图5所示,其中,年、月、日通过上下滑动选择。单独弹出日 期对话框的效果如图5-34所示,其中年、月、日按照日历风格展示 下面是使用日期对话框的Java代码例子,包括弹出日期对话框和处理日期监听事件: // 该页面类实现了接口OnDateSetListener,意味着要重写日期监听器的onDateSet方法 public class DatePickerActivity extends AppCompatActivity implements View.OnClickListener, DatePickerDialog.OnDateSetListener { private TextView tv_date; // 声明一个文本视图对象 private DatePicker dp_date; // 声明一个日期选择器对象 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_date_picker); tv_date = findViewById(R.id.tv_date); // 从布局文件中获取名叫dp_date的日期选择器 dp_date = findViewById(R.id.dp_date); findViewById(R.id.btn_date).setOnClickListener(this); } @Override public void onClick(View v) { if (v.getId() == R.id.btn_date) { // 获取日历的一个实例,里面包含了当前的年月日 Calendar calendar = Calendar.getInstance(); // 构建一个日期对话框,该对话框已经集成了日期选择器。 // DatePickerDialog的第二个构造参数指定了日期监听器 DatePickerDialog dialog = new DatePickerDialog(this, this, calendar.get(Calendar.YEAR), // 年份 calendar.get(Calendar.MONTH), // 月份 calendar.get(Calendar.DAY_OF_MONTH)); // 日子 dialog.show(); // 显示日期对话框 } } // 一旦点击日期对话框上的确定按钮,就会触发监听器的onDateSet方法 @Override public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { // 获取日期对话框设定的年月份 String desc = String.format(\"您选择的日期是%d年%d月%d日\", year, monthOfYear + 1, dayOfMonth); tv_date.setText(desc); } } ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:4:2","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.4.3 时间对话框TimePickerDialog 既然有了日期选择器,还得有对应的时间选择器。同样,实际开发中也很少直接用TimePicker,而是用 封装好的时间选择对话框TimePickerDialog。该对话框的用法类似DatePickerDialog,不同之处主要有 两个: (1)构造方法传的是当前的小时与分钟,最后一个参数表示是否采取24小时制,一般为true表示小时 的数值范围为0~23;若为false则表示采取12小时制。 (2)时间选择监听器为OnTimeSetListener,对应需要实现onTimeSet方法,在该方法中可获得用户选 择的小时和分钟。 在界面上内嵌显示TimePicker的效果如图7所示,其中,小时与分钟可通过上下滑动选择。单独弹出 时间对话框的效果如图8所示,其中小时与分钟按照钟表风格展示。 下面是使用时间对话框的Java代码例子,包括弹出时间对话框和处理时间监听事件: // 该页面类实现了接口OnTimeSetListener,意味着要重写时间监听器的onTimeSet方法 public class TimePickerActivity extends AppCompatActivity implements View.OnClickListener, TimePickerDialog.OnTimeSetListener { private TextView tv_time; // 声明一个文本视图对象 private TimePicker tp_time; // 声明一个时间选择器对象 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_time_picker); tv_time = findViewById(R.id.tv_time); // 从布局文件中获取名叫tp_time的时间选择器 tp_time = findViewById(R.id.tp_time); findViewById(R.id.btn_time).setOnClickListener(this); } @Override public void onClick(View v) { if (v.getId() == R.id.btn_time) { // 获取日历的一个实例,里面包含了当前的时分秒 Calendar calendar = Calendar.getInstance(); // 构建一个时间对话框,该对话框已经集成了时间选择器。 // TimePickerDialog的第二个构造参数指定了时间监听器 TimePickerDialog dialog = new TimePickerDialog(this, this, calendar.get(Calendar.HOUR_OF_DAY), // 小时 calendar.get(Calendar.MINUTE), // 分钟 true); // true表示24小时制,false表示12小时制 dialog.show(); // 显示时间对话框 } } // 一旦点击时间对话框上的确定按钮,就会触发监听器的onTimeSet方法 @Override public void onTimeSet(TimePicker view, int hourOfDay, int minute) { // 获取时间对话框设定的小时和分钟 String desc = String.format(\"您选择的时间是%d时%d分\", hourOfDay, minute); tv_time.setText(desc); } } ","date":"2023-04-29","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/:4:3","tags":["Android"],"title":"Android基础学习_中级控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E4%B8%AD%E7%BA%A7%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.Android开发 活动Activity ","date":"2023-04-25","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/:0:0","tags":["Android"],"title":"Android基础学习_活动Activity","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/"},{"categories":["Andorid基础"],"content":"1.1 启停活动页面 本节介绍如何正确地启动和停止活动页面,首先描述活动页面的启动方法与结束方法,用户看到的页面就是开发者塑造的活动;接着详细分析活动的完整生命周期,以及每个周期方法的发生场景和流转过程;然后描述活动的几种启动模式,以及如何在代码中通过启动标志控制活动的跳转行为。 ","date":"2023-04-25","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/:1:0","tags":["Android"],"title":"Android基础学习_活动Activity","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/"},{"categories":["Andorid基础"],"content":"1.1.1 Activity的启动和结束 在之前的基础控件中,我们使用startActivity(源页面.this,目标页面.class);跳转想要跳转的界面。而当我们需要关闭某个也页面也就是一个Activity的时候,我们可以通过一个按钮,去设置这个按钮的setOnClickListener,并在onClick()方法中调用finish方法,即可关闭当前页面,并返回上一级页面。 ","date":"2023-04-25","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/:1:1","tags":["Android"],"title":"Android基础学习_活动Activity","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/"},{"categories":["Andorid基础"],"content":"1.1.2 Activity的生命周期 每次创建新的活动页面,自动生成的Java代码都给出了onCreate方法,该方法用于执行活动创建的相关操作,包括加载XML布局、设置文本视图的初始文字、注册按钮控件的点击监听,等等。onCreate方法所代表的创建动作,正是一个活动最开始的行为,除了onCreate,活动还有其他几种生命周期行为,它们对应的方法说明如下: 1.onCreate:创建活动。此时会把页面布局加载进内存,进入了初始状态。 2.onStart:开启活动。此时会把活动页面显示在屏幕上,进入了就绪状态。 3.onResume:恢复活动。此时活动页面进入活跃状态,能够与用户正常交互,例如允许响应用户的点击动作、允许用户输入文字等。 4.onPause:暂停活动。此时活动页面进入暂停状态(也就是退回就绪状态),无法与用户正常交互。 5.onStop:停止活动。此时活动页面将不在屏幕上显示。 6.onDestroy:销毁活动。此时回收活动占用的系统资源,把页面从内存中清除掉。 7.onRestart:重启活动。处于停止状态的活动,若想重新开启的话,无须经历onCreate的重复创建过程,而是走onRestart的重启过程。 8.onNewIntent:重用已有的活动实例。 上述的生命周期方法,涉及复杂的App运行状态,更直观的活动状态切换过程如下图2所示。 ","date":"2023-04-25","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/:1:2","tags":["Android"],"title":"Android基础学习_活动Activity","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/"},{"categories":["Andorid基础"],"content":"1.1.3 Activity的启动模式 系统给每个正在运行的App都分配了活动栈,栈里面容纳着已经创建且尚未销毁的活动信息。鉴于栈是一种先进后出、后进先出的数据结构,故而后面入栈的活动总是先出栈,假设3个活动的入栈顺序为:活动A→活动B→活动C,则它们的出栈顺序将变为:活动C→活动B→活动A,可见活动C结束之后会返回活动B,而不是返回活动A或者别的地方。假定某个App分配到的活动栈大小为3,该App先后打开两个活动,此时活动栈的变动情况如图3所示。 然后按下返回键,依次结束已打开的两个活动,此时活动栈的变动情况如图4所示。 结合图3与图4的入栈与出栈流程,即可验证结束活动之时的返回逻辑了。不过前述的出入栈情况仅是默认的标准模式,实际上Android允许在创建活动时指定该活动的启动模式,通过启动模式控制活动的出入栈行为。App提供了两种办法用于设置活动页面的启动模式,其一是修改AndroidManifest.xml,在指定的activity节点添加属性android:launchMode,表示本活动以哪个启动模式运行。其二是在代码中调用Intent对象的setFlags方法,表明后续打开的活动页面采用该启动标志。下面分别予以详细说明。 1.1.3.1 在配置文件中指定启动模式 打开AndroidManifest.xml,给activity节点添加属性android:launchMode,属性值填入standard表示 采取标准模式,当然不添加属性的话默认就是标准模式。具体的activity节点配置内容示例如下: \u003cactivity android:name=\".JumpFirstActivity\" android:launchMode=\"standard\" /\u003e 其中launchMode属性的几种取值说明见下表。 Intent类的启动标志 说明 Intene.FLAGS_ACTIVITY_NEW_TASK 开辟一个新的任务栈,该值类似于launchMode=“standard”,不同之处在于,如果原本不存在活动栈,则FLAGS_ACTIVITY_NEW_TASK会创建一个新栈 Intene.FLAGS_ACTIVITY_SINGLE_TOP 当栈顶为待跳转的活动实例时,则重复使用栈顶的实例.该值等于launchMode=“singleTop” Intene.FLAGS_ACTIVITY_CLEAR_TOP 当栈中存在待跳转的实例时,则重新创建一个新实例,并清除原实例上方的所有实例。该值与launchMode=“singleTask\"类似,但singleTask采用onNewIntent方法启用原任务,而FLAGS_ACTIVITY_CLEAR_TOP采取先调用onDestroy再调用onCreate来创建新任务 Intene.FLAGS_ACTIVITY_NO_HISTORY 该标志与launchMode=“standard\"情况类似,但栈中不保存新启动的活动实例。这样无论下次以何种方式再启动该实例,也要走standard模式的完整流程 Intene.FLAGS_ACTIVITY_CLEAR_TASK 该标志非常暴力,栈中的原有实例都被清空。注意该标志需要结合FLAGS_ACTIVITY_NEW_TASK使用,即调用Intent实例调用setFlags()时需要设置为FLAGS_ACTIVITY_CLEAR_TASK|FLAGS_ACTIVITY_NEW_TASK 1.两个活动之间相互交替 假设活动A有个按钮,点击该按钮会跳到活动B;同时活动B也有个按钮,点击按钮会跳到活动A;从首页打开活动A之后,就点击按钮在活动A与活动B之间轮流跳转。此时活动页面的跳转流程为:首页→活动A→活动B→活动A→活动B→活动A→活动B→……多次跳转之后想回到首页,正常的话返回流程是这样 的:……→活动B→活动A→活动B→活动A→活动B→活动A→首页,注意每个箭头都代表按一次返回键, 可见要按下许多次返回键才能返回首页。其实在活动A和活动B之间本不应该重复返回,因为回来回去总是这两个页面有什么意义呢?照理说每个活动返回一次足矣,同一个地方返回两次已经是多余的了,再返回应当回到首页才是。也就是说,不管过去的时候怎么跳转,回来的时候应该按照这个流程:……→活动B→活动A→首页,或者按照这个流程:……→活动A→活动B→首页,总之已经返回了的页面,决不再 返回第二次。 对于不允许重复返回的情况,可以设置启动标志FLAG_ACTIVITY_CLEAR_TOP,即使活动栈里面存在待 跳转的活动实例,也会重新创建该活动的实例,并清除原实例上方的所有实例,保留该实例和实例下方的实例,保证栈中最多只有该活动的唯一实例,从而避免了无谓的重复返回。于是活动A内部的跳转代码就改成了下面这般: // 创建一个意图对象,准备跳到指定的活动页面 Intent intent = new Intent(this, JumpSecondActivity.class); // 当栈中存在待跳转的活动实例时,则重新创建该活动的实例,并清除原实例上方的所有实例 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // 设置启动标志 startActivity(intent); // 跳转到意图对象指定的活动页面 当然活动B内部的跳转代码也要设置同样的启动标志: // 创建一个意图对象,准备跳到指定的活动页面 Intent intent = new Intent(this, JumpFirstActivity.class); // 当栈中存在待跳转的活动实例时,则重新创建该活动的实例,并清除原实例上方的所有实例 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // 设置启动标志 startActivity(intent); // 跳转到意图指定的活动页面 这下两个活动的跳转代码都设置了FLAG_ACTIVITY_CLEAR_TOP,运行测试App发现多次跳转之后,每个活动仅会返回一次而已。 2.登陆成功后不再返回登陆页面 很多App第一次打开都要求用户登录,登录成功再进入App首页,如果这时按下返回键,发现并没有回 到上一个登录页面,而是直接退出App了,这又是什么缘故呢?原来用户登录成功后,App便记下用户 的登录信息,接下来默认该用户是登录状态,自然不必重新输入用户名和密码了。既然默认用户已经登 录,哪里还需要回到登录页面?不光登录页面,登录之前的其他页面包括获取验证码、找回密码等页面 都不应回去,每次登录成功之后,整个App就焕然一新仿佛忘记了有登录页面这回事。 对于回不去的登录页面情况,可以设置启动标志FLAG_ACTIVITY_CLEAR_TASK,该标志会清空当前活动栈里的所有实例。不过全部清空之后,意味着当前栈没法用了,必须另外找个活动栈才行,也就是同时设置启动标志FLAG_ACTIVITY_NEW_TASK,该标志用于开辟新任务的活动栈。于是离开登录页面的跳转代码变成下面这样: // 创建一个意图对象,准备跳到指定的活动页面 Intent intent = new Intent(this, LoginSuccessActivity.class); // 设置启动标志:跳转到新页面时,栈中的原有实例都被清空,同时开辟新任务的活动栈 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); // 跳转到意图指定的活动页面 运行测试App,登录成功进入首页之后,点击返回键果然没回到登录页面。 ","date":"2023-04-25","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/:1:3","tags":["Android"],"title":"Android基础学习_活动Activity","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/"},{"categories":["Andorid基础"],"content":"1.2 在活动间传递消息 本节介绍如何在两个活动之间传递各类消息,首先描述Intent的用途和组成部分,以及显式Intent和隐式Intent的区别;接着阐述结合Intent和Bundle向下一个活动页面发送数据,再在下一个页面中解析收到的请求数据;然后叙述从下一个活动页面返回应答数据给上一个页面,并由上一个页面解析返回的应答数据。 ","date":"2023-04-25","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/:2:0","tags":["Android"],"title":"Android基础学习_活动Activity","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/"},{"categories":["Andorid基础"],"content":"1.2.1 显示Intent和隐式Intent 上一小节的Java代码,通过Intent对象设置活动的启动标志,这个Intent究竟是什么呢?Intent的中文名是意图,意思是我想让你干什么,简单地说,就是传递消息。Intent是各个组件之间信息沟通的桥梁,既能在Activity之间沟通,又能在Activity与Service之间沟通,也能在Activity与Broadcast之间沟通。总而言之,Intent用于Android各组件之间的通信,它主要完成下列3部分工作: (1)标明本次通信请求从哪里来、到哪里去、要怎么走。 (2)发起方携带本次通信需要的数据内容,接收方从收到的意图中解析数据。 (3)发起方若想判断接收方的处理结果,意图就要负责让接收方传回应答的数据内容。 为了做好以上工作,就要给意图配上必需的装备,Intent的组成部分见下表。 元素名称 设置方法 说法与用途 Component setComponent 组件,它指定意图的来源和目标 Action setAction 动作,它指定意图的动作行为 Data setData 即Uri,它指定动作要操纵的数据路径 Category addCategory 类别,它指定意图的操作类别 Type setType 数据类型,它指定消费的数据类型 Extras putExtras 扩展信息,它指定装载的包裹信息 Flags setFlags 标志位,它指定的启动标志 指定意图对象的目标有两种表达方式,一种是显式Intent,另一种是隐式Intent。 1.2.1.1 显示的Intent,直接指定来源活动与目标活动,属于精准匹配 在构建一个意图对象时,需要指定两个参数,第一个参数表示跳转的来源界面,即来源Activity.this,第二个参数为需要跳转的页面,即目标Activity.class。具体的构建方法与如下三种: 1.在Intent的构造函数中指定。 Intent intent = new Intent(this, ActNextActivity.class); // 创建一个目标确定的意图 2.调用Intent实例对象的setClass方法指定。 Intent intent = new Intent(); // 创建一个新意图 intent.setClass(this, ActNextActivity.class); // 设置意图要跳转的目标活动 3.调用意图对象的setComponent方法指定 ntent intent = new Intent(); // 创建一个新意图 // 创建包含目标活动在内的组件名称对象 ComponentName component = new ComponentName(this, ActNextActivity.class); intent.setComponent(component); // 设置意图携带的组件信息 1.2.1.2 隐式Intent 没有明确指定要跳转的目标活动,只给出一个动作字符串让系统自动匹配,属于模糊匹配 通常App不希望向外部暴露活动名称,只给出一个事先定义好的标记串,这样大家约定俗成、按图索骥 就好,隐式Intent便起到了标记过滤作用。这个动作名称标记串,可以是自己定义的动作,也可以是已 有的系统动作。常见系统动作的取值说明见下表。 Intent类的系统动作常量名 系统动作的常量值 说明 ACTION_MAIN android.intent.action.MAIN App启动时的入口 ACTION_VIEW android.intent.action.VIEW 向用户显示数据 ACTION_SEND android.intent.action.SEND 分享内容 ACTION_CALL android.intent.action.CALL 直接拨号 ACITON_DIAL android.intent.action.DIAL 准备拨号 ACTION_SENDTO android.intent.action.SENDTO 发送短信 ACTION_ANSWER android.intent.action.ANSWER 接听电话 动作名称既可以通过setAction方法指定,也可以通过构造函数Intent(String action)直接生成意图对象。当然,由于动作是模糊匹配,因此有时需要更详细的路径,比如仅知道某人住在天通苑小区,并不能直接找到他家,还得说明他住在天通苑的哪一期、哪栋楼、哪一层、哪一个单元。Uri和Category便是这样的路径与门类信息,Uri数据可通过构造函数Intent(String action, Uri uri)在生成对象时一起指定,也可通过setData方法指定(setData这个名字有歧义,实际相当于setUri);Category可通过addCategory方法指定,之所以用add而不用set方法,是因为一个意图允许设置多个Category,方便一起过滤。下面是一个调用系统拨号程序的代码例子,其中就用到了Uri: String phoneNo = \"12345\"; Intent intent = new Intent(); // 创建一个新意图 intent.setAction(Intent.ACTION_DIAL); // 设置意图动作为准备拨号 Uri uri = Uri.parse(\"tel:\" + phoneNo); // 声明一个拨号的Uri intent.setData(uri); // 设置意图前往的路径 startActivity(intent); // 启动意图通往的活动页面 隐式Intent还用到了过滤器的概念,把不符合匹配条件的过滤掉,剩下符合条件的按照优先顺序调用。 譬如创建一个App模块,AndroidManifest.xml里的intent-filter就是配置文件中的过滤器。像最常见的首页活动MainAcitivity,它的activity节点下面便设置了action和category的过滤条件。其中 android.intent.action.MAIN表示App的入口动作,而android.intent.category.LAUNCHER表示在桌面上显示App图标,配置样例如下: \u003cactivity android:name=\".MainActivity\"\u003e \u003cintent-filter\u003e \u003caction android:name=\"android.intent.action.MAIN\" /\u003e \u003ccategory android:name=\"android.intent.category.LAUNCHER\" /\u003e \u003c/intent-filter\u003e \u003c/activity\u003e ","date":"2023-04-25","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/:2:1","tags":["Android"],"title":"Android基础学习_活动Activity","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/"},{"categories":["Andorid基础"],"content":"1.2.2 向下一个Activity发送数据 Intent对象的setData方法只指定到达目标的路径,并非本次通信所携带的参数信息,真正的参数信息存放在Extras中。Intent重载了很多种putExtra方法传递各种类型的参数,包括整型、双精度型、字符串等基本数据类型,甚至Serializable这样的序列化结构。只是调用putExtra方法显然不好管理,像送快递一样大小包裹随便扔,不但找起来不方便,丢了也难以知道。所以Android引入了Bundle概念,可以把Bundle理解为超市的寄包柜或快递收件柜,大小包裹由Bundle统一存取,方便又安全。Bundle内部用于存放消息的数据结构是Map映射,既可添加或删除元素,还可判断元素是否存在。开发者若要把Bundle数据全部打包好,只需调用一次意图对象的putExtras方法;若要把Bundle数据全部取出来,也只需调用一次意图对象的getExtras方法。Bundle对象操作各类型数据的读写方法说明见下表. 数据类型 读方法 写方法 整型 getInt putInt 浮点型 getFloat putDouble 双精度型 getDouble putDouble 布尔值 getBoolean putBoolean 字符串 getString putString 字符数组 getStringArray putStringArray 字符串列表 getStringArrayList putStringArrayList 可序列化结构 getSerializable putSerializable 接下来举个在活动之间传递数据的例子,首先在上一个活动使用包裹封装好数据,把包裹塞给意图对 象,再调用startActivity方法跳到意图指定的目标活动。完整的活动跳转代码示例如下: // 创建一个意图对象,准备跳到指定的活动页面 Intent intent = new Intent(this, ActReceiveActivity.class); Bundle bundle = new Bundle(); // 创建一个新包裹 // 往包裹存入名为request_time的字符串 bundle.putString(\"request_time\", DateUtil.getNowTime()); // 往包裹存入名为request_content的字符串 bundle.putString(\"request_content\", tv_send.getText().toString()); intent.putExtras(bundle); // 把快递包裹塞给意图 startActivity(intent); // 跳转到意图指定的活动页面 然后在下一个活动中获取意图携带的快递包裹,从包裹取出各参数信息,并将传来的数据显示到文本视 图。下面便是目标活动获取并展示包裹数据的代码例子 // 从布局文件中获取名为tv_receive的文本视图 TextView tv_receive = findViewById(R.id.tv_receive); // 从上一个页面传来的意图中获取快递包裹 Bundle bundle = getIntent().getExtras(); // 从包裹中取出名为request_time的字符串 String request_time = bundle.getString(\"request_time\"); // 从包裹中取出名为request_content的字符串 String request_content = bundle.getString(\"request_content\"); String desc = String.format(\"收到请求消息:\\n请求时间为%s\\n请求内容为%s\", request_time, request_content); tv_receive.setText(desc); // 把请求消息的详情显示在文本视图上 ","date":"2023-04-25","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/:2:2","tags":["Android"],"title":"Android基础学习_活动Activity","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/"},{"categories":["Andorid基础"],"content":"1.2.3 向上一个Activity发送数据 数据传递经常是相互的,上一个页面不但把请求数据发送到下一个页面,有时候还要处理下一个页面的 应答数据,所谓应答发生在下一个页面返回到上一个页面之际。如果只把请求数据发送到下一个页面, 上一个页面调用startActivity方法即可;如果还要处理下一个页面的应答数据,此时就得分多步处理,详细步骤说明如下: 步骤一,上一个页面打包好请求数据,调用startActivityForResult方法执行跳转动作,表示需要处理下一个页面的应答数据,该方法的第二个参数表示请求代码,它用于标识每个跳转的唯一性。但是由于startActivityResult这个方法已经过时了。需要使用registerForActivityResult方法代替。代码如下: 该request类先创建一个意图 调用regist.launch方法携带信息跳转到下一个活动视图。接受到回传的消息之后通过之前register设置的callback函数解析intent内的消息,显示在View内部。 package com.example.chapter04; import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AppCompatActivity; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; import com.example.chapter04.util.DataUtil; public class ActRequestActivity extends AppCompatActivity implements View.OnClickListener { private static final String mRequest = \"你睡了么?来我家睡吧\"; private ActivityResultLauncher\u003cIntent\u003e register; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_act_request); Button btn_request = findViewById(R.id.btn_request); TextView tv_response = findViewById(R.id.tv_response); TextView tv_request = findViewById(R.id.tv_request); tv_request.setText(\"待发送消息:\"+mRequest); btn_request.setOnClickListener(this); register = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -\u003e { if(result!=null){ Intent intent = result.getData(); if(intent!=null \u0026\u0026 result.getResultCode() == Activity.RESULT_OK){ Bundle bundle = intent.getExtras(); String response_time = bundle.getString(\"Response_time\"); String response_content = bundle.getString(\"Response_content\"); String desc = String.format(\"收到返回消息:\\n 返回时间:%s\\n返回内容:%s\",response_time,response_content); tv_response.setText(desc); } } }); } @Override public void onClick(View v) { Intent intent = new Intent(this, ActResponseActivity.class); Bundle bundle = new Bundle(); bundle.putString(\"Request_time\", DataUtil.getNowTime()); bundle.putString(\"Request_content\",mRequest); intent.putExtras(bundle); register.launch(intent); } } response类接收到上一个活动视图传来的消息,调用getIntent方法读取消息。之后通过点击按钮,执行serResult方法,回传携带消息的意图。 package com.example.chapter04; import androidx.appcompat.app.AppCompatActivity; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.TextView; import com.example.chapter04.util.DataUtil; public class ActResponseActivity extends AppCompatActivity implements View.OnClickListener { private static final String mResponse = \"我还没睡,我爸妈不在家\"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_act_response); TextView tv_request = findViewById(R.id.tv_request); //从上个页面传来的意图中获取快递包裹 Bundle bundle = getIntent().getExtras(); String request_time = bundle.getString(\"Request_time\"); String request_content = bundle.getString(\"Request_content\"); String desc = String.format(\"收到请求消息:\\n 请求时间:%s\\n请求内容:%s\",request_time,request_content); //把请求信息的详情传到文本视图上 tv_request.setText(desc); findViewById(R.id.tv_response).setOnClickListener(this); TextView tv_response = findViewById(R.id.tv_response); tv_response.setText(\"待返回的消息:\"+mResponse); } @Override public void onClick(View v) { Intent intent = new Intent(this, ActRequestActivity.class); Bundle bundle = new Bundle(); bundle.putString(\"Response_time\", DataUtil.getNowTime()); bundle.putString(\"Response_content\",mResponse); intent.putExtras(bundle); //携带意图返回上一个页面,result_ok表示处理成功 setResult(Activity.RESULT_OK,intent); //结束当前页面 finish(); } } ","date":"2023-04-25","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/:2:3","tags":["Android"],"title":"Android基础学习_活动Activity","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/"},{"categories":["Andorid基础"],"content":"1.3 为活动补充信息 ","date":"2023-04-25","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/:3:0","tags":["Android"],"title":"Android基础学习_活动Activity","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/"},{"categories":["Andorid基础"],"content":"1.3.1 利用资源文件配置字符串 通过res/value/string.xml内配置String字符串来补充信息。 ","date":"2023-04-25","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/:3:1","tags":["Android"],"title":"Android基础学习_活动Activity","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/"},{"categories":["Andorid基础"],"content":"1.3.2 利用元数据传递配置信息 尽管资源文件能够配置字符串参数,然而有时候为安全起见,某个参数要给某个活动专用,并不希望其 他活动也能获取该参数,此时就不方便到处使用getString了。好在Activity提供了元数据(Metadata)的概念,元数据是一种描述其他数据的数据,它相当于描述固定活动的参数信息。打开 AndroidManifest.xml,在测试活动的activity节点内部添加meta-data标签,通过属性name指定元数据的名称,通过属性value指定元数据的值。仍以天气为例,添加meta-data标签之后的activity节点如下所示: \u003cactivity android:name=\".MetaDataActivity\"\u003e \u003cmeta-data android:name=\"weather\" android:value=\"晴天\" /\u003e \u003c/activity\u003e 配置好了activity节点的meta-data标签,再回到Java代码获取元数据信息,获取步骤分为下列3步: 1.调用getPackageManager方法获得当前应用的包管理器。 2.调用包管理器的getActivityInfo方法获得当前活动的信息对象。 3.活动信息对象的metaData是Bundle包裹类型,调用包裹对象的getString即可获得指定名称的参数 值。 上述三个步骤的代码如下: TextView tv_meta = findViewById(R.id.tv_mate); //从上下文里面获取我们的包管理器 PackageManager pm = getPackageManager(); //获取活动附加的元数据 try { ActivityInfo activityInfo = pm.getActivityInfo(getComponentName(), PackageManager.GET_META_DATA); Bundle metaData = activityInfo.metaData; String weather = metaData.getString(\"android.app.shortcuts\"); tv_meta.setText(weather); } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException(e); } ","date":"2023-04-25","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/:3:2","tags":["Android"],"title":"Android基础学习_活动Activity","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/"},{"categories":["Andorid基础"],"content":"1.3.3 给应用注册快捷方式 元数据不单单能传递简单的字符串参数,还能传送更复杂的资源数据,从Android 7.1开始新增的快捷方 式便用到了这点,譬如在手机桌面上长按支付宝图标,会弹出如图5的快捷菜单。 参考链接如下创建快捷方式教程 创建静态快捷方式 1.在应用的清单文件 (AndroidManifest.xml) 中,找到 intent 过滤器设置为 android.intent.action.MAIN 操作和 android.intent.category.LAUNCHER 类别的 Activity。 2.向此 Activity 添加 元素,该元素引用了定义应用快捷方式的资源文件 \u003cmanifest xmlns:android=\"http://schemas.android.com/apk/res/android\" package=\"com.example.myapplication\"\u003e \u003capplication ... \u003e \u003cactivity android:name=\"Main\"\u003e \u003cintent-filter\u003e \u003caction android:name=\"android.intent.action.MAIN\" /\u003e \u003ccategory android:name=\"android.intent.category.LAUNCHER\" /\u003e \u003c/intent-filter\u003e \u003cmeta-data android:name=\"android.app.shortcuts\" android:resource=\"@xml/shortcuts\" /\u003e \u003c/activity\u003e \u003c/application\u003e \u003c/manifest\u003e 3.创建新的资源文件:res/xml/shortcuts.xml。 4.在这个新的资源文件中,添加 shortcuts 根元素,其中包含 shortcut 元素的列表。每个 shortcut 元素都包含有关一个静态快捷方式的信息,包括其图标、说明标签及其在应用内启动的 intent: \u003cshortcuts xmlns:android=\"http://schemas.android.com/apk/res/android\"\u003e \u003cshortcut android:shortcutId=\"compose\" android:enabled=\"true\" android:icon=\"@drawable/compose_icon\" android:shortcutShortLabel=\"@string/compose_shortcut_short_label1\" android:shortcutLongLabel=\"@string/compose_shortcut_long_label1\" android:shortcutDisabledMessage=\"@string/compose_disabled_message1\"\u003e \u003cintent android:action=\"android.intent.action.VIEW\" android:targetPackage=\"com.example.myapplication\" android:targetClass=\"com.example.myapplication.ComposeActivity\" /\u003e \u003c!-- If your shortcut is associated with multiple intents, include them here. The last intent in the list determines what the user sees when they launch this shortcut. --\u003e \u003ccategories android:name=\"android.shortcut.conversation\" /\u003e \u003ccapability-binding android:key=\"actions.intent.CREATE_MESSAGE\" /\u003e \u003c/shortcut\u003e \u003c!-- Specify more shortcuts here. --\u003e \u003c/shortcuts\u003e shortcutId:为该shortcut的ID enabled:为是否启用该shortcut icon:表示快捷方式的图标 shortcutShortLabel:显示的最短标签 shortcutLongLabel:显示的最长标签,如果最长标签超出显示范围,则显示最短标签 shortcutDisabledMessage:当快捷方式不可使用的时候,显示的信息 action:给出目标的动作 targetPackage为启动活动的当前包的路径 targetClass为当前包的路径下的启动的活动的名称 categories android:name=“android.shortcut.conversation” 目前只给出这种写法 创建动态快捷方式 参考上述连接。之后补充。 ","date":"2023-04-25","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/:3:3","tags":["Android"],"title":"Android基础学习_活动Activity","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E6%B4%BB%E5%8A%A8activity/"},{"categories":["Andorid基础"],"content":"1.Android开发 简单控件 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:0:0","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.1 文本的显示 文本的显示有两种:一种为xml文件中设置,一种为java代码中设置。具体可以设置文本内容,文字大小,文本颜色,文本背景色等。 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:1:0","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.1.1 设置文件内容 文本内容设置有两种方式,一种是在XML文件中通过属性andorid:text设置文本,如下面这样: \u003cTextView android:id=\"@+id/tv_hello\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:text=\"你好世界\" /\u003e 另外一种是在java代码中调用文本视图对象的setText方法设置文本,如下面这样: //获取名为tv_hello的文本视图 TextView tv_hello = findViewById(R.id.tv_hello); tv_hello.setText(R.string.hello); 在XML文件中设置文本的话,在页面的右上方会出现一个黄色感叹号点击感叹号会出现如下解释。 看到提示内容为“Hardcoded string “你好,世界”, should use @string resouce”,意思说这几个字是硬编码的字符串,建议使用来自@string的资源。原来Android Studio不推荐在XML布局文件里直接写字符串,因为可能有好几个页面都显示“你好,世界”,若想把这句话换成“你吃饭了吗?”,就得一个一个XML文件改过去,无疑费时费力。故而Android Studio推荐把字符串放到专门的地方管理,这个名为@string的地方位于res/values目录下的strings.xml,打开该文件发现它的初始内容如下所示: \u003cresources\u003e \u003cstring name=\"app_name\"\u003echapter03\u003c/string\u003e \u003c/resources\u003e strings.xml定义了一个名为“app_name”的字符串常量,其值为“chapter03”。 为了解除硬编码问题,只需要在strings.xml内定义一个名为\"hello\"的字符串,字符串的值为\"你好,世界\"。如下所示: \u003cresources\u003e \u003cstring name=\"app_name\"\u003echapter03\u003c/string\u003e \u003cstring name=\"hello\"\u003e你好,世界\u003c/string\u003e \u003c/resources\u003e 在strings.xml内添加完新的字符串定义之后。回到res/layout/下的XML文件内,将android:text属性值改为@string/字符串(即为@string/hello),就可以解决硬编码问题。 如果要在Java代码中引用字符串资源,则在调用setText方法时填写R.string.字符串名(即为R.string.hello)。就可以设置文本内容了. ```java //获取名为tv_hello的文本视图 TextView tv_hello = findViewById(R.id.tv_hello); tv_hello.setText(R.string.hello); 解决硬编码只需要利用解耦的思想,在strings.xml内定义一个字符串资源,每次修改这个字符串资源只需要在strings.xml内修改即可。 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:1:1","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.1.2 设置文本大小 TextView设置文本大小也有两种,即为XML内定义或者java代码内部定义。 xml内部定义只需要在TextView内定义android:textSize属性即可。 java内定义只需要根据id获取TextView的文本视图,调用setTextSize()方法设置文字大小即可。 //从布局文件中获取id为text_size的文本视图 TextView textSize = findViewById(R.id.text_size); textSize.setTextSize(30);//设置text_size的大小 这里的大小数值越大,文本就越大。大小数值越小,文本越小。并且深入看setTextSize()的实现代码可以发现。数值的单位其实就是sp。 @android.view.RemotableViewMethod public void setTextSize(float size) { setTextSize(TypedValue.COMPLEX_UNIT_SP, size); } 在XML文件中则通过属性android:textSize指定文本大小,可是如果给TextView标签添加“android:textSize=“30””,数字马上变成红色如图3-2所示,鼠标移过去还会提示错误“Cannot resolve symbol ‘30’”,意思是无法解析“30”这个符号。 文本大小存在不同的字号单位,XML文件要求在字号数字后面写明单位类型,常见的字号单位主要有px、dp、sp 3种 名称 解释 px(Pixel像素 也称为图像元素,是作为图像构成的基本单元,单个像素的大小并不固定,跟随屏幕大小和像素数量的关系变化,一个像素点为1px。 Resolution(分辨率) 是指屏幕的垂直和水平方向的像素数量,如果分辨率是 1920*1080 ,那就是垂直方向有 1920 个像素,水平方向有 1080 个像素。 Dpi(像素密度) 是指屏幕上每英寸(1英寸 = 2.54 厘米)距离中有多少个像素点。 Density(密度) 是指屏幕上每平方英寸(2.54 ^ 2 平方厘米)中含有的像素点数量。 Dip / dp (设备独立像素) 也可以叫做dp,长度单位,同一个单位在不同的设备上有不同的显示效果,具体效果根据设备的密度有关 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:1:2","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.1.3 设置文本颜色 文本颜色的修改,可以在java代码中使用setTextColor方法即可设置文本颜色,具体在color类中定义了12种颜色。 Color类中的颜色类型 说明 BLACK 黑色 DKGRAY 深灰 GRAY 灰色 LTGRAY 浅灰 WHITE 白色 RED 红色 GREEN 绿色 BLUE 蓝色 YELLOW 黄色 CYAN 青色 MAGENTA 玫红 TRANSPARENT 透明 比如,下列代码将视图的文字改为绿色。 //从布局文件中获取tc_col的文本视图 TextView tv_col = findViewById(R.id.tc_col); //将文本的颜色设置为Color类自带的红色 tv_col.setTextColor(Color.RED); 可是XML文件无法引用Color类的颜色常量,为此Android制定了一套规范的编码标准,将色值交由透明度alpha和RGB三原色(红色red、绿色green、蓝色blue)联合定义。该标准又有八位十六进制数与六位十六进制数两种表达方式,例如八位编码FFEEDDCC中,FF表示透明度,EE表示红色的浓度,DD表示绿色的浓度,CC表示蓝色的浓度。透明度为FF表示完全不透明,为00表示完全透明。RGB三色的数值大,表示颜色越浓,也就越暗;数值越小,表示颜色越淡,也就越亮。RGB亮到极致就白色,暗到极致就是黑色。至于六位十六进制编码,则有两种情况,它在XML文件中默认不透明(等价于透明度为FF),而在代码中默认透明(等价于透明度为00)。以下代码给两个文本视图分别设置六位色值与八位色值,注意添加0x前缀表示十六进制数: // 从布局文件中获取名为tv_code_six的文本视图 TextView tv_code_six = findViewById(R.id.tv_code_six); // 将tv_code_six的文字颜色设置为透明的绿色,透明就是看不到 tv_code_six.setTextColor(0x00ff00); // 从布局文件中获取名为tv_code_eight的文本视图 TextView tv_code_eight = findViewById(R.id.tv_code_eight); // 将tv_code_eight的文字颜色设置为不透明的绿色,即正常的绿色 tv_code_eight.setTextColor(0xff00ff00); 运行测试App,发现tv_code_six控件的文本不见了(其实是变透明了),而tv_code_eight控件的文本显示正常的绿色 在XML文件中可通过属性android:textColor设置文字颜色,但要给色值添加井号前缀“#”,设定好文本颜色的TextView标签示例如下: \u003cTextView android:id=\"@+id/tv_xml\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:text=\"布局文件设置六位文字颜色\" android:textColor=\"#00ff00\" android:textSize=\"17sp\" /\u003e ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:1:3","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.2 视图基础 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:2:0","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.2.1 设置视图的宽高 手机屏幕是块长方形区域,较短的那条边叫作宽,较长的那条边叫作高。App控件通常也是长方形状,控件宽度通过属性android:layout_width表达,控件高度通过属性android:layout_height表达,宽高的取值主要有下列3种: 1.match_parent:表示与上级视图保持一致。上级视图的尺寸有多大,当前视图的尺寸就有多大 2.wrap_content:表示与内容自适应。对于文本视图来说,内部文字需要多大的显示空间,当前视图就要占据多大的尺寸。但最宽不能超过上级视图的宽度,一旦超过就要换行;最高不能超过上级视图的高度,一旦超过就会隐藏。 3.以dp为单位的具体尺寸,比如300dp,表示宽度或者高度就是这么大。 在XML文件中采用以上任一方式均可设置视图的宽高,但在Java代码中设置宽高就有点复杂了,首先确保XML中的宽高属性值为wrap_content,这样才允许在代码中修改宽高。接着打开该页面对应的Java代码,依序执行以下3个步骤: 1.调用控件对象的getLayoutParams方法,获取该控件的布局参数,参数类型为 ViewGroup.LayoutParams。 2.布局参数的width属性表示宽度,height属性表示高度,修改这两个属性值,即可调整控件的宽高。 3.调用控件对象的setLayoutParams方法,填入修改后的布局参数使之生效。 不过布局参数的width和height两个数值默认是px单位,需要将dp单位的数值转换为px单位的数值,然后才能赋值给width属性和height属性。下面是把dp大小转为px大小的方法代码: // 根据手机的分辨率从 dp 的单位 转成为 px(像素) public static int dip2px(Context context, float dpValue) { // 获取当前手机的像素密度(1个dp对应几个px) float scale = context.getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); // 四舍五入取整 } 有了上面定义的公共方法dip2px,就能将某个dp数值转换成px数值,比如准备把文本视图的宽度改为300dp,那么调整宽度的Java代码示例如下: // 获取名为tv_code的文本视图 TextView tv_code = findViewById(R.id.tv_code); // 获取tv_code的布局参数(含宽度和高度) ViewGroup.LayoutParams params = tv_code.getLayoutParams(); // 修改布局参数中的宽度数值,注意默认px单位,需要把dp数值转成px数值 params.width = Utils.dip2px(this, 300); tv_code.setLayoutParams(params); // 设置tv_code的布局参数 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:2:1","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.2.2 设置视图的间距 设置视图的间距,通过编写TextView的属性,layout_margin或者padding来修改当前视图与视图外部以及视图内部间的间距。 属性名 作用 layout_margin 当前视图与上下左右间隔的距离 layout_marginTop 当前视图与上方间隔的距离 layout_marginLeft 当前视图与左边间隔的距离 layout_marginRight 当前视图与左边间隔的距离 layout_marginBottom 当前视图与下方间隔的距离 padding 当前视图的内部间距 paddingTop 当前视图与视图内容上方的间距 paddingLeft 当前视图与视图内容左边的间距 paddingRight 当前视图与视图内容右边的间距 paddingBottom 当前视图与视图内容下方的间距 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:2:2","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.2.3 设置视图的对齐方式 App界面上的视图排列,默认靠左朝上对齐,这也符合日常的书写格式。然而页面的排版不是一成不变的,有时出于美观或者其他原因,要将视图排列改为朝下或靠右对齐,为此需要另外指定视图的对齐方式。在XML文件中通过属性android:layout_gravity可以指定当前视图的对齐方向,当属性值为top时表示视图朝上对齐,为bottom时表示视图朝下对齐,为left时表示视图靠左对齐,为right时表示视图靠右对齐。如果希望视图既朝上又靠左,则用竖线连接top与left,此时属性标记为android:layout_gravity=“top|left”;如果希望视图既朝下又靠右,则用竖线连接bottom与right,此时属性标记为android:layout_gravity=“bottom|right”。注意layout_gravity规定的对齐方式,指的是当前视图往上级视图的哪个方向对齐,并非当前视图的内部对齐。若想设置内部视图的对齐方向,则需由当前视图的android:gravity指定,该属性一样拥有top、bottom、left、right 4种取值及其组合。它与layout_gravity的不同之处在于:layout_gravity设定了当前视图相对于上级视图的对齐方式,而gravity设定了下级视图相对于当前视图的对齐方式;前者决定了当前视图的位置,而后者决定了下级视图的位置。 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:2:3","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.3 常用布局 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:3:0","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.3.1 线性布局LinearLayout LinearLayout布局它的学名为线性布局。顾名思义,线性布局像是用一根线把它的内部视图串起来,故而内部视图之间的排列顺序是固定的,要么从左到右排列,要 么从上到下排列。在XML文件中,LinearLayout通过属性android:orientation区分两种方向,其中从左到右排列叫作水平方向,属性值为horizontal;从上到下排列叫作垂直方向,属性值为vertical。如果LinearLayout标签不指定具体方向,则系统默认该布局为水平方向排列,也就是默认android:orientation=“horizontal”。 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:3:1","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.3.2 相对布局RelativeLayout 相对布局的下级视图位置由其他视图决定。相对布局名为RelativeLayout,因为下级视图的位置是相对位置,所以得有具体的参照物才能确定最终的位置。如果不设置下级视图的参照物,那么下级视图默认显示在RelativeLayout内部的左上角。 用于确定下级视图位置的参照物分两种,一种是与该视图自身平级的视图;另一种是该视图的上级视图(也就是它归属的RelativeLayout)。 相对位置的属性取值 相对位置说明 layout_toLeftOf 当前视图在指定视图的左边 layout_toRightOf 当前视图在指定视图的右边 layout_above 当前视图在指定视图的上方 layout_below 当前视图在指定视图的下方 layout_alignLeft 当前视图与置顶视图的左侧对齐 layout_alignRight 当前视图与指定视图的右侧对齐 layout_alignTop 当前视图与指定视图的顶部对齐 layout_alignBottom 当前视图与指定视图的底部对齐 layout_centerInParent 当前视图在上级视图的中间 layout_centerHorizontal 当前视图在上级视图的水平方向居中 layout_centerVertical 当前视图在上级视图的垂直方向居中 layout_alignParentLeft 当前视图与上级视图的左侧对齐 layout_alignParentRight 当前视图与上级视图的右侧对齐 layout_alignParentTop 当前视图与上级视图的顶部对齐 layout_alignParentBottom 当前视图与上级视图的底部对齐 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:3:2","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.3.3 网格布局GridLayout 实现多行多列的布局方式,可以采用网格布局GridLayout。 网格布局默认从左往右、从上到下排列,它先从第一行从左往右放置下级视图,塞满之后另起一行放置其余的下级视图,如此循环往复直至所有下级视图都放置完毕。为了判断能够容纳几行几列,网格布局新增了android:columnCount与android:rowCount两个属性,其中columnCount指定了网格的列数,即每行能放多少个视图;rowCount指定了网格的行数,即每列能放多少个视图。 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:3:3","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.3.4 滚动布局ScrollView 手机屏幕的显示空间有限,常常需要上下滑动或左右滑动才能拉出其余页面内容,可惜一般的布局节点都不支持自行滚动,这时就要借助滚动视图了。与线性布局类似,滚动视图也分为垂直方向和水平方向两类,其中垂直滚动视图名为ScrollView,水平滚动视图名为HorizontalScrollView。这两个滚动视图的 使用并不复杂,主要注意以下3点: (1)垂直方向滚动时,layout_width属性值设置为match_parent,layout_height属性值设置为wrap_content。 (2)水平方向滚动时,layout_width属性值设置为wrap_content,layout_height属性值设置为match_parent。 (3)滚动视图节点下面必须且只能挂着一个子布局节点,否则会在运行时报错Caused by:java.lang.IllegalStateException:ScrollView can host only one direct child。 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:3:4","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.4 按钮触控 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:4:0","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.4.1 按钮控件Button 除了文本视图之外,按钮Button也是一种基础控件。因为Button是由TextView派生而来,所以文本视图拥有的属性和方法,包括文本内容、文本大小、文本颜色等,按钮控件均能使用。不同的是,Button拥有默认的按钮背景,而TextView默认无背景;Button的内部文本默认居中对齐,而TextView的内部文本默认靠左对齐。此外,按钮还要额外注意textAllCaps与onClick两个属性,分别介绍如下: 1.textAllCaps属性 对于TextView来说,text属性设置了什么文本,文本视图就显示什么文本。但对于Button来说,不管text属性设置的是大写字母还是小写字母,按钮控件都默认转成大写字母显示。比如在XML文件中加入下面的Button标签: \u003cButton android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:text=\"Hello World\"/\u003e 编译运行后的App界面,按钮上显示全大写的“HELLO WORLD”,而非原来大小写混合的“HelloWorld”。显然这个效果不符合预期,为此需要给Button标签补充textAllCaps属性,该属性默认为true表示全部转为大写,如果设置为false则表示不转为大写。于是在布局文件添加新的Button标签,该标签补充了android:textAllCaps=“false”,具体内容如下所示: \u003cButton android:layout_width=\"match_parent\" android:layout_height=\"wrap_content\" android:text=\"Hello World\" android:textAllCaps=\"false\"/\u003e 2.onClick属性(已废弃) 按钮之所以成为按钮,是因为它会响应按下动作,就手机而言,按下动作等同于点击操作,即手指轻触屏幕然后马上松开。每当点击按钮之时,就表示用户确认了某个事项,接下来轮到App接着处理了。onClick属性便用来接管用户的点击动作,该属性的值是个方法名,也就是当前页面的Java代码存在这么一个方法:当用户点击按钮时,就自动调用该方法。 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:4:1","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.4.2 点击事件和长按事件 虽然按钮控件能够在XML文件中通过onClick属性指定点击方法,但是方法的名称可以随便叫,既能叫doClick也能叫doTouch,甚至叫它doA或doB都没问题,这样很不利于规范化代码,倘若以后换了别人接手,就不晓得doA或doB是干什么用的。因此在实际开发中,不推荐使用Button标签的onClick属性,而是在代码中给按钮对象注册点击监听器。所谓监听器,意思是专门监听控件的动作行为,它平时无所事事,只有控件发生了指定的动作,监听器才会触发开关去执行对应的代码逻辑。点击监听器需要实现接口View.OnClickListener,并重写onClick方法补充点击事件的处理代码,再由按钮调用setOnClickListener方法设置监听器对象。比如下面的代码给按钮控件btn_click_single设置了一个点击监听器: // 从布局文件中获取名为btn_click_single的按钮控件 Button btn_click_single = findViewById(R.id.btn_click_single); // 给btn_click_single设置点击监听器,一旦用户点击按钮,就触发监听器的onClick方法 btn_click_single.setOnClickListener(new MyOnClickListener()); 上面的点击监听器名为MyOnClickListener,它的定义代码示例如下: // 定义一个点击监听器,它实现了接口View.OnClickListener class MyOnClickListener implements View.OnClickListener { @Override public void onClick(View v) { // 点击事件的处理方法 String desc = String.format(\"%s 您点击了按钮:%s\",DateUtil.getNowTime(), ((Button)v).getText()); tv_result.setText(desc); // 设置文本视图的文本内容 } } 如果一个页面只有一个按钮,单独定义新的监听器倒也无妨,可是如果存在许多按钮,每个按钮都定义自己的监听器,那就劳民伤财了。对于同时监听多个按钮的情况,更好的办法是注册统一的监听器,也就是让当前页面实现接口View.OnClickListener,如此一来,onClick方法便写在了页面代码之内。因为是统一的监听器,所以onClick内部需要判断是哪个按钮被点击了,也就是利用视图对象的getId方法检查控件编号,完整的onClick代码举例如下: @Override public void onClick(View v) { // 点击事件的处理方法 if (v.getId() == R.id.btn_click_public) { // 来自于按钮btn_click_public String desc = String.format(\"%s 您点击了按钮:%s\",DateUtil.getNowTime(), ((Button)v).getText()); tv_result.setText(desc); // 设置文本视图的文本内容 } } 当然该页面的onCreate内部别忘了调用按钮对象的setOnClickListener方法,把按钮的点击监听器设置成当前页面,设置代码如下所示: // 从布局文件中获取名为btn_click_public的按钮控件 Button btn_click_public = findViewById(R.id.btn_click_public); // 设置点击监听器,一旦用户点击按钮,就触发监听器的onClick方法 btn_click_public.setOnClickListener(this); 除了点击事件,Android还设计了另外一种长按事件,每当控件被按住超过500毫秒之后,就会触发该控件的长按事件。若要捕捉按钮的长按事件,可调用按钮对象的setOnLongClickListener方法设置长按监听器。具体的设置代码示例如下: @Override public boolean onLongClick(View v) { // 长按事件的处理方法 if (v.getId() == R.id.btn_longclick_public) { // 来自于按钮 btn_longclick_public String desc = String.format(\"%s 您长按了按钮:%s\",DateUtil.getNowTime(), ((Button)v).getText()); tv_result.setText(desc); // 设置文本视图的文本内容 } return true; } 值得注意的是,点击监听器和长按监听器不局限于按钮控件,其实它们都来源于视图基类View,凡是从View派生而来的各类控件,均可注册点击监听器和长按监听器。譬如文本视图TextView,其对象也能调用setOnClickListener方法与setOnLongClickListener方法,此时TextView控件就会响应点击动作和长按动作。因为按钮存在按下和松开两种背景,便于提示用户该控件允许点击,但文本视图默认没有按压背景,不方便判断是否被点击,所以一般不会让文本视图处理点击事件和长按事件。 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:4:2","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.4.3 禁用与恢复按钮 尽管按钮控件生来就是给人点击的,可是某些情况希望暂时禁止点击操作,譬如用户在注册的时候,有的网站要求用户必须同意指定条款,而且至少浏览10秒之后才能点击注册按钮。那么在10秒之前,注册按钮应当置灰且不能点击,等过了10秒之后,注册按钮才恢复正常。在这样的业务场景中,按钮先后拥有两种状态,即不可用状态与可用状态,它们在外观和功能上的区别如下: (1)不可用按钮:按钮不允许点击,即使点击也没反应,同时按钮文字为灰色。 (2)可用按钮:按钮允许点击,点击按钮会触发点击事件,同时按钮文字为正常的黑色。 从上述的区别说明可知,不可用与可用状态主要有两点差异:其一,是否允许点击;其二,按钮文字的颜色。就文字颜色而言,可在布局文件中使用textColor属性设置颜色,也可在Java代码中调用setTextColor方法设置颜色。至于是否允许点击,则需引入新属性android:enabled,该属性值为true时表示启用按钮,即允许点击按钮;该属性值为false时表示禁用按钮,即不允许点击按钮。在Java代码 中,则可通过setEnabled方法设置按钮的可用状态(true表示启用,false表示禁用)。 在Java代码中给3个按钮分别注册点击监听器,注册代码如下所示: // 因为按钮控件的setOnClickListener方法来源于View基类,所以也可对findViewById得到的视图直 接设置点击监听器 findViewById(R.id.btn_enable).setOnClickListener(this); findViewById(R.id.btn_disable).setOnClickListener(this); btn_test = findViewById(R.id.btn_test); // 获取名叫btn_test的按钮控件 btn_test.setOnClickListener(this); // 设置btn_test的点击监听器 同时重写页面的onClick方法,分别处理3个按钮的点击事件,修改之后的onClick代码示例如下: @Override public void onClick(View v) { // 点击事件的处理方法 // 由于多个控件都把点击监听器设置到了当前页面,因此公共的onClick方法内部需要区分来自于哪个按钮 if (v.getId() == R.id.btn_enable) { // 点击了按钮“启用测试按钮” btn_test.setTextColor(Color.BLACK); // 设置按钮的文字颜色 btn_test.setEnabled(true); // 启用当前控件 } else if (v.getId() == R.id.btn_disable) { // 点击了按钮“禁用测试按钮” btn_test.setTextColor(Color.GRAY); // 设置按钮的文字颜色 btn_test.setEnabled(false); // 禁用当前控件 } else if (v.getId() == R.id.btn_test) { // 点击了按钮“测试按钮” String desc = String.format(\"%s 您点击了按钮:%s\", DateUtil.getNowTime(), ((Button)v).getText()); tv_result.setText(desc); // 设置文本视图的文本内容 } } ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:4:3","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.5 图像显示 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:5:0","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.5.1 图像视图ImageView 显示文本用到了文本视图TextView,显示图像则用到图像视图ImageView。由于图像通常保存为单独的图片文件,因此需要先把图片放到res/drawable目录,然后再去引用该图片的资源名称。比如现在有张苹果图片名为apple.png,那么XML文件通过属性android:src设置图片资源,属性值格式形如“@drawable/不含扩展名的图片名称”。添加了src属性的ImageView标签示例如下: \u003cImageView android:id=\"@+id/iv_scale\" android:layout_width=\"match_parent\" android:layout_height=\"220dp\" android:src=\"@drawable/apple\" /\u003e 若想在Java代码中设置图像视图的图片资源,可调用ImageView控件的setImageResource方法,方法参数格式形如“R.drawable.不含扩展名的图片名称”。仍以上述的苹果图片为例,给图像视图设置图片资源的代码例子如下所示 // 从布局文件中获取名为iv_scale的图像视图 ImageView iv_scale = findViewById(R.id.iv_scale); iv_scale.setImageResource(R.drawable.apple); // 设置图像视图的图片资源 ImageView本身默认图片居中显示,不管图片有多大抑或有多小,图像视图都会自动缩放图片,使之刚好够着ImageView的边界,并且缩放后的图片保持原始的宽高比例,看起来图片很完美地占据视图中央。这种缩放类型在XML文件中通过属性android:scaleType定义,即使图像视图未明确指定该属性,系统也会默认其值为fitCenter,表示让图像缩放后居中显示。 XNL中的缩放类型 Scale Type类中的缩放类型 说明 fitCenter FIT_CENTER 保持宽高比例,缩放图片使其位于视图中间 centerCrop CENTERT_CROP 缩放图片使其充满视图,超出部分会被裁剪,并位于视图中心 centerInside CENTER_INSIDE 保持宽高比例,缩小图片使之位于视图中间,只缩小不放大 center CENTER 保持图片原尺寸,使其位于视图中间 fitXY FIT_XY 缩放图片使其正好填满视图,视图可能被拉伸变形 fitStart FIT_START 保持宽高比,缩放图片使其位于视图上方或左侧 fitEnd FIT_END 保持宽高比,缩放图片使其位于视图下方或右侧 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:5:1","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.5.2 图像按钮ImageButton 常见的按钮控件Button其实是文本按钮,因为按钮上面只能显示文字,不能显示图片,ImageButton才是显示图片的图像按钮。虽然ImageButton号称图像按钮,但它并非继承Button,而是继承了ImageView,所以凡是ImageView拥有的属性和方法,ImageButton统统拿了过来,区别在于ImageButton有个按钮背景。 尽管ImageButton源自ImageView,但它毕竟是个按钮呀,按钮家族常用的点击事件和长按事件,ImageButton全都没落下。不过ImageButton和Button之间除了名称不同,还有下列差异:Button既可显示文本也可显示图片(通过setBackgroundResource方法设置背景图片),而ImageButton只能显示图片不能显示文本。ImageButton上的图像可按比例缩放,而Button通过背景设置的图像会拉伸变形,因为背景图采取fitXY方式,无法按比例缩放。Button只能靠背景显示一张图片,而ImageButton可分别在前景和背景显示图片,从而实现两张图片叠加的效果。从上面可以看出,Button与ImageButton各有千秋,通常情况使用Button就够用了。但在某些场合,比如输入法打不出来的字符,以及特殊字体显示的字符串,就适合先切图再放到ImageButton。举个例子,数学常见的开平方运算,由输入法打出来的运算符号为“√”,但该符号缺少右上角的一横,正确的开平方符号是带横线的,此时便需要通过ImageButton显示这个开方图片。不过使用ImageButton得注意,图像按钮默认的缩放类型为center(保持原始尺寸不缩放图片),而非 图像视图默认的fitCenter,倘若图片尺寸较大,那么图像按钮将无法显示整个图片。为避免显示不完整的情况,XML文件中的ImageButton标签必须指定fitCenter的缩放类型, ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:5:2","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["Andorid基础"],"content":"1.5.3 同时展示文本与图像 现在有了Button可在按钮上显示文字,又有ImageButton可在按钮上显示图像,照理说绝大多数场合都够用了。然而现实项目中的需求往往捉摸不定,例如客户要求在按钮文字的左边加一个图标,这样按钮内部既有文字又有图片,乍看之下Button和ImageButton都没法直接使用。若用LinearLayout对ImageView和TextView组合布局,虽然可行,XML文件却变得冗长许多。其实有个既简单又灵活的办法,要想在文字周围放置图片,使用按钮控件Button就能实现。原来Button悄悄提供了几个与图标有关的属性,通过这些属性即可指定文字旁边的图标,以下是有关的图标属性说明。 drawableTop:指定文字上方的图片。 drawableBottom:指定文字下方的图片。 drawableLeft:指定文字左边的图片。 drawableRight:指定文字右边的图片。 drawablePadding:指定图片与文字的间距。 ","date":"2023-04-24","objectID":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/:5:3","tags":["Android"],"title":"Android基础学习_简单控件","uri":"/android%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0_%E7%AE%80%E5%8D%95%E6%8E%A7%E4%BB%B6/"},{"categories":["力扣hot100"],"content":"接雨水 接雨水 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 示例1 输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] 输出:6 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 ","date":"2023-03-16","objectID":"/%E5%8A%9B%E6%89%A3hot20_%E6%8E%A5%E9%9B%A8%E6%B0%B4/:0:0","tags":["力扣hot100"],"title":"力扣hot100_20_接雨水","uri":"/%E5%8A%9B%E6%89%A3hot20_%E6%8E%A5%E9%9B%A8%E6%B0%B4/"},{"categories":["力扣hot100"],"content":"题解和思路 ","date":"2023-03-16","objectID":"/%E5%8A%9B%E6%89%A3hot20_%E6%8E%A5%E9%9B%A8%E6%B0%B4/:1:0","tags":["力扣hot100"],"title":"力扣hot100_20_接雨水","uri":"/%E5%8A%9B%E6%89%A3hot20_%E6%8E%A5%E9%9B%A8%E6%B0%B4/"},{"categories":["力扣hot100"],"content":"组合总和 组合总和 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。 candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 对于给定的输入,保证和为 target 的不同组合数少于 150 个。 示例1 输入:candidates = [2,3,6,7], target = 7 输出:[[2,2,3],[7]] 解释: 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7 。 仅有这两种组合。 ","date":"2023-03-16","objectID":"/%E5%8A%9B%E6%89%A3hot19_%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8C/:0:0","tags":["力扣hot100"],"title":"力扣hot100_19_组合总和","uri":"/%E5%8A%9B%E6%89%A3hot19_%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8C/"},{"categories":["力扣hot100"],"content":"题解和思路 注意:数字可被重复选,但是数字的个数的数量需要不同。 对数组先进性排序,其目的是如果target - candidates[i] \u003c 0 则其后面的都小于0,可以直接break 可以使用回溯算法,从数组的0-len开始进行回溯 回溯算法内部可以是顺序遍历数组的begin 和 end 位置,判断target - candidates[i]是否小于0,如果小于零 则break 如果等于0 则将其加入ans 并将target - candidates[i]继续传入dfs函数内部,进行进一步判断。 class Solution { public: vector\u003cvector\u003cint\u003e\u003e combinationSum(vector\u003cint\u003e\u0026 candidates, int target) { int len = candidates.size(); vector\u003cvector\u003cint\u003e\u003e ans; if(len == 0)return ans; sort(candidates.begin(),candidates.end()); vector\u003cint\u003e path; dfs(candidates,0,len,target,path,ans); return ans; } void dfs(vector\u003cint\u003e \u0026candidates,int begin,int end,int target,vector\u003cint\u003e\u0026path,vector\u003cvector\u003cint\u003e\u003e \u0026ans){ if(target == 0){ ans.push_back(path); return; } for(int i = begin;i \u003c end;i ++){ if(target-candidates[i]\u003c0)break; path.push_back(candidates[i]); dfs(candidates,i,end,target-candidates[i],path,ans); path.pop_back(); } } }; ","date":"2023-03-16","objectID":"/%E5%8A%9B%E6%89%A3hot19_%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8C/:1:0","tags":["力扣hot100"],"title":"力扣hot100_19_组合总和","uri":"/%E5%8A%9B%E6%89%A3hot19_%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8C/"},{"categories":["力扣hot100"],"content":"在数组中查找元素的第一个和最后一个位置 在数组中查找元素的第一个和最后一个位置 给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。 如果数组中不存在目标值 target,返回 [-1, -1]。 你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。 示例1 输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4] ","date":"2023-03-15","objectID":"/%E5%8A%9B%E6%89%A3hot18_%E5%9C%A8%E6%95%B0%E7%BB%84%E4%B8%AD%E6%9F%A5%E6%89%BE%E5%85%83%E7%B4%A0%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%92%8C%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E4%BD%8D%E7%BD%AE/:0:0","tags":["力扣hot100"],"title":"力扣hot100_18_在数组中查找元素的第一个和最后一个位置","uri":"/%E5%8A%9B%E6%89%A3hot18_%E5%9C%A8%E6%95%B0%E7%BB%84%E4%B8%AD%E6%9F%A5%E6%89%BE%E5%85%83%E7%B4%A0%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%92%8C%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E4%BD%8D%E7%BD%AE/"},{"categories":["力扣hot100"],"content":"题解和思路 使用两个二分 第一个二分将r往target处移动,如果有target,则l和r会逐渐重合,找出第一个target的下标 第二个二分将l往最后的target处移动,如果有target,则l和r会逐渐重合,找出最后一个target的下标 class Solution { public: vector\u003cint\u003e searchRange(vector\u003cint\u003e\u0026 nums, int target) { vector\u003cint\u003e ans = {-1,-1}; int len = nums.size(); if(len == 0)return ans; int l = 0; int r = len - 1; while(l\u003cr){ int mid = ( l + r ) \u003e\u003e 1; if(nums[mid]\u003e=target){ r = mid; }else{ l = mid + 1; } } if(nums[l]!=target)return ans; ans[0] = l; l = 0; r = len - 1; while(l\u003cr){ int mid = (l + r + 1) \u003e\u003e 1; if(nums[mid]\u003c=target){ l = mid; }else{ r = mid - 1; } } ans[1] = l; return ans; } }; ","date":"2023-03-15","objectID":"/%E5%8A%9B%E6%89%A3hot18_%E5%9C%A8%E6%95%B0%E7%BB%84%E4%B8%AD%E6%9F%A5%E6%89%BE%E5%85%83%E7%B4%A0%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%92%8C%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E4%BD%8D%E7%BD%AE/:1:0","tags":["力扣hot100"],"title":"力扣hot100_18_在数组中查找元素的第一个和最后一个位置","uri":"/%E5%8A%9B%E6%89%A3hot18_%E5%9C%A8%E6%95%B0%E7%BB%84%E4%B8%AD%E6%9F%A5%E6%89%BE%E5%85%83%E7%B4%A0%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%92%8C%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E4%BD%8D%E7%BD%AE/"},{"categories":["力扣hot100"],"content":"搜索旋转排序数组 搜索旋转排序数组 整数数组 nums 按升序排列,数组中的值 互不相同 。 在传递给函数之前,nums 在预先未知的某个下标 k(0 \u003c= k \u003c nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。 给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。 你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。 示例1 输入:nums = [4,5,6,7,0,1,2], target = 0 输出:4 ","date":"2023-03-14","objectID":"/%E5%8A%9B%E6%89%A3hot17_%E6%90%9C%E7%B4%A2%E6%97%8B%E8%BD%AC%E6%8E%92%E5%BA%8F%E6%95%B0%E7%BB%84/:0:0","tags":["力扣hot100"],"title":"力扣hot100_17_搜索旋转排序数组","uri":"/%E5%8A%9B%E6%89%A3hot17_%E6%90%9C%E7%B4%A2%E6%97%8B%E8%BD%AC%E6%8E%92%E5%BA%8F%E6%95%B0%E7%BB%84/"},{"categories":["力扣hot100"],"content":"题解和思路 可以通过mid和target的关系来判断target究竟处于哪个空间 在判断的同时还需要注意left 和 right 是否逆序 class Solution { public: int search(vector\u003cint\u003e\u0026 nums, int target) { int left = 0; int right = nums.size()-1; while(left \u003c= right){ int mid = (left + right)\u003e\u003e1; if(nums[mid]==target)return mid; if(nums[left]\u003c=nums[mid]){ if(nums[left]\u003c=target\u0026\u0026target\u003cnums[mid]){ right = mid -1; }else{ left = mid + 1; } }else{ if(target\u003enums[mid]\u0026\u0026target\u003c=nums[right]){ left = mid + 1; }else{ right = mid - 1; } } } return -1; } }; ","date":"2023-03-14","objectID":"/%E5%8A%9B%E6%89%A3hot17_%E6%90%9C%E7%B4%A2%E6%97%8B%E8%BD%AC%E6%8E%92%E5%BA%8F%E6%95%B0%E7%BB%84/:1:0","tags":["力扣hot100"],"title":"力扣hot100_17_搜索旋转排序数组","uri":"/%E5%8A%9B%E6%89%A3hot17_%E6%90%9C%E7%B4%A2%E6%97%8B%E8%BD%AC%E6%8E%92%E5%BA%8F%E6%95%B0%E7%BB%84/"},{"categories":["力扣hot100"],"content":"括号生成 括号生成 数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。 示例1 输入:n = 3 输出:[\"((()))\",\"(()())\",\"(())()\",\"()(())\",\"()()()\"] ","date":"2023-03-10","objectID":"/%E5%8A%9B%E6%89%A3hot13_%E6%8B%AC%E5%8F%B7%E7%94%9F%E6%88%90/:0:0","tags":["力扣hot100"],"title":"力扣hot100_13_括号生成","uri":"/%E5%8A%9B%E6%89%A3hot13_%E6%8B%AC%E5%8F%B7%E7%94%9F%E6%88%90/"},{"categories":["力扣hot100"],"content":"题解和思路 使用回溯算法 可以按照左右括号的匹配情况判断 优先使用左括号,如果左括号小于右括号,则使用右括号 class Solution { public: vector\u003cstring\u003e generateParenthesis(int n) { if(n\u003c=0){ return ans; } dfs(\"\",n,n); return ans; } private: vector\u003cstring\u003e ans; void dfs(const string \u0026str,int left,int right){ if(left==0\u0026\u0026right==0){ ans.push_back(str); return; } //优先使用左括号 if(left\u003e0){ dfs(str+'(',left-1,right); } if(left\u003cright){ dfs(str+')',left,right-1); } } }; ","date":"2023-03-10","objectID":"/%E5%8A%9B%E6%89%A3hot13_%E6%8B%AC%E5%8F%B7%E7%94%9F%E6%88%90/:1:0","tags":["力扣hot100"],"title":"力扣hot100_13_括号生成","uri":"/%E5%8A%9B%E6%89%A3hot13_%E6%8B%AC%E5%8F%B7%E7%94%9F%E6%88%90/"},{"categories":["力扣hot100"],"content":"合并两个有序链表 合并两个有序链表 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的 示例1 输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4] ","date":"2023-03-10","objectID":"/%E5%8A%9B%E6%89%A3hot12_%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8/:0:0","tags":["力扣hot100"],"title":"力扣hot100_12_合并两个有序链表","uri":"/%E5%8A%9B%E6%89%A3hot12_%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8/"},{"categories":["力扣hot100"],"content":"题解和思路 循环遍历两个链表,对比大小,小的插入到新链表 直到某个链表为空 继续循环不为空的链表插入到新链表 返回新链表 /** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode() : val(0), next(nullptr) {} * ListNode(int x) : val(x), next(nullptr) {} * ListNode(int x, ListNode *next) : val(x), next(next) {} * }; */ class Solution { public: ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) { ListNode* head = new ListNode(0); ListNode* tail = head; while(list1\u0026\u0026list2){ int v1 = list1-\u003eval; int v2 = list2-\u003eval; if(v1\u003ev2){ tail-\u003enext=new ListNode(v2); tail = tail-\u003enext; list2 = list2-\u003enext; }else{ tail-\u003enext=new ListNode(v1); tail = tail-\u003enext; list1 = list1-\u003enext; } } while(list1){ tail-\u003enext=new ListNode(list1-\u003eval); tail = tail-\u003enext; list1 = list1-\u003enext; } while(list2){ tail-\u003enext=new ListNode(list2-\u003eval); tail = tail-\u003enext; list2 = list2-\u003enext; } return head-\u003enext; } }; ","date":"2023-03-10","objectID":"/%E5%8A%9B%E6%89%A3hot12_%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8/:1:0","tags":["力扣hot100"],"title":"力扣hot100_12_合并两个有序链表","uri":"/%E5%8A%9B%E6%89%A3hot12_%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8/"},{"categories":["力扣hot100"],"content":"有效括号 有效括号 给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 每个右括号都有一个对应的相同类型的左括号。 示例1 输入:s = \"()[]{}\" 输出:true ","date":"2023-03-09","objectID":"/%E5%8A%9B%E6%89%A3hot11_%E6%9C%89%E6%95%88%E6%8B%AC%E5%8F%B7/:0:0","tags":["力扣hot100"],"title":"力扣hot100_11_有效括号","uri":"/%E5%8A%9B%E6%89%A3hot11_%E6%9C%89%E6%95%88%E6%8B%AC%E5%8F%B7/"},{"categories":["力扣hot100"],"content":"题解和思路 使用栈 将括号的组合放入哈希表中 依次判断即可 class Solution { public: map\u003cchar,char\u003e map = { {')', '('}, {']', '['}, {'}', '{'} }; bool isValid(string s1) { if(s1.size()%2 == 1)return false; stack\u003cchar\u003e z; for(char ch : s1){ if(map.count(ch)){ if(z.empty()||z.top()!=map[ch]){ return false; } z.pop(); }else{ z.push(ch); } } return z.empty(); } }; ","date":"2023-03-09","objectID":"/%E5%8A%9B%E6%89%A3hot11_%E6%9C%89%E6%95%88%E6%8B%AC%E5%8F%B7/:1:0","tags":["力扣hot100"],"title":"力扣hot100_11_有效括号","uri":"/%E5%8A%9B%E6%89%A3hot11_%E6%9C%89%E6%95%88%E6%8B%AC%E5%8F%B7/"},{"categories":["力扣hot100"],"content":"删除链表倒数第N个节点 删除链表倒数第N个节点 题目: 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 示例1 输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] ","date":"2023-03-09","objectID":"/%E5%8A%9B%E6%89%A3hot10_%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E5%80%92%E6%95%B0%E7%AC%ACn%E4%B8%AA%E8%8A%82%E7%82%B9/:0:0","tags":["力扣hot100"],"title":"力扣hot100_10_删除链表倒数第N个节点","uri":"/%E5%8A%9B%E6%89%A3hot10_%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E5%80%92%E6%95%B0%E7%AC%ACn%E4%B8%AA%E8%8A%82%E7%82%B9/"},{"categories":["力扣hot100"],"content":"题解和思路 使用快慢指针 定义一个fast指向head 定义一个新的ListNode节点dummy,并将其next指向head,让slow指针指向dummy 对fast进行遍历 遍历次数为n(倒数节点的值); 然后我们对slow和fast都进行遍历,直到fast节点指向null。这样slow就指向了倒数第n个节点。 那么如果我们让slow指向dummy,再进行遍历,就可以得到需要删除的节点的前置节点,就便于删除 /** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode() : val(0), next(nullptr) {} * ListNode(int x) : val(x), next(nullptr) {} * ListNode(int x, ListNode *next) : val(x), next(next) {} * }; */ class Solution { public: ListNode* removeNthFromEnd(ListNode* head, int n) { //快慢指针 ListNode *dummy = new ListNode(0,head); ListNode *fast = head; ListNode *slow = dummy; for(int i = 0;i\u003cn;i++){ fast = fast-\u003enext; } while(fast){ fast=fast-\u003enext; slow=slow-\u003enext; } slow-\u003enext = slow-\u003enext-\u003enext; return dummy-\u003enext; } }; ","date":"2023-03-09","objectID":"/%E5%8A%9B%E6%89%A3hot10_%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E5%80%92%E6%95%B0%E7%AC%ACn%E4%B8%AA%E8%8A%82%E7%82%B9/:1:0","tags":["力扣hot100"],"title":"力扣hot100_10_删除链表倒数第N个节点","uri":"/%E5%8A%9B%E6%89%A3hot10_%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E5%80%92%E6%95%B0%E7%AC%ACn%E4%B8%AA%E8%8A%82%E7%82%B9/"},{"categories":["力扣hot100"],"content":"电话号码的字母组合 电话号码的字母组合 题目: 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 “”,\" “,“abc”,“def”,“ghi”,“jkl”,“mno”,“pqrs”,“tuv”,“wxyz” 示例1 输入:digits = \"23\" 输出:[\"ad\",\"ae\",\"af\",\"bd\",\"be\",\"bf\",\"cd\",\"ce\",\"cf\"] ","date":"2023-03-09","objectID":"/%E5%8A%9B%E6%89%A3hot9_%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E6%AF%8D%E7%BB%84%E5%90%88/:0:0","tags":["力扣hot100"],"title":"力扣hot100_9_电话号码的字母组合","uri":"/%E5%8A%9B%E6%89%A3hot9_%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E6%AF%8D%E7%BB%84%E5%90%88/"},{"categories":["力扣hot100"],"content":"题解和思路 定义一个全局number变量去存储电话号码按键。 使用回溯法去回溯。 class Solution { public: vector\u003cstring\u003e number = {\"\",\" \",\"abc\",\"def\",\"ghi\",\"jkl\",\"mno\",\"pqrs\",\"tuv\",\"wxyz\"}; void dfs(vector\u003cstring\u003e \u0026ans,const string \u0026digits,int pos,string\u0026 temp){ if(pos==digits.size()){ ans.push_back(temp); return; } int num = digits[pos] - '0'; for(int i=0;i\u003cnumber[num].size();i++){ char ch = number[num][i]; temp.push_back(ch); dfs(ans,digits,pos+1,temp); temp.pop_back(); } } vector\u003cstring\u003e letterCombinations(string digits) { vector\u003cstring\u003e ans; string temp; if(digits.size()\u003c1)return ans; dfs(ans,digits,0,temp); return ans; } }; ","date":"2023-03-09","objectID":"/%E5%8A%9B%E6%89%A3hot9_%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E6%AF%8D%E7%BB%84%E5%90%88/:1:0","tags":["力扣hot100"],"title":"力扣hot100_9_电话号码的字母组合","uri":"/%E5%8A%9B%E6%89%A3hot9_%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E6%AF%8D%E7%BB%84%E5%90%88/"},{"categories":["力扣hot100"],"content":"三数之和 三数之和 题目: 给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请 你返回所有和为 0 且不重复的三元组。 注意:答案中不可以包含重复的三元组。 示例1 输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 解释: nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。 ","date":"2023-03-08","objectID":"/%E5%8A%9B%E6%89%A3hot8_%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/:0:0","tags":["力扣hot100"],"title":"力扣hot100_8_三数之和","uri":"/%E5%8A%9B%E6%89%A3hot8_%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/"},{"categories":["力扣hot100"],"content":"题解和思路 三个数之和,可以先固定一个数,再在内层使用双指针从除i外的两端开始向内查找, 并且,需要每次都对三个数进行查重。 class Solution { public: vector\u003cvector\u003cint\u003e\u003e threeSum(vector\u003cint\u003e\u0026 nums) { //创建答案数组 vector\u003cvector\u003cint\u003e\u003e ans; //判断长度 if(nums.size()\u003c3||nums.empty()) return ans; //排序 sort(nums.begin(),nums.end()); //从头循环 int i = 0 ; while(i\u003cnums.size()){ if(nums[i]\u003e0)break;//提前终止 int left = i+1; int right = nums.size()-1; while(left\u003cright){ int x = nums[i]; int y = nums[left]; int z = nums[right]; if(x + y \u003e0 - z){ right-- ; }else if(x + y \u003c 0 - z){ left ++; }else{ ans.push_back({nums[i],nums[left],nums[right]}); //不允许重复 while(left\u003cright\u0026\u0026nums[left] == nums[left+1]){ left++; } while(left\u003cright\u0026\u0026nums[right] == nums[right-1]){ right--; } left++; right--; } } //避免nums[i]重复 while(i+1\u003cnums.size()\u0026\u0026nums[i]==nums[i+1]){ i++; } i++; } return ans; } }; ","date":"2023-03-08","objectID":"/%E5%8A%9B%E6%89%A3hot8_%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/:1:0","tags":["力扣hot100"],"title":"力扣hot100_8_三数之和","uri":"/%E5%8A%9B%E6%89%A3hot8_%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/"},{"categories":["力扣hot100"],"content":"盛最多水的容器 盛最多水的容器 题目: 给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明:你不能倾斜容器 示例1 输入:[1,8,6,2,5,4,8,3,7] 输出:49 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。 ","date":"2023-03-08","objectID":"/%E5%8A%9B%E6%89%A3hot7_%E7%9B%9B%E6%9C%80%E5%A4%9A%E6%B0%B4%E7%9A%84%E5%AE%B9%E5%99%A8/:0:0","tags":["力扣hot100"],"title":"力扣hot100_7_盛最多水的容器","uri":"/%E5%8A%9B%E6%89%A3hot7_%E7%9B%9B%E6%9C%80%E5%A4%9A%E6%B0%B4%E7%9A%84%E5%AE%B9%E5%99%A8/"},{"categories":["力扣hot100"],"content":"题解和思路 双指针 双向向中间遍历 判断哪边最短,选取最短的边的计算结果 最短边向中间移动一格 返回结果 class Solution { public: int maxArea(vector\u003cint\u003e\u0026 height) { int left = 0; int right = height.size()-1; int ans = 0; while(left \u003c right){ if(height[right]\u003eheight[left]){ int area = height[left] *(right - left ); ans = max(ans,area); left++; }else{ int area = height[right] *(right - left ); ans = max(ans,area); right--; } } return ans; } }; ","date":"2023-03-08","objectID":"/%E5%8A%9B%E6%89%A3hot7_%E7%9B%9B%E6%9C%80%E5%A4%9A%E6%B0%B4%E7%9A%84%E5%AE%B9%E5%99%A8/:1:0","tags":["力扣hot100"],"title":"力扣hot100_7_盛最多水的容器","uri":"/%E5%8A%9B%E6%89%A3hot7_%E7%9B%9B%E6%9C%80%E5%A4%9A%E6%B0%B4%E7%9A%84%E5%AE%B9%E5%99%A8/"},{"categories":["力扣hot100"],"content":"最长回文子串 最长回文子串 题目: 给你一个字符串 s,找到 s 中最长的回文子串。 如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。 示例1 输入:s = \"babad\" 输出:\"bab\" 解释:\"aba\" 同样是符合题意的答案 ","date":"2023-03-08","objectID":"/%E5%8A%9B%E6%89%A3hot5_%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2/:0:0","tags":["力扣hot100"],"title":"力扣hot100_5_最长回文子串","uri":"/%E5%8A%9B%E6%89%A3hot5_%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2/"},{"categories":["力扣hot100"],"content":"题解和思路 //中心扩散法 从字符串的每一个index开始操作; 如果往左边字符串相同,则长度+1 直到没有重复 如果往右边字符串相同,则长度+1 直到没有重复 然后开始 向两边扩张,left的字母等于right 的字母,则长度+2; 进行一轮判断之后,如果长度比maxlen大,将left赋值给maxleft right赋值给maxright 重复 class Solution { public: string longestPalindrome(string s) { //使用中心扩散方法 int maxleft = 0; int maxright = 0; int maxlen = 0; int len = 1 ; for(int mid = 0; mid \u003c s.size() ;mid++){ int left = mid - 1; //重复字符串左边界 int right = mid + 1; //重复字符串右边界限 while( left \u003e= 0 \u0026\u0026 s[left] == s[mid]){ left --; len++; } while(right \u003c= s.size()-1\u0026\u0026s[right]==s[mid]){ right++; len++; } while(left \u003e=0 \u0026\u0026 right \u003c=s.size()-1 \u0026\u0026 s[left] == s[right]){ left -- ; right ++ ; len +=2; } if(len \u003e maxlen){ maxleft = left; maxright = right; maxlen = len; } len = 1; } return s.substr(maxleft + 1,maxlen); } }; ","date":"2023-03-08","objectID":"/%E5%8A%9B%E6%89%A3hot5_%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2/:1:0","tags":["力扣hot100"],"title":"力扣hot100_5_最长回文子串","uri":"/%E5%8A%9B%E6%89%A3hot5_%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2/"},{"categories":["力扣hot100"],"content":"无重复字符的最长子串 无重复字符的最长子串 题目: 给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度。 示例1 输入: s = \"abcabcbb\" 输出: 3 解释: 因为无重复字符的最长子串是 \"abc\",所以其长度为 3。 ","date":"2023-03-07","objectID":"/%E5%8A%9B%E6%89%A3hot3_%E6%97%A0%E9%87%8D%E5%A4%8D%E5%AD%97%E7%AC%A6%E7%9A%84%E6%9C%80%E9%95%BF%E5%AD%90%E4%B8%B2/:0:0","tags":["力扣hot100"],"title":"力扣hot100_3_无重复字符的最长子串","uri":"/%E5%8A%9B%E6%89%A3hot3_%E6%97%A0%E9%87%8D%E5%A4%8D%E5%AD%97%E7%AC%A6%E7%9A%84%E6%9C%80%E9%95%BF%E5%AD%90%E4%B8%B2/"},{"categories":["力扣hot100"],"content":"题解和思路 创建一个桶,其内的元素为128个,并将其置为0; 创建一个最大长度值maxvalue = 0; 创建一个指向最左边的变量head = 0; 循环访问字符串s内的每一个元素; head代表不重复字符串最左边的字符的下标,如果重复出现了a,则当前head的值为上一个v[a]内的值,而上一个v[a]内的值是上一个a的下标加1,这样可以让窗口往前移动。 最大值为已经计算的最大值,和 当前不重复字符串的最大值的对比。 class Solution { public: int lengthOfLongestSubstring(string s) { //判断是否为0 if(s.size()==0)return 0; // 滑动窗口 vector\u003cint\u003e v(128,0); int head = 0; int maxvalue = 0; for(int i =0;i\u003cs.size();i++){ head = max(head,v[s[i]]); v[s[i]] = i + 1 ; maxvalue = max(maxvalue , i - head + 1); } return maxvalue; } }; ","date":"2023-03-07","objectID":"/%E5%8A%9B%E6%89%A3hot3_%E6%97%A0%E9%87%8D%E5%A4%8D%E5%AD%97%E7%AC%A6%E7%9A%84%E6%9C%80%E9%95%BF%E5%AD%90%E4%B8%B2/:1:0","tags":["力扣hot100"],"title":"力扣hot100_3_无重复字符的最长子串","uri":"/%E5%8A%9B%E6%89%A3hot3_%E6%97%A0%E9%87%8D%E5%A4%8D%E5%AD%97%E7%AC%A6%E7%9A%84%E6%9C%80%E9%95%BF%E5%AD%90%E4%B8%B2/"},{"categories":["力扣hot100"],"content":"两数相加 两数相加 题目: 给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储 一位数字。 请你将两个数相加,并以相同形式返回一个表示和的链表 示例1 输入:l1 = [2,4,3], l2 = [5,6,4] 输出:[7,0,8] 解释:342 + 465 = 807. ","date":"2023-03-07","objectID":"/%E5%8A%9B%E6%89%A3hot2_%E4%B8%A4%E6%95%B0%E7%9B%B8%E5%8A%A0/:0:0","tags":["力扣hot100"],"title":"力扣hot100_2_两数相加","uri":"/%E5%8A%9B%E6%89%A3hot2_%E4%B8%A4%E6%95%B0%E7%9B%B8%E5%8A%A0/"},{"categories":["力扣hot100"],"content":"题解和思路 创建一个ListNode *head作为链表头 创建一个ListNode *tail作为链表尾部. 使用||符号,不去判断每个链表的节点是否为空,直到两个链表都到达链表最后。 创建新链表内的值 为两个链表内值相加 / 10 循环到最后的时候判断一下carry是否\u003e0 /** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode() : val(0), next(nullptr) {} * ListNode(int x) : val(x), next(nullptr) {} * ListNode(int x, ListNode *next) : val(x), next(next) {} * }; */ class Solution { public: ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { ListNode *head = new ListNode(0); ListNode *tail = head; int sum = 0; int carry = 0; while(l1 || l2){ sum += (l1!=NULL?l1-\u003eval:0); sum += (l2!=NULL?l2-\u003eval:0); sum += carry; tail-\u003enext = new ListNode( sum % 10); tail = tail-\u003enext; carry = sum / 10; sum = 0; l1 = l1-\u003enext; l2 = l2-\u003enext; } if(carry\u003e0){ tail-\u003enext = new ListNode( carry ); tail = tail-\u003enext ; } return head-\u003enext; } }; ","date":"2023-03-07","objectID":"/%E5%8A%9B%E6%89%A3hot2_%E4%B8%A4%E6%95%B0%E7%9B%B8%E5%8A%A0/:1:0","tags":["力扣hot100"],"title":"力扣hot100_2_两数相加","uri":"/%E5%8A%9B%E6%89%A3hot2_%E4%B8%A4%E6%95%B0%E7%9B%B8%E5%8A%A0/"},{"categories":["力扣hot100"],"content":"两数之和 两数之和 题目: 给定一个整数数组nums和一个整数目标值target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 示例1 输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 ","date":"2023-03-07","objectID":"/%E5%8A%9B%E6%89%A3hot1_%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C/:0:0","tags":["力扣hot100"],"title":"力扣hot100_1_两数之和","uri":"/%E5%8A%9B%E6%89%A3hot1_%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C/"},{"categories":["力扣hot100"],"content":"思路和题解 创建一个ans的vector容器 创建一个map的哈希表 遍历nums内的元素,并查找哈希表内target - nums[i]的值是否在哈希表内,不在则将键值对为key:nums[i],value:i 加入到哈希表中,如果target - nums[i]的值在哈希表内,则将当前值的index和目标值的index加入到ans中,并返回即可。 单次hash遍历的时间复杂度为O(n). class Solution { public: vector\u003cint\u003e twoSum(vector\u003cint\u003e\u0026 nums, int target) { vector\u003cint\u003e ans; map\u003cint,int\u003e map; ans.push_back(-1); ans.push_back(-1); for(int i = 0;i \u003c nums.size();i++){ if(map.count(target - nums[i])\u003e0){ ans[0] = (map[target - nums[i]]); ans[1] = i; return ans; } map[nums[i]] = i; } return ans; } }; ","date":"2023-03-07","objectID":"/%E5%8A%9B%E6%89%A3hot1_%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C/:1:0","tags":["力扣hot100"],"title":"力扣hot100_1_两数之和","uri":"/%E5%8A%9B%E6%89%A3hot1_%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C/"},{"categories":["Linux网络编程实战"],"content":"1.阻塞/非阻塞,同步/异步 典型的一次IO的两个阶段:数据的就绪和数据的读写 数据就绪:根据系统IO操作的就绪状态 1.阻塞 调用IO方法的线程进入阻塞状态,如果没数据,线程被挂起 2.非阻塞 不会改变线程的状态,通过返回值判断 -------------------------------------------- 数据读写:根据应用程序和内核的交互方式 1.同步 例如读数据,读取内核缓冲区数据,直到读取完,我们才知道有多少数据,才可以执行下面步骤 2.异步 例如读数据,读取内核缓冲区数据,我们不管读没读完,我们都可以执行下面的代码 一个典型的网络IO接口的调用,分为两个阶段,分别是数据就绪和数据读写。数据就绪阶段分为阻塞和非阻塞。表现的结果就是,阻塞当前线程或者直接返回。 同步表示A向B请求一个网络接口IO的时候(或者调用某个业务逻辑API接口时),数据的读写都是由请求方A自己来完成的(不管阻塞或者非阻塞);异步表示A向B请求调用一个网络接口IO时(或者调用某个业务API接口时),向B传入请求事件,以及事件发生时通知的方式,A就可以处理其他逻辑了,当B监听到事件处理完成之后,会用事先约定好的通知方式,通知A处理结果 ","date":"2023-03-06","objectID":"/%E9%98%BB%E5%A1%9E_%E9%9D%9E%E9%98%BB%E5%A1%9E/:0:0","tags":["Linux网络编程实战"],"title":"阻塞/阻塞_同步/异步","uri":"/%E9%98%BB%E5%A1%9E_%E9%9D%9E%E9%98%BB%E5%A1%9E/"},{"categories":["Linux网络编程"],"content":"UDP ","date":"2023-03-06","objectID":"/udp%E9%80%9A%E4%BF%A1/:0:0","tags":["Linux网络编程"],"title":"UDP","uri":"/udp%E9%80%9A%E4%BF%A1/"},{"categories":["Linux网络编程"],"content":"UDP通信 #include \u003csys/types.h\u003e #include \u003csys/socket.h\u003e ssize_t sendto(int sockfd,const void *buf,size_t len,int flags,const struct sockaddr *dest_addr,socklen_t addrlen); 参数: -sockfd:通信的fd -buf:需要发送的数据 -len:数组的大小 -flags:设置一些标志 -dest_addr:通信的另外一端的地址信息 -addrlen :地址的内存大小 返回值:。。。 ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr *src_addr,socklen_t *addrlen); 参数 -sockfd:通信的fd -buf:接受数据的数组 -len:数组的大小 -flags:0 -src_addr:接收方的地址指针 -addrlen:接收方地址的内存大小 ","date":"2023-03-06","objectID":"/udp%E9%80%9A%E4%BF%A1/:1:0","tags":["Linux网络编程"],"title":"UDP","uri":"/udp%E9%80%9A%E4%BF%A1/"},{"categories":["Linux网络编程"],"content":"广播 向子网中多台计算机发送消息,并且子网中所有计算机都可以接受到发送方发送的消息,每个广播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1 a.只能在局域网中使用 b.客户端要绑定服务器广播使用的端口,才可以接收到广播的消息。 //设置广播属性 int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen); sockfd:文件描述符 level:级别 SOL_SOCKET optname:SO_BROADCAST optval : 1表示允许广播 optlen : optval的大小 ","date":"2023-03-06","objectID":"/udp%E9%80%9A%E4%BF%A1/:2:0","tags":["Linux网络编程"],"title":"UDP","uri":"/udp%E9%80%9A%E4%BF%A1/"},{"categories":["Linux网络编程"],"content":"组播 单播地址标识单个IP接口,广播地址标识某个子网的所有IP接口,多播地址标识一组IP接口。单播是寻址方案的两个极端(要么单个要么全部),多播则实在两者之间提供一个折中的方案。多播数据报只应该由对它感兴趣的接口接受。也就是说由允许相应多播会话系统的主机上的接口接受。另外,广播一般局限于局域网使用,而多播既可以用于局域网,也可跨局域网使用。 a.既可以局域网,也可以跨局域网 b.客户端需要加入多播组,才能接收到多播的数据 ","date":"2023-03-06","objectID":"/udp%E9%80%9A%E4%BF%A1/:3:0","tags":["Linux网络编程"],"title":"UDP","uri":"/udp%E9%80%9A%E4%BF%A1/"},{"categories":["Linux网络编程"],"content":"组播地址 IP多播通信必须依赖于IP多播地址,在IPv4中它的范围224.0.0.0到239.255.255.255,并被划分为局部链接多播地址,预留多播地址和管理权限多播地址三类。 设置组播 int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen); //服务器设置多播信息,外出接口 sockfd:文件描述符 level:级别 IPPROTO_IP optname: optval : struct in_addr optlen : optval的大小 //客户端加入到多播组 -level :IPPROTO_IP -optname : IP_ADD_MEMBERSHIP -optval : struct ip_mreqn; struct ip_mreq{ //组播组的IP地址 struct in_addr_imr_multiaddr; //组播组的ip地址 //本地某一网路设备接口的IP地址 struct in_addr imr_address; int imr_ifindex; //网卡编号 } typedef uint32_t in_addr_t; struct in_addr{ } ","date":"2023-03-06","objectID":"/udp%E9%80%9A%E4%BF%A1/:3:1","tags":["Linux网络编程"],"title":"UDP","uri":"/udp%E9%80%9A%E4%BF%A1/"},{"categories":["Linux网络编程"],"content":"IO多路复用 I/O 多路复用使得程序能够同时监听多个文件描述符,能够提高程序的性能,Linux下实现I/O多路复用的系统调用主要有select ,poll和epoll ","date":"2023-03-05","objectID":"/io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/:0:0","tags":["Linux网络编程"],"title":"IO多路复用(select,poll,epoll)","uri":"/io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/"},{"categories":["Linux网络编程"],"content":"2.select 缺点: 1.每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。 2.同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多的时候也很大。 3.select支持的文件描述符数量太小了,默认是1024 4.fds集合不能重用,每次都需要重置。 /* 主旨思想: 1.要构造一个关于文件描述符的列表(将要监听的文件描述符) 2.调用系统函数 select,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行了I/O操作时,该函数才返回。 a:这个函数时阻塞的 b:函数对文件描述符的检测是由内核完成的 3.在返回时,它会告诉进程有多少(哪些)描述符进行I/O操作。 //sizeof(fd_set) = 128 ----\u003e1024bit */ #include \u003csys/time.h\u003e #include \u003csys/types.h\u003e #include \u003cunistd.h\u003e #includ \u003csys/select.h\u003e int select(int nfds,fd_set *readfds,fd_set *writefds, fd_set *exceptfds,struct timeval *timeout); -参数 -nfds:委托内核检测的最大文件描述符的值 + 1 -readfds:要检测的读的文件描述符的集合,委托内核检测哪些文件描述符的读的属性。 一般只检测读操作。 对应的是对方发送过来的数据,因为读是被动的接受数据,检测的是读缓冲区。 -writefds:要检测的文件描述符的写集合,委托内核检测哪些文件描述符的写的属性。 委托内核检测写缓冲区是不是还可以写数据(不满就可以写)。 -exceptfds:检测发生异常的文件操作描述符的集合。 -timeout:设置的超时时间 struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; -NULL:永久阻塞 -tv_sec=0,tv_usec =0; 不阻塞 -tv_sec\u003e0,tv_usec \u003e0; 阻塞对应的时间 -返回值: --1:失败 \u003e0:检测的集合中有n个文件描述符发生变化 //将参数文件描述符fd对应的标志位清零 void FD_CLR(int fd,fd_set *set); //判断fd对应的标志位是0还是1,返回值是fd对应的标志位的值 int FD_ISSET(int fd,fd_set *set); //将fd对应的标志位设为1 void FD_SET(int fd,fd_set *set); //清空整个标志位 void FD_ZERO(fd_set *set); ","date":"2023-03-05","objectID":"/io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/:1:0","tags":["Linux网络编程"],"title":"IO多路复用(select,poll,epoll)","uri":"/io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/"},{"categories":["Linux网络编程"],"content":"3.poll 1.移除了select的大小的限制 2.移除了fds这样的不可重用的限制。 #include \u003cpoll.h\u003e struct pollfd{ int fd; //委托内核检测的文件描述符 short events;//委托内核检测文件描述符的什么事件 short revents;//文件描述符实际发生的事件 }; int poll(struct pollfd *fds,nfds_t nfds,int timeout); 参数: -fds:结构体数组,需要检测的文件描述符集合 -nfs:这个是第一个参数数组中最后一个有效元素的下标 + 1 -timeout :阻塞时长 -1:永远阻塞 0:不阻塞 1:阻塞,当检测到需要检测的文件描述符有变化,就解除阻塞 返回值: -1:失败 \u003e0:成功 表示检测到集合中有n个文件描述符发生变化。 ","date":"2023-03-05","objectID":"/io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/:2:0","tags":["Linux网络编程"],"title":"IO多路复用(select,poll,epoll)","uri":"/io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/"},{"categories":["Linux网络编程"],"content":"epoll #include \u003csys/epoll.h\u003e //在内核中创建了一个新的实例,在这个数据中有两个重要的数据,一个数需要检测文件描述符信息的红黑树,还有一个就绪列表,存放检测到数据发生改变的文件描述符的信息(双向链表) int epoll_create(int size); -参数: size:目前乜有意义了,随便写一个就行。 -返回值 -1:失败 \u003e0:文件描述符,操作epoll实例的 typedef union epoll_data{ void *ptr; int fd; uint32_t u32; uint64_t u64; }epoll_data_t; struct epoll_event{ uint32_t events; //epoll event epoll_data_t data; //user data variable }; //对epoll实例进行管理:添加,删除,修改 int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event); 参数: epfd:epoll实例 op:进行什么操作 fd:需要操作的文件描述符 event:对该文件描述符需要检测的事件 //检测函数 int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout); 参数: epfd:epoll实例 events:传出参数,保存了发生变化的文件描述符的信息 maxevents:发生改变的事件的数量(第二个参数的大小) timeout:阻塞时间 0:不阻塞 -1:阻塞,直到检测到数据发生变化 \u003e0:阻塞时长 返回值: 成功:返回发生变化的文件描述符的个数 失败:返回-1 ","date":"2023-03-05","objectID":"/io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/:3:0","tags":["Linux网络编程"],"title":"IO多路复用(select,poll,epoll)","uri":"/io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/"},{"categories":["Linux网络编程"],"content":"Epoll的工作模式 LT模式(水平触发) 假设委托内核检测该事件 ---\u003e 检测fd缓冲区 读缓冲区有数据 ---\u003eepoll检测到会给用户通知 a.用户不读数据 数据一直在缓冲区,epoll一直通知 b.用户只读了一部分,epoll继续通知 c.缓冲区的数据读完了,不通知 LT(level-triggered)是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个描述符的fd进行I/O操作。如果你不作任何操作,内核还是会继续通知你。 ET模式(边沿触发) 假设委托内核检测该事件 ---\u003e 检测fd缓冲区 读缓冲区有数据 ---\u003eepoll检测到会给用户通知 a.用户不读数据,数据一直在缓冲区,epoll下一次检测就不通知了 b.用户只读了一部分数据,epoll不通知 c.缓冲区的数据读完了,不通知 ET(edge-triggered)是高速工作方式,只支持no-block scoket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,知道你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fd做I/O操作(从而导致它再次编程未就绪),内核不会发送更多的通知(only once)。 ET模式在很大程度上减少了epoll时间被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述的任务饿死。 ","date":"2023-03-05","objectID":"/io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/:3:1","tags":["Linux网络编程"],"title":"IO多路复用(select,poll,epoll)","uri":"/io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/"},{"categories":["Linux网络编程"],"content":"半关闭 当TCP连接中A向B发送FIN请求关闭,另一端B回应ACK之后(A端进入FIN_WAIT_2状态),并没有立即发送FIN给A,A方处于半连接状态(半开关),此时A可以接受B发送的数据,但是A已经不能再向B发送数据。 从程序的角度可以使用API控制实现半连接状态: #include \u003csys/socket.h\u003e int shutdown(int sockfd,int how); sockfd:需要关闭的sockfd描述符 how:允许为shutdown操作选择一下几种方式 SHUT_RD(0):关闭sockfd上的读功能,此选项不允许sockfd进行读操作。该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。 SHUT_WR(1):关闭sockfd的写功能,此选项不允许sockfd进行写操作,进程不能再对此套接字发出写操作。 SHUT_PDWR(2):关闭sockfd的读写功能、 使用close终止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。shutdown不考虑描述符的引用计数,直接关闭描述符。也可选择终止一个方向的连接。 注意: 1.如果有多个进程共享一个套接字,close没被调用一次,计数减1,直到计数为0时,也就是所有进程都调用了close,套接字被释放。 2.在多进程中如果一个进程调用了shutdown(sfd,SHUT_RDWR)后,其它的进程将无法进行通信。但如果一个进程close(sfd)将不会影响进程。 端口复用 端口复用的最常用的用途是: 1.防止服务器重启时之前绑定的端口还未释放。 2.程序突然退出而系统没有释放端口。 #include \u003csys/types.h\u003e #include \u003csys/socket.h\u003e //设置套接字的属性 int setsockopt(int sockfd,int level,int optname,const void*optval,socklen_t optlen); 参数 -sockfd:要操作的文件描述符 -level:级别 :SOL_SOCKET(端口复用级别) -optname : 选项的名 -SO_REUSEADDR -SO_REUSERPORT - optval: 端口复用的值(整型) -1:可以复用 -0:不可以复用 - optlen: optval参数的大小 端口复用,设置的时机是在服务器绑定端口之前、 查看网络相关信息的命令 netstat: 参数: -a 所有的socket -p 显示正在使用socket的程序的名称 -n 直接使用IP地址,而不通过域名服务器 -l 显示正在监听的socket ","date":"2023-03-05","objectID":"/%E5%8D%8A%E5%85%B3%E9%97%AD-%E7%AB%AF%E5%8F%A3%E5%A4%8D%E7%94%A8/:0:0","tags":["Linux网络编程"],"title":"端口复用","uri":"/%E5%8D%8A%E5%85%B3%E9%97%AD-%E7%AB%AF%E5%8F%A3%E5%A4%8D%E7%94%A8/"},{"categories":["Linux网络编程"],"content":"TCP通信流程 ! //TCP通信流程 //服务器端(被动接受的角色) 1.创建一个用于监听的套接字 -监听:监听有无客户端连接 -套接字:这个套接字其实是一个文件描述符 2.将这个监听文件描述符和本地的Ip和端口绑定(IP和端口就是服务器的地址信息); -客户端连接服务器的时候使用的就是这个ip和端口 3.设置监听,监听的fd开始工作 4.阻塞等待,当有客户端发起连接,接触阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd) 5.通信 - 接收数据 - 发送数据 6.通信结束,断开连接 //客户端 1.创建一个用于通信的套接字 2.连接服务器,需要指定的服务器的ip和端口 3.通信 -接收数据 -发送数据 4.通信结束,断开连接 socket编程有三种 1.流式套接字(SOCKET_STREAM) 2.数据报式套接字(SOCKET_DGRAM) 3,原始套接字(SOCKET_RAW) 前两者较常使用,基于TCP使用的是SOCKET_STREAM(流式套接字) 服务端 1.socket():创建流式socket int socket(int, int, int); 头文件: #include \u003csys/types.h\u003e #include \u003csys/socket.h\u003e 函数原型: int socket(int domain, int type, int protocol) domain: 协议类型,一般为AF_INET type: socket类型(SOCKET_STREAM,SOCKET_DGRAM,SOCKET_RAW) protocol:用来指定socket所使用的传输协议编号,通常设为0即可 2.bind():指定用于通信的IP地址和port端口 bind() 头文件: #include \u003csys/types.h\u003e #include \u003csys/socket.h\u003e 函数原型: int bind(int sockfd, struct sockaddr *my_addr, int addrlen) sockfd: socket描述符 my_addr:是一个指向包含有本机ip地址和端口号等信息的sockaddr类型的指针 addrlen:常被设为sizeof(struct sockaddr) 3.listen():把socket设为监听对象 3 listen() 头文件: #include \u003csys/socket.h\u003e 函数原型: int listen(int sockfd, int backlog); sockfd:socket()系统调用返回的socket描述符 backlog:指定在请求队列中的最大请求数,进入的连接请求将在队列中等待accept()它们。 4.accept():接受客户端发来的连接请求 4 accept() 头文件: #include \u003csys/types.h\u003e #inlcude \u003csys/socket.h\u003e 函数原型: int accept(int sockfd, void *addr, int addrlen) sockfd:是被监听的socket描述符 addr:通常是一个指向sockaddr_in变量的指针,该变量用来存放提出连接请求服务的主机的信息 addrlen:sizeof(struct sockaddr_in) 5.recv()/send():接受或者发送 send() 头文件: #include \u003csys/socket.h\u003e 函数原型: int send(int sockfd, const void *msg, int len, int flags); sockfd:用来传输数据的socket描述符 msg:要发送数据的指针 flags: 0 recv() 头文件: #include \u003csys/types.h\u003e #include \u003csys/socket.h\u003e 函数原型: int recv(int sockfd, void *buf, int len, unsigned int flags) sockfd:接收数据的socket描述符 buf:存放数据的缓冲区 len:缓冲的长度 flags:0 6.close():关闭socket连接 客户端 1.socket():创建流式套接字 2.connect():连接服务器,发起请求 3.send()/recv():接受或者发送 4.close():关闭socket连接 释放资源 ","date":"2023-03-04","objectID":"/socket%E9%80%9A%E4%BF%A1/:1:0","tags":["Linux网络编程"],"title":"Socket通信","uri":"/socket%E9%80%9A%E4%BF%A1/"},{"categories":["Linux网络编程"],"content":"备注 htons 头文件: #include \u003carpa/inet.h\u003e uint16_t htons(uint16_t hostshort); htons的功能: 将一个无符号短整型数值转换为网络字节序,即大端模式(big-endian) 参数u_short hostshort: 16位无符号整数 返回值: TCP / IP网络字节顺序. htonl() #include \u003carpa/inet.h\u003e uint32_t htonl(uint32_t hostlong); 简述:将主机的无符号长整形数转换成网络字节顺序。 hostlong:主机字节顺序表达的32位数。 注释: 本函数将一个32位数从主机字节顺序转换成网络字节顺序。 返回值: htonl()返回一个网络字节顺序的值。 使用gethostname(const char *name); 返回如下结构体 struct hostent { char *h_name; /* official name of host */ char **h_aliases; /* alias list */ int h_addrtype; /* host address type */ int h_length; /* length of address */ char **h_addr_list; /* list of addresses */ } hostent-\u003eh_name 表示的是主机的规范名。例如www.baidu.com的规范名其实是www.a.shifen.com。 hostent-\u003eh_aliases 表示的是主机的别名。www.baidu.com就是baidu他自己的别名。 hostent-\u003eh_addrtype 表示的是主机ip地址的类型。只会是ipv4(AF_INET), 这个函数处理不了ipv6 hostent-\u003eh_length 表示的是主机ip地址的长度。 hostent-\u003eh_addr_list 表示的是主机的ip地址。是网络字节序,需要通过inet_ntop函数转换。 ","date":"2023-03-04","objectID":"/socket%E9%80%9A%E4%BF%A1/:2:0","tags":["Linux网络编程"],"title":"Socket通信","uri":"/socket%E9%80%9A%E4%BF%A1/"},{"categories":["Linux网络编程"],"content":"代码样例 需求:客户端向服务器发送消息,服务器回传该消息给客户端。 服务器部分: #include \u003cstdio.h\u003e #include \u003cstdlib.h\u003e #include \u003carpa/inet.h\u003e #include \u003csys/socket.h\u003e #include \u003csys/types.h\u003e #include \u003cunistd.h\u003e #include \u003cstring.h\u003e //服务端 int main(){ int socket_fd,connect_fd; struct sockaddr_in saddr,caddr; //创建套接字 if((socket_fd=socket(AF_INET,SOCK_STREAM,0))==-1){ perror(\"socket\"); exit(0); } //定义服务器地址 saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = htonl(INADDR_ANY); saddr.sin_port = htons(8000); if((bind(socket_fd,(struct sockaddr *)\u0026saddr,sizeof(saddr)))==-1){ perror(\"bind\"); exit(0); } //监听 if((listen(socket_fd,10))==-1){ perror(\"listen\"); exit(0); } //阻塞接受 socklen_t clen = sizeof(caddr); if((connect_fd=accept(socket_fd,(struct sockaddr * )\u0026caddr,\u0026clen))==-1){ perror(\"accept\"); exit(0); } //客户端信息 char cip[16];//客户端ip inet_ntop(AF_INET,\u0026caddr.sin_addr.s_addr,cip,sizeof(cip)); unsigned int cport = ntohs(caddr.sin_port);//客户端端口 //接受消息 char recvbuf[1024]={0}; while(1){ // recvbuf int num = read(connect_fd, recvbuf, sizeof(recvbuf)); if(num == -1) { perror(\"read\"); exit(-1); } else if(num \u003e 0) { printf(\"我接受到了客户端的数据 : %s\\n\", recvbuf); } else if(num == 0) { // 表示客户端断开连接 printf(\"clinet closed...\\n\"); break; } char * data = recvbuf; // 给客户端发送数据(实现回射服务器) write(connect_fd, data, strlen(data)); } //关闭文件描述符 close(connect_fd); close(socket_fd); return 0; } 客户端部分 #include \u003cstdio.h\u003e #include \u003cstdlib.h\u003e #include \u003carpa/inet.h\u003e #include \u003csys/socket.h\u003e #include \u003csys/types.h\u003e #include \u003cunistd.h\u003e #include \u003cstring.h\u003e int main(){ int socket_fd; struct sockaddr_in saddr; //描述符 if((socket_fd = socket(AF_INET,SOCK_STREAM,0))==-1){ perror(\"socket\"); exit(0); } //设置服务器信息 saddr.sin_family=AF_INET; inet_pton(AF_INET,\"127.0.0.1\",\u0026saddr.sin_addr.s_addr); saddr.sin_port=htons(8000); //连接 int ret = connect(socket_fd, (struct sockaddr *)\u0026saddr, sizeof(saddr)); if(ret == -1) { perror(\"connect\"); exit(-1); } //发送消息 char recvbuf[1024]={0}; while(1){ // 从键盘输入,给客户端发送数据 char data[1024]; memset(data,0,sizeof data); printf(\"请输入发送数据:\"); scanf(\"%s\", data); write(socket_fd, data , strlen(data)); sleep(1); int len = read(socket_fd, recvbuf, sizeof(recvbuf)); if(len == -1) { perror(\"read\"); exit(-1); } else if(len \u003e 0) { printf(\"我接受到了回射服务器的返回的数据 : %s\\n\", recvbuf); } else if(len == 0) { // 表示服务器端断开连接 printf(\"server closed...\\n\"); break; } } close(socket_fd); return 0; } ","date":"2023-03-04","objectID":"/socket%E9%80%9A%E4%BF%A1/:2:1","tags":["Linux网络编程"],"title":"Socket通信","uri":"/socket%E9%80%9A%E4%BF%A1/"},{"categories":["Linux网络编程"],"content":"socket地址 ","date":"2023-03-04","objectID":"/socket%E5%9C%B0%E5%9D%80/:0:0","tags":["Linux网络编程"],"title":"Socket地址","uri":"/socket%E5%9C%B0%E5%9D%80/"},{"categories":["Linux网络编程"],"content":"通用socket地址 socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下: #include \u003cbits/socket.h\u003e struct sockaddr { sa_family_t sa_family; char sa_data[14]; }; typedef unsigned short int sa_family_t; sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应,常见的协议族(protocol family,也成domain)和对应的地址族如下所示: 协议族 地址族 描述 PF_UNIX AF_UNIX UNIX本地域协议族 PF_INET AF_INET TCP/IPv4协议族 PF_INET6 AF_INET6 TCP/IPv6协议族 宏PF_*和AF_*都定义在bit/socket.h头文件中,且后者与前者有完全相同的值,所有二者通常混用。sa_data成员用于存放socket地址值.但是,不同的协议族的地址值具有不同的含义和长度,如下表示: 协议族 地址值含义和长度 PF_UNIX 文件的路径名,长度可达到108字节 PF_INET 16bit端口号和32bitIPv4地址,共6字节 PF_INET6 16bit端口号,32bit流标识,128bitIPv6地址,32bit范围ID,共26字节 由上表可知,14字节的sa_data根本无法容纳多数协议族的地址值。因此,Linux定义了下面这个新的通用socket地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对象的。 #include \u003cbits/socket.h\u003e struct sockadr_storage{ sa_family_t sa_family; unsigned long int __sa_align; char __ss_padding[128 - sizeof(__sa_align)]; } typedef unsigned short int sa_family_t; ","date":"2023-03-04","objectID":"/socket%E5%9C%B0%E5%9D%80/:1:0","tags":["Linux网络编程"],"title":"Socket地址","uri":"/socket%E5%9C%B0%E5%9D%80/"},{"categories":["Linux网络编程"],"content":"专用socket地址 很多网络编程函数诞生早于IPv4协议,那时候都使用的是struct sockaddr结构体,为了向前兼容,现在的sockaddr退化成(void *)的作用,传递一个地址给函数,至于这个函数时sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转换为所需的地址类型。 UNIX本地域协议族使用如下专用的socket地址结构体 #include \u003csys/un.h\u003e struct sockaddr_un{ sa_family_t sin_family; char sun_path[108]; } TCP/IP协议族由sockaddr_in和sockaddr_in6两个专用的socket地址结构体,他们分别用于IPv4和Ipv6 #include \u003cnetinet/in.h\u003e struct sockaddr_in{ sa_family_t sa_family; in_port_t sin_port; struct in_addr sin_addr; unsigned char sin_zero[sizeof(struct sockaddr)-__SOCKADDR_COMMON_SIZE-sizeof(in_port_t)-sizeof(struct in_addr)]; }; struct in_addr{ in_addr_t s_addr; }; struct sockaddr_in6{ sa_family_t sa_family; in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr; uint32_t sin6_scope_id; }; typedef unsigned short uint16_t; typedef unsigned int uint32_t; typedef uint16_t in_port_t; typedef uint32_t in_addr_t; #define __SOCKADDR_COMMON_SIZE (sizeof(unsigned short int)) 所有专用socket地址(以及sockaddr_storage)类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数类型都是sockaddr。 IP地址转换 通常,人们习惯用可读性好的字符串来表示ip地址,比如用点分十进制字符串表示IPv4地址,以及用十六进制字符串表示IPv6地址。但编程中我们需要先把它们转化成整数(二进制)方能使用。而记录日志时则相反,我们要把整数表示的IP地址转化成为可读的字符串。下面3个函数可用于用点分是兼职字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换: #includ \u003carpa/inet.h\u003e in_addr_t inet_addr(const char *cp); int inet_aton(const char *cp,struct in_addr *inp); char *inet_ntoa(struct in_addr in); 下面这对更新的函数也能完成前面3个函数同样的功能,并且它们同时适用IPv4地址和IPv6地址 #includ \u003carpa/inet.h\u003e //p : 点分十进制的字符串 //n : 表示network字节序的整数 int inet_pton(int af,const char *src,void *dest); //af:地址族 //src :需要转换的点分十进制字符串 //dest:转换后保存的目标 const char *inet_ntop(int af,const void *src,char *dest,socklen_t size); //af:地址族 //src :需要转换的Ip的整数 //dest:转换后保存的目标 //size : 第三个参数的大小 //返回值:返回转换后的字符串 ","date":"2023-03-04","objectID":"/socket%E5%9C%B0%E5%9D%80/:2:0","tags":["Linux网络编程"],"title":"Socket地址","uri":"/socket%E5%9C%B0%E5%9D%80/"},{"categories":["Linux网络编程"],"content":"字节序 现代CPU的累加器一次都能装载至少4字节(32位机器),即一个整数。那么这四个字节在内存中排列的顺序将影响它被累加器装载成的整数的值,这就是字节序问题。在各种计算机体系结构中,对于字节,字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特,字节,字,双字节等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。 字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。 字节序分为大端字节序(Big-Ending),和小端字节(Little-Ending)。大端字节序是指一个整数的最高位字节(23~31bit)存储在内存的低地址处,低位字节(0-7bit)存储在内存的高地址处;小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。 ","date":"2023-03-04","objectID":"/%E5%AD%97%E8%8A%82%E5%BA%8F/:0:0","tags":["Linux网络编程"],"title":"字节序","uri":"/%E5%AD%97%E8%8A%82%E5%BA%8F/"},{"categories":["Linux网络编程"],"content":"字节序举例 ","date":"2023-03-04","objectID":"/%E5%AD%97%E8%8A%82%E5%BA%8F/:1:0","tags":["Linux网络编程"],"title":"字节序","uri":"/%E5%AD%97%E8%8A%82%E5%BA%8F/"},{"categories":["Linux网络编程"],"content":"小端字节序 小端字节序:最高有效位存于最高内存地址,最低有效位存于最低内存处 内存方向—–\u003e 内存低——\u003e内存高 ","date":"2023-03-04","objectID":"/%E5%AD%97%E8%8A%82%E5%BA%8F/:1:1","tags":["Linux网络编程"],"title":"字节序","uri":"/%E5%AD%97%E8%8A%82%E5%BA%8F/"},{"categories":["Linux网络编程"],"content":"大端字节序 大端字节序:最高有效位存于最低内存地址处,最低有效位存于最高内存处 内存方向—–\u003e 内存低——\u003e内存高 ","date":"2023-03-04","objectID":"/%E5%AD%97%E8%8A%82%E5%BA%8F/:1:2","tags":["Linux网络编程"],"title":"字节序","uri":"/%E5%AD%97%E8%8A%82%E5%BA%8F/"},{"categories":["Linux网络编程"],"content":"整体视图 ","date":"2023-03-04","objectID":"/%E5%AD%97%E8%8A%82%E5%BA%8F/:1:3","tags":["Linux网络编程"],"title":"字节序","uri":"/%E5%AD%97%E8%8A%82%E5%BA%8F/"},{"categories":["Linux网络编程"],"content":"字节序转换函数 当格式化的数据在两台不同字节序的主机之间直接传递时,接收端必然错误的解释。解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换) 网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型,操作系统等无关,从而可以保证数据在不同主机之间传输是能够被正确解释,网络字节顺序采用大端排序方式。 BSD Socket_t提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons,htonl:从网络字节序到主机字节序的转换函数:ntohs,ntohl。 h - host :主机,主机字节序 to - 转换成什么 n - network:网络字节序 s - short:unsigned short l - long: unsigned int #include \u003carpa/inet.h\u003e //转换端口 uint_16_t htons(uint_16_t hostshort);//主机字节序到网络字节序 uint_16_t ntohs(uint_16_t netshort);//网络字节序到主机字节序 //转换ip uint_16_t htonl(uint_16_t hostlong);//转ip uint_16_t ntohl(uint_16_t netlong); ","date":"2023-03-04","objectID":"/%E5%AD%97%E8%8A%82%E5%BA%8F/:2:0","tags":["Linux网络编程"],"title":"字节序","uri":"/%E5%AD%97%E8%8A%82%E5%BA%8F/"},{"categories":["Linux多线程开发"],"content":"线程同步 1.线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的,必须确保多个线程不会同时修改同一个变量,或者某个线程不会读取正在由其他线程修改的变量。 2.临界区是指访问某一种共享资源的代码片段,并且这段代码的执行应为原子操作,也就是同时访问同一共享资源的其他线程不应该终端该片段的执行, 3.线程同步:即当有一个线程在对内存进程操作时,其他线程不可以对这个内存地址进行操作,知道该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。 ","date":"2023-03-03","objectID":"/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5/:1:0","tags":["Linux多线程开发"],"title":"线程同步+锁","uri":"/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5/"},{"categories":["Linux多线程开发"],"content":"互斥锁 1.为了避免线程更新共享变量时出现问题,可以使用互斥量(mutex是mutual exclusion的缩写)来确保同时仅用一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问。 2.互斥量有两种状态:已锁定(locked)和未锁定(unlocked),任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。 3.一旦线程锁定互斥量,随即成文该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一个共享资源(可能由多个相关变量组成)会使用不同的互斥量。每一个线程在访问同一资源时将采用如下协议: 3.1 针对共享资源锁定互斥量 3.2 访问共享资源 3.3 对互斥量解锁 ","date":"2023-03-03","objectID":"/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5/:1:1","tags":["Linux多线程开发"],"title":"线程同步+锁","uri":"/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5/"},{"categories":["Linux多线程开发"],"content":"互斥量 如果多个线程试图执行这一块代码(一个临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域,如下图所示。 !(mutex)(../images/thread/1.png) //互斥量的类型 pthread_mutex_t int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutex_t *restrict mutex); int pthread_mutex_destroy(pthread_mutex_t *mutex); int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthraed_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); 示例代码 /* //互斥量的类型 pthread_mutex_t int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutex_t *restrict attr); -初始化互斥量 -参数 -mutex:需要初始化的互斥量变量 -attr:互斥量相关的属性,NULL 或者自己写入 - restrict:c语言修饰符,被修饰的指针,不能由另外的一个指针进行操作 int pthread_mutex_destroy(pthread_mutex_t *mutex); -释放互斥量 int pthread_mutex_lock(pthread_mutex_t *mutex); -加锁 阻塞的 如果有一个线程加锁了,其他的线程只能阻塞等待 int pthread_mutex_trylock(pthraed_mutex_t *mutex); -尝试加锁 int pthread_mutex_unlock(pthread_mutex_t *mutex); -解锁 */ #include \u003cpthread.h\u003e #include \u003cstdio.h\u003e #include \u003cstring.h\u003e #include \u003cunistd.h\u003e int ticket = 1000; //创建一个互斥量 pthread_mutex_t mutex; void * callback(void * arg){ //卖票 while(1){ pthread_mutex_lock(\u0026mutex); if(ticket\u003e0){ printf(\"thread id = %ld,正在卖第%d张票\\n\",pthread_self(),ticket); ticket--; }else{ pthread_mutex_unlock(\u0026mutex); break; } pthread_mutex_unlock(\u0026mutex); } return NULL; } int main(){ //初始化互斥量 pthread_mutex_init(\u0026mutex,NULL); //创建一个线程 pthread_t tid1; pthread_t tid2; pthread_t tid3; //创建 pthread_create(\u0026tid1,NULL,callback,NULL); pthread_create(\u0026tid2,NULL,callback,NULL); pthread_create(\u0026tid3,NULL,callback,NULL); //回收 pthread_join(tid1,NULL); pthread_join(tid2,NULL); pthread_join(tid3,NULL); // //分离 // pthread_detach(tid1); // pthread_detach(tid2); // pthread_detach(tid3); //主线程退出 pthread_exit(NULL); //释放互斥量 pthread_mutex_destroy(\u0026mutex); return 0; } ","date":"2023-03-03","objectID":"/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5/:1:2","tags":["Linux多线程开发"],"title":"线程同步+锁","uri":"/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5/"},{"categories":["Linux多线程开发"],"content":"死锁 1.有时候,一个线程需要用时访问两个或者更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。 2.两个或两个以上的进程在执行过程中,因争夺共享资源而造成一种互相等待的线程,若无外力作用,它们都无法推进下去。此时称系统处于死锁状态或者处于死锁。 3.死锁的集中场景: 3.1 忘记释放锁 3.2 重复加锁 3.3 多线程多锁,抢占锁资源 ","date":"2023-03-03","objectID":"/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5/:1:3","tags":["Linux多线程开发"],"title":"线程同步+锁","uri":"/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5/"},{"categories":["Linux多线程开发"],"content":"读写锁 1.当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排他性,所有其它线程都无法访问数据锁,也就是无法访问共享资源,但是实际上多个线程同时访问共享资源也不会导致问题。 2.在对数据的读写操作时,更多的是读操作,写操作较少,例如对数据库数据的读写和应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。 3.读写锁的特点: 3.1 如果有其它线程读数据,则允许其它线程执行读操作,但是不允许写操作。 3.2 如果有其它线程写数据,则其他线程不允许读,写操作。 3.3 写是独占,写的优先级高 读写锁相关操作函数 //读写锁的类型 pthread_rwlock_t int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr); int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); int phtread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_trydlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); ","date":"2023-03-03","objectID":"/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5/:1:4","tags":["Linux多线程开发"],"title":"线程同步+锁","uri":"/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5/"},{"categories":["Linux多线程开发"],"content":"生产者消费者模型 生产者消费者模型,即多个生产者生产,多个消费者消费,共同操作一个变量,如果不加锁,则会导致内存越界,报错。但是加了锁,由于没有提醒机制,某个线程会单方面的阻塞,也不利于流程的实施。 /* 生产者消费者模型 */ #include \u003cpthread.h\u003e #include \u003cstdio.h\u003e #include \u003cunistd.h\u003e #include \u003cstdlib.h\u003e struct Node { int num; struct Node * next; }; //头结点 struct Node * head = NULL; //定义互斥锁 pthread_mutex_t mutex; void * producer(void *arg){ while(1){ pthread_mutex_lock(\u0026mutex); struct Node * newNode = (struct Node *)malloc(sizeof(struct Node)); newNode-\u003enext = head; head = newNode; newNode-\u003enum = rand()%1000; printf(\"add node,num : %d,tid : %ld \\n\",newNode-\u003enum,pthread_self()); pthread_mutex_unlock(\u0026mutex); usleep(100); } return NULL; } void * customer(void *arg){ while(1){ pthread_mutex_lock(\u0026mutex); struct Node * temp = head; if(head!=NULL){ head = head-\u003enext; printf(\"del node num : %d, tid : %ld\\n\",temp-\u003enum,pthread_self()); temp-\u003enext = NULL; free(temp); pthread_mutex_unlock(\u0026mutex); usleep(100); }else{ pthread_mutex_unlock(\u0026mutex); } } return NULL; } int main(){ //创建互斥锁 pthread_mutex_init(\u0026mutex,NULL); //创建5个生产,5个消费 pthread_t ptids[5]; pthread_t ctids[5]; for(int i=0;i\u003c5;i++){ pthread_create(\u0026ptids[i],NULL,producer,NULL); } for(int i=0;i\u003c5;i++){ pthread_create(\u0026ctids[i],NULL,customer,NULL); } //线程连接 for(int i=0;i\u003c5;i++){ pthread_join(ptids[i],NULL); pthread_join(ctids[i],NULL); } // while(1){ // sleep(10); // } // //消除互斥锁 pthread_mutex_destroy(\u0026mutex); pthread_exit(NULL); return 0; } 条件变量 使用条件变量可以根据线程内的运行,适当的阻塞线程 或者唤醒线程 //条件变量的类型 pthread_cond_t int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); int pthread_cond_destroy(pthread_cond_t *cond); int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime); int pathread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthead_cond_t *cond); 代码示例 /* //条件变量的类型 pthread_cond_t int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); 初始化 int pthread_cond_destroy(pthread_cond_t *cond); 释放 int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); 等待唤醒 int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime); 等待,依据传入的时间 设置阻塞时间 int pathread_cond_signal(pthread_cond_t *cond); 唤醒一个或者多个等待线程 int pthread_cond_broadcast(pthead_cond_t *cond); 唤醒全部线程 */ /* 生产者消费者模型 */ #include \u003cpthread.h\u003e #include \u003cstdio.h\u003e #include \u003cunistd.h\u003e #include \u003cstdlib.h\u003e struct Node { int num; struct Node * next; }; //头结点 struct Node * head = NULL; //定义互斥锁 pthread_mutex_t mutex; //创建条件变量 pthread_cond_t cond; void * producer(void *arg){ while(1){ pthread_mutex_lock(\u0026mutex); struct Node * newNode = (struct Node *)malloc(sizeof(struct Node)); newNode-\u003enext = head; head = newNode; newNode-\u003enum = rand()%1000; printf(\"add node,num : %d,tid : %ld \\n\",newNode-\u003enum,pthread_self()); //只要生产了一个就通知消费者 pthread_cond_signal(\u0026cond); pthread_mutex_unlock(\u0026mutex); usleep(100); } return NULL; } void * customer(void *arg){ while(1){ pthread_mutex_lock(\u0026mutex); struct Node * temp = head; if(head!=NULL){ head = head-\u003enext; printf(\"del node num : %d, tid : %ld\\n\",temp-\u003enum,pthread_self()); temp-\u003enext = NULL; free(temp); pthread_mutex_unlock(\u0026mutex); usleep(100); }else{ //释放 pthread_mutex_unlock(\u0026mutex); //没有数据等待 //当这个函数调用阻塞的时候,会释放锁,生产者开始生成 //但不阻塞的时候,继续向下执行,不阻塞 pthread_cond_wait(\u0026cond,\u0026mutex); } } return NULL; } int main(){ //创建互斥锁 pthread_mutex_init(\u0026mutex,NULL); //初始化cond pthreaD_cond_init(\u0026cond,NULL); //创建5个生产,5个消费 pthread_t ptids[5]; pthread_t ctids[5]; for(int i=0;i\u003c5;i++){ pthread_create(\u0026ptids[i],NULL,producer,NULL); } for(int i=0;i\u003c5;i++){ pthread_create(\u0026ctids[i],NULL,customer,NULL); } //线程连接 for(int i=0;i\u003c5;i++){ pthread_join(ptids[i],NULL); pthread_join(ctids[i],NULL); } // while(1){ // sleep(10); // } // //消除互斥锁 pthread_mutex_destroy(\u0026mutex); //消除条件变量 pthread_cond_destroy(\u0026cond); pthread_exit(NULL);","date":"2023-03-03","objectID":"/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5/:2:0","tags":["Linux多线程开发"],"title":"线程同步+锁","uri":"/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5/"},{"categories":["Linux多线程开发"],"content":"线程概述 1.与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序,且共享同一份内存区域,其中包括初始化数据段,未初始化数据段,以及堆内存段。(传统意义上的UNIX进程只是多线程程序的一个特例,该进程只包含一个线程)。 2.进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位。 3.线程是轻量级的进程(LWP:Light Weight Process),在Linux环境下线程的本质是进程。 4.查看指定进程的LWP号:ps -Lf pid ","date":"2023-03-02","objectID":"/%E7%BA%BF%E7%A8%8B/:1:0","tags":["Linux多线程开发"],"title":"线程","uri":"/%E7%BA%BF%E7%A8%8B/"},{"categories":["Linux多线程开发"],"content":"线程和进程的区别 1.进程间的信息难以共享,由于除去只读代码之外,父子进程并未共享内存,因此必须采用一些进程间通信的方式,在进程间进行信息交换。 2.调用fork()来创建进程的代价相对较高,即便利用写时复制技术,但仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着fork()调用在时间上的开销依然不菲。 3.线程之间能够方便,快速的共享信息。只需要将数据复制到共享(全局或堆)变量中即可。 4.创建线程比创建进程通常要快10倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表。 ","date":"2023-03-02","objectID":"/%E7%BA%BF%E7%A8%8B/:1:1","tags":["Linux多线程开发"],"title":"线程","uri":"/%E7%BA%BF%E7%A8%8B/"},{"categories":["Linux多线程开发"],"content":"线程之间共享和非共享资源 共享资源 进程ID和父进程ID 进程组ID和会话ID 用户ID和用户组ID 文件描述符 信号处理 文件系统的相关信息:文件权限掩码,当前工作目录 虚拟地址空间(除栈,text) 非共享资源 线程ID 信号掩码 线程特有数据 error变量 实时调度策略和优先级 栈,本地变量和函数的调用链接信息 ","date":"2023-03-02","objectID":"/%E7%BA%BF%E7%A8%8B/:1:2","tags":["Linux多线程开发"],"title":"线程","uri":"/%E7%BA%BF%E7%A8%8B/"},{"categories":["Linux多线程开发"],"content":"线程的创建 线程操作函数 pthread_t pthread_self(void); int pthread_equal(pthread_t t1,pthread_t t2); int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void *),void *arg); void pthread_exit(void *retval); int pthread_join(pthread_t thread,void **retval); int pthread_detach(pthread_t thread); int pthread_cancel(pthread_t thread); int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void *),void *arg); /* 一般情况下,main函数所在的线程称之为主线程,之后创建的称之为子线程 #include \u003cpthread.h\u003e int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); 作用:创建一个子线程 参数: pthread_t *thread:传出参数,代表的是线程创建成功之后,子线程的线程ID会写到这个参数里 const pthread_attr_t *attr:需要设置的线程的属性,一般使用默认值,NULL void *(*start_routine) (void *):函数指针,子线程需要处理的逻辑代码 void *arg:给第三个参数使用,传参 返回值: 成功:0 错误:失败会返回错误号码,这个错误号和之前的errno不太一样 不能通过perror(),而是使用char *strerror(int errornum); */ #include \u003cstdio.h\u003e #include \u003cstdlib.h\u003e #include \u003cpthread.h\u003e #include \u003cstring.h\u003e #include \u003cunistd.h\u003e //子线程需要处理的逻辑代码 void *callback(){ printf(\"chil thread。。。。\\n\"); return NULL; } int main(){ //创建一个线程 pthread_t tid; int ret = pthread_create(\u0026tid,NULL,callback(),NULL); if(ret !=0){ char *str = strerror(ret); printf(\"error : %s\\n\",str); } for(int i = 0;i \u003c 5;i++){ printf(\"%d\\n\",i); } sleep(1); return 0; } void pthread_exit(void *retval); /* #include \u003cpthread.h\u003e void pthread_exit(void *retval); 作用L:终止一个线程,在哪个线程调用,就终止哪个线程 参数: -retval:需要传递一个指针,作为一个返回值,可以再pthread_join()中获取到 pthread_t pthread_self(void); 作用:获取当前线程的ID */ #include \u003cstdio.h\u003e #include \u003cstring.h\u003e #include \u003cpthread.h\u003e #include \u003cunistd.h\u003e #include \u003cstdlib.h\u003e void *callback(){ printf(\"child thread id:%ld\\n\",pthread_self()); return NULL; } int main(){ //创建一个子线程 pthread_t tid; int ret = pthread_create(\u0026tid,NULL,callback,NULL); if(ret!=0){ char *str = strerror(ret); printf(\"%s\\n\",str); exit(0); } //主线程 for(int i = 0;i \u003c 5;i++){ printf(\"%d\\n\",i); } printf(\"tid : %ld ,main thread id:%ld\\n\",tid,pthread_self()); //让主线程退出,当主线程退出时,不会影响其他正常运行的线程 pthread_exit(NULL); return 0; } int pthread_join(pthread_t thread,void **retval); /* #include \u003cpthread.h\u003e int pthread_join(pthread_t thread, void **retval); 作用:和一个终止的线程进行连接 回收子线程的资源 这个函数是阻塞函数,调用一次只能回收一个子线程 一般在主线程中去使用 参数: -thread:需要回收的子线程的id -retval: 接受子线程退出时的返回值 返回值: 成功返回0 失败返回错误号 */ #include \u003cstdio.h\u003e #include \u003cstring.h\u003e #include \u003cpthread.h\u003e #include \u003cunistd.h\u003e #include \u003cstdlib.h\u003e void *callback(){ printf(\"child thread id:%ld\\n\",pthread_self()); int num = 10; pthread_exit((void *)\u0026num); } int main(){ //创建一个子线程 pthread_t tid; int ret = pthread_create(\u0026tid,NULL,callback,NULL); if(ret!=0){ char *str = strerror(ret); printf(\"%s\\n\",str); exit(0); } //主线程 for(int i = 0;i \u003c 5;i++){ printf(\"%d\\n\",i); } printf(\"tid : %ld ,main thread id:%ld\\n\",tid,pthread_self()); //主线程调用pthread_join去回收子线程的资源 int *thread_return_value = NULL; ret = pthread_join(tid,(void **)\u0026thread_return_value); if(ret!=0){ char *str = strerror(ret); printf(\"%s\\n\",str); exit(0); } printf(\"exit date : %d\\n\",*thread_return_value); printf(\"回收子线程资源成功\\n\"); //让主线程退出,当主线程退出时,不会影响其他正常运行的线程 pthread_exit(NULL); return 0; } int pthread_detach(pthread_t thread); /* #include \u003cpthread.h\u003e int pthread_detach(pthread_t thread); 作用:分离一个线程 参数: -pthread_t thread:传入一个线程ID,将指定ID的线程标记为分离 返回值: 成功返回 0 错误返回 error number 注意: 1.不可多次分离,否则会产生不可预料的行为。 2.不可以join一个已经detach的线程 */ #include \u003cpthread.h\u003e #include \u003cstdio.h\u003e #include \u003cstdlib.h\u003e #include \u003cunistd.h\u003e #include \u003cstring.h\u003e void * callback(void * arg){ printf(\"child thread id : %ld\\n\",pthread_self()); return NULL; } int main(){ //创建一个线程 pthread_t tid; int ret = pthread_create(\u0026tid,NULL,callback,NULL); if(ret != 0){ char * str = strerror(ret); printf(\"error1 :%s\\n\",str); exit(0); } // 输出主线程和子线程的id printf(\"tid = %ld,main thread id = %ld\\n\",tid,pthread_self()); //设置子线程分离,子线程结束时对应的资源就不需要主线程释放 ret = pthread_detach(tid); if(ret != 0){ char * str = strerror(ret); printf(\"error2 :%s\\n\",str); exit(0); } //设置分离后,对分离的子线程进程链接,pthread_join()//Invalid argument ret = pthread_join(tid","date":"2023-03-02","objectID":"/%E7%BA%BF%E7%A8%8B/:2:0","tags":["Linux多线程开发"],"title":"线程","uri":"/%E7%BA%BF%E7%A8%8B/"},{"categories":["Linux多线程开发"],"content":"线程属性操作函数 int pthread_attr_init(pthread_addr_t *attr); -初始化线程属性变量 int pthread_attr_destory(pthread_addr_t *addr); -释放线程资源 int pthread_attr_getdetachstate(const pthread_attr_t *attr,int *detachstate); -获取线程分离的状态属性 int pthread_attr_setdetachstate(pthread_attr_t *addr,int detachstate); -设置线程分离的状态属性 在终端输入 man pthread_attr + Tab 可以查看其余函数 ","date":"2023-03-02","objectID":"/%E7%BA%BF%E7%A8%8B/:2:1","tags":["Linux多线程开发"],"title":"线程","uri":"/%E7%BA%BF%E7%A8%8B/"},{"categories":["Linux多进程开发"],"content":"共享内存 1.共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分。因此这种IPC机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。 2.与管道等,要求进程发送数据,并将数据从用户空间的缓冲区复制进内核内存以及接受进程讲数据从内核内存复制进用户空间的缓冲区的做法对比,这种IPC技术的速度更快。 ","date":"2023-03-02","objectID":"/%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98/:1:0","tags":["Linux多进程开发"],"title":"共享内存","uri":"/%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98/"},{"categories":["Linux多进程开发"],"content":"共享内存的使用步骤 1.调用shmget(),创建一个新共享内存段或取得一个既有共享内存的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要额共享内存标识符。 2.使用shmat()来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分 3.此刻在进程中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由shmat()调用后返回的addr值,他是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。 4.调用shmdt()来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了,这步是可选额,并且在进程终止时会自动完成这一步。 5.调用shmctl()来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。 ","date":"2023-03-02","objectID":"/%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98/:2:0","tags":["Linux多进程开发"],"title":"共享内存","uri":"/%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98/"},{"categories":["Linux多进程开发"],"content":"贡献内存相关函数 共享内存相关的函数 #include \u003csys/ipc.h\u003e #include \u003csys/shm.h\u003e int shmget(key_t key, size_t size, int shmflg); - 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。 新创建的内存段中的数据都会被初始化为0 - 参数: - key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。 一般使用16进制表示,非0值 - size: 共享内存的大小 - shmflg: 属性 - 访问权限 - 附加属性:创建/判断共享内存是不是存在 - 创建:IPC_CREAT - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用 IPC_CREAT | IPC_EXCL | 0664 - 返回值: 失败:-1 并设置错误号 成功:\u003e0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。 void *shmat(int shmid, const void *shmaddr, int shmflg); - 功能:和当前的进程进行关联 - 参数: - shmid : 共享内存的标识(ID),由shmget返回值获取 - shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定 - shmflg : 对共享内存的操作 - 读 : SHM_RDONLY, 必须要有读权限 - 读写: 0 - 返回值: 成功:返回共享内存的首(起始)地址。 失败(void *) -1 int shmdt(const void *shmaddr); - 功能:解除当前进程和共享内存的关联 - 参数: shmaddr:共享内存的首地址 - 返回值:成功 0, 失败 -1 int shmctl(int shmid, int cmd, struct shmid_ds *buf); - 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进程被销毁了对共享内存是没有任何影响。 - 参数: - shmid: 共享内存的ID - cmd : 要做的操作 - IPC_STAT : 获取共享内存的当前的状态 - IPC_SET : 设置共享内存的状态 - IPC_RMID: 标记共享内存被销毁 - buf:需要设置或者获取的共享内存的属性信息 - IPC_STAT : buf存储数据 - IPC_SET : buf中需要初始化数据,设置到内核中 - IPC_RMID : 没有用,NULL key_t ftok(const char *pathname, int proj_id); - 功能:根据指定的路径名,和int值,生成一个共享内存的key - 参数: - pathname:指定一个存在的路径 /home/nowcoder/Linux/a.txt / - proj_id: int类型的值,但是这系统调用只会使用其中的1个字节 范围 : 0-255 一般指定一个字符 'a' 问题1:操作系统如何知道一块共享内存被多少个进程关联? - 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch - shm_nattach 记录了关联的进程个数 问题2:可不可以对共享内存进行多次删除 shmctl - 可以的 - 因为shmctl 标记删除共享内存,不是直接删除 - 什么时候真正删除呢? 当和共享内存关联的进程数为0的时候,就真正被删除 - 当共享内存的key为0的时候,表示共享内存被标记删除了 如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。 共享内存和内存映射的区别 1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外) 2.共享内存效果更高 3.内存 所有的进程操作的是同一块共享内存。 内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。 4.数据安全 - 进程突然退出 共享内存还存在 内存映射区消失 - 运行进程的电脑死机,宕机了 数据存在在共享内存中,没有了 内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。 5.生命周期 - 内存映射区:进程退出,内存映射区销毁 - 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机 如果一个进程退出,会自动和共享内存进行取消关联。 ","date":"2023-03-02","objectID":"/%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98/:3:0","tags":["Linux多进程开发"],"title":"共享内存","uri":"/%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98/"},{"categories":["Linux多进程开发"],"content":"终端 1.在UNIX系统中,用户通过终端登陆系统后得到一个shell进程,这个终端成为shell进程的控制终端(Controlling Terminal),进程中,控制终端是保存在PCB中的信息,而fork()会复制PCB中的信息,因此由Shell进程启动的其他进程的控制终端也是这个终端。 2.默认情况下(没有重定向),每个进程的标准输入,标准输出和标准错误输出都是指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写,也就是输出到显示器上。 3。在控制终端输入一些特殊额控制键可以给前台进程发信号。例如Ctrl+C会产生SIGINT信号,CTRL+\\会产生SIGQUIT信号 ","date":"2023-03-02","objectID":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/:1:0","tags":["Linux多进程开发"],"title":"守护进程","uri":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/"},{"categories":["Linux多进程开发"],"content":"进程组 1.进程组和会话在进程之间形成了一种两级层次的关系:进程组是一组相关进程的集合。会话是一组相关进程组的集合。进程组和会话是为了支持shell作业控制,而定义的抽象概念,用户通过shell能够交互式的在前台或者后台运行命令。 2.进程组由一个或多个共享同一进程组标识符(PGID)的进程组成,一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程ID为该进程组的ID,新警察会继承其父进程的所属的进程组ID。 3.进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入另一个进程组而退出当前进程组。进程组首进程无需是最后一个离开进程组的成员 ","date":"2023-03-02","objectID":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/:2:0","tags":["Linux多进程开发"],"title":"守护进程","uri":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/"},{"categories":["Linux多进程开发"],"content":"会话 1.会话是一组进程组的集合。会话首进程是创建新会话额进程。其进程ID会称为会话ID。新进程会继承其父进程的会话ID。 2.一个会话中的所有进程共享一个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。 3.在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端读取输入。当用户在控制终端中输入终端字符产生信号后,该信号会被发送到前台进程组中的所有成员。 4.当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。 ","date":"2023-03-02","objectID":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/:3:0","tags":["Linux多进程开发"],"title":"守护进程","uri":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/"},{"categories":["Linux多进程开发"],"content":"进程组,会话,控制终端之间的关系 如下图: ","date":"2023-03-02","objectID":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/:4:0","tags":["Linux多进程开发"],"title":"守护进程","uri":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/"},{"categories":["Linux多进程开发"],"content":"进程组,会话操作函数 pid_t getpgrp(void);//获取组id pid_t getpgid(pid_t pid);//获取当前进程的进程组id int setpgid(pid_t pid,pid_t pgid);//设置进程的进程组id pid_t getsid(pid_t pid);//获取会话id pid_t setsid(void);//设置会话id ","date":"2023-03-02","objectID":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/:5:0","tags":["Linux多进程开发"],"title":"守护进程","uri":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/"},{"categories":["Linux多进程开发"],"content":"守护进程 1.守护进程(Daemon Process),也就是通常说的Daemon进程(精灵进程),是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用d结尾的名字。 2.守护进程具备一下特性: 2.1 生命周期长,守护进程会在系统启动的时候被创建并一直运行知道系统关闭 2.2 他在后台运行并且不拥有控制终端,没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号 3Linux的大多数服务器就是用守护进程实现的。比如Internet服务器inetd,Web服务器httpd等 ","date":"2023-03-02","objectID":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/:6:0","tags":["Linux多进程开发"],"title":"守护进程","uri":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/"},{"categories":["Linux多进程开发"],"content":"守护进程创建步骤 1.执行一个fork(),之后父进程退出,子进程继续执行 2.子进程调用setsid()并开启一个会话。 3.清除进程的umask以确保当守护进程创建文件和目录时拥有所需的权限 4.修改进程的当前工作目录,通常会改为根目录(/) 5、关闭守护进程从其父进程继承而来的所有打开着的文件描述符 6 在关闭了文件描述符0,1,2之后,守护进程通常会打开/dev/null并使用dup2()使所有这些描述符指向这个设备、 7 核心业务逻辑 例子 使用守护进程去实现每2秒记录系统时间并写入磁盘 /* 写一个守护进程,每隔两秒获取一下系统时间,将这个时间写入磁盘文件中 */ #include \u003cstdio.h\u003e #include \u003cstdlib.h\u003e #include \u003csys/types.h\u003e #include \u003cunistd.h\u003e #include \u003csys/stat.h\u003e #include \u003cfcntl.h\u003e #include \u003csys/time.h\u003e #include \u003csignal.h\u003e #include \u003ctime.h\u003e #include \u003cstring.h\u003e void work(int num){ printf(\"捕捉到信号:%d\\n\",num); //获取系统时间 time_t t = time(NULL); //将描述转换成系统时间 struct tm * loc = localtime(\u0026t); // char buffer[1024]; // sprintf(buffer,\"%d-%d-%d %d:%d:%d\\n\",loc-\u003etm_year,loc-\u003etm_mon,loc-\u003etm_mday,loc-\u003etm_hour,loc-\u003etm_min,loc-\u003etm_sec); char *buffer = asctime(loc); //写文件 int fd = open(\"time.txt\",O_RDWR|O_CREAT|O_APPEND,0775); write(fd,buffer,strlen(buffer)); close(fd); } int main(){ //创建子进程,退出父进程 pid_t pid = fork(); if(pid\u003e0){ //父进程直接退出 exit(0); } //将子进程重新创建一个会话 //新的会话脱离终端 setsid(); //修改进程的umask umask(022); //更改工作目录 chdir(\"/home/zxl/\"); //关闭重定向文件描述符 int fd = open(\"/dev/null\",O_RDWR); dup2(fd,STDIN_FILENO); dup2(fd,STDERR_FILENO); dup2(fd,STDOUT_FILENO); //业务逻辑 //需要信号捕捉 struct sigaction act; act.sa_flags = 0; act.sa_handler =work; sigemptyset(\u0026act.sa_mask); sigaction(SIGALRM,\u0026act,NULL); //每隔两秒获取系统时间 struct itimerval val; val.it_value.tv_sec=2; val.it_value.tv_usec=0; val.it_interval.tv_sec=2; val.it_interval.tv_usec=0; setitimer(ITIMER_REAL,\u0026val,NULL); //不让进程结束 while(1){ sleep(3); } return 0; } ","date":"2023-03-02","objectID":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/:6:1","tags":["Linux多进程开发"],"title":"守护进程","uri":"/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/"},{"categories":["Linux多进程开发"],"content":"信号的概念 1.信号是Linux进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式,信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某个突发事件 2.发往进程的诸多信号,通常都源于内核。引发内核为进程产生信号的各类事件如下: 2.1 对于前台进程,用户可以通过输入特殊的终端字符给它发送信号。比如输入Ctrl+C通常会给进程发送一个终端信号 2.2 硬件发生异常,及硬件检测到一个错误条件并通知内核,随机再由内核发送响应信号给相关进程。比如执行一条异常的机器语言指令,比如被0除,或者引用了无法访问的内存区域 2.3 系统状态变化,比如alarm定时器到期引起SIGALRM信号,进程执行cpu时间超限,或者该进程的某个子进程退出 2.4 运行kill命令 或者 kill函数 ","date":"2023-03-01","objectID":"/%E4%BF%A1%E5%8F%B7/:0:0","tags":["Linux多进程开发"],"title":"信号","uri":"/%E4%BF%A1%E5%8F%B7/"},{"categories":["Linux多进程开发"],"content":"使用信号的目的 1.使用信号的两个主要目的: 1.1让进程知道已经发生了特定的事情 1.2强迫进程执行他自己代码中的信号处理程序 2.信号的特点 2.1 简单 2.2 不能携带大量信息 2.3 满足某个特定条件才发送 2.4 优先级比较高 3.查看系统定义的信号列表 kill -i 4.前31个信号为常规信号,其余为实时信号 ","date":"2023-03-01","objectID":"/%E4%BF%A1%E5%8F%B7/:1:0","tags":["Linux多进程开发"],"title":"信号","uri":"/%E4%BF%A1%E5%8F%B7/"},{"categories":["Linux多进程开发"],"content":"信号列表 编号 信号名称 对应事件 默认动作 1 SIGHUP 用户退出shell时,由该shell启动的所有进程将收到这个信号 终止进程 2 SIGINT 当用户按下CTRL+C组合键时,用户终端向正在运行中的由该终端启动的程序发送此信号 终止进程 3 SICQUIT 用户按下CTRL+\\组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发送该信号 终止进程 4 SIGILL CPU检测到某进程执行了非法指令 终止并产生core文件 5 SIGTRAP 该信号由断点指令或其他trap指令产生 终止并产生core文件 6 SIGABRT 调用abort函数时产生该信号 终止并产生core文件 7 SIGBUS 非法访问内存地址,包括内存对其出错 终止并产生core文件 8 SIGFPE 在发生致命的运算错误时发出,不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误 终止并产生core文件 9 SIGKILL 无条件终止进程,该信号不能被忽略,处理和阻塞 终止进程,可以杀死任何进程 10 SIGUSEL 用户定义的信号,即程序员可以再程序中定义并使用 终止进程 11 SIGSECV 指示进程进行了无效内存访问(段错误) 终止并产生core文件 12 SIGUSR2 另外一个用户自定义信号,程序员可以在程序中定义并使用 终止进程 13 SIGPIPE Broken pipe向一个没有读端的管道写数据 终止进程 14 SIGALRM 定时器超时,超时的时间,由系统调用alarm设置 终止进程 15 SIGTERM 程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来表示程序正常退出。执行shell命令kill时,缺省产生这个信号 进程终止 16 SIGSTKFLT LINUX早期版本出现的信号,现在仍然保留 程序终止 17 SIGCHLD 子进程结束时,父进程会收到这个信号 忽略信号 18 SIGCONT 如果进程已停止,则使其继续运行 继续/忽略 19 SIGSTOP 停止进程的执行,信号不能被忽略,处理和阻塞 为终止进程 20 SIGTSTP 停止终端交互进程的运行,按下CTRL+X发出该信号 暂停进程 21 SIGTTIN 后台程序读终端控制台 暂停进程 22 SIGTTOU 该信号类似于SIGTTIN,后台进程向终端发送数据 暂停进程 23 SIGURG 套接字上有紧急数据时,向当前正在运行的进程发出信号,报告有紧急数据到达,如网络带外数据到达 忽略该信号 24 SIGXCPU 进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给进程 终止进程 25 SIGXFSZ 超过文件的最大长度设置 终止进程 26 SIGVTALRM 虚拟时钟超时时产生该信号,类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间 终止进程 27 SIGPROF 类似于SIGVTALRM,它不仅包括该进程占用CPU时间还包括执行系统调用时间 终止进程 28 SIGWINCH 窗口大小变化时发出 忽略 29 SIGIO 此信号向进程指示发出一个异步IO事件 忽略 30 SIGPWR 关机 终止进程 31 SIGSYS 无效系统调用 终止进程并产生core文件 34-64 SIGRTIMN—SIGRTMAX 没有固定含义 终止进程 ","date":"2023-03-01","objectID":"/%E4%BF%A1%E5%8F%B7/:2:0","tags":["Linux多进程开发"],"title":"信号","uri":"/%E4%BF%A1%E5%8F%B7/"},{"categories":["Linux多进程开发"],"content":"信号的5种默认处理动作 1.查看信号的详细信息: man 7 signal 2.信号的5种默认处理动作 2.1 Term 终止进程 2.2 Ign 当前进程忽略掉这个信号 2.3 Core 终止进程,并产生一个Core文件 2.4 Stop 暂停当前进程 2.5 Cont 继续执行当前被暂停的进程 信号的几种状态:产生,未决,递达 SIGKILL 和 SIGSTOP信号不能被捕捉,阻塞或忽略 ","date":"2023-03-01","objectID":"/%E4%BF%A1%E5%8F%B7/:3:0","tags":["Linux多进程开发"],"title":"信号","uri":"/%E4%BF%A1%E5%8F%B7/"},{"categories":["Linux多进程开发"],"content":"信号相关函数 int kill(pid_t pid,int sig); int raise(int sig); void abort(void); unsigned int alarm(unsigned int seconds); in setitimer(int which,const struct itimerval *new_val,struct itimerval * old_value); kill,raise,abort函数详细 #include \u003csys/types.h\u003e #include \u003csignal.h\u003e int kill(pid_t pid,int sig); 作用:给某个进程pid,发送信号sig 参数: pid:需要发送的进程的pid pid\u003e0:将信号发送给指定的进程 pid=0:将信号发送给当前的进程组 pid=-1:将信号发送给每一个有权限接受这个信号的进程 pid\u003c-1:pid=某个进程组的id取反,给进程组内所有成员发送信号 sig:需要发送的sig编号或者宏值 例如:kill(getppid(),9); int raise(int sig); 功能:给当前进程发送信号 参数: sig:需要发送的sig编号或者宏值 返回值: 成功0 失败除0外的数字 void abort(void); 功能:发送SIGABRT信号给当前进程,杀死当前进程 kill(getpid(),SIGABRT); alarm(),setitimer(..) #include \u003cunistd.h\u003e unsigned int alarm(unsigned int seconds); 功能:定时 参数:seconds 秒 返回值:返回调用时的时间 #include \u003csys/time.h\u003e int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); 功能:设置定时器(闹钟),可以替代alarm函数,精度好,实现周期性定时 参数: which:定时器以什么时间计时 ITIMER_REAL:真实时间,时间到达,发送SIGALRM 常用的 ITIMER_VIRTUAL:用户时间,时间到达,发送SIGVTALRM ITIMER_PROF:以该进程在用户态和内核态下所消耗额时间来计算,时间到达,发送SIGPROF new_value:设置定时器的属性, struct itimerval { //定时器的结构体 struct timeval it_interval; //间隔时间 struct timeval it_value; //延迟多长时间执行定时器 }; struct timeval { //时间的结构体 time_t tv_sec; // 秒数 suseconds_t tv_usec; // 微秒 }; old_value:记录上一次的定时的时间参数,一般不使用,可以指定NULL 返回值: 成功:0 失败:-1 设置errno ","date":"2023-03-01","objectID":"/%E4%BF%A1%E5%8F%B7/:3:1","tags":["Linux多进程开发"],"title":"信号","uri":"/%E4%BF%A1%E5%8F%B7/"},{"categories":["Linux多进程开发"],"content":"信号捕捉函数 sighandler_ signal(int signum,sighandler_t handler); int sigaction(int signum,const struct sigaction *act,struct sigaciton *oldact); sighandler_ signal(int signum,sighandler_t handler); /* #include \u003csignal.h\u003e typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); 功能: 参数: -signum:要捕捉的信号 -handler:捕捉到信号如何处理 SIG_IGN:忽略,捕捉到就忽略 进程不终止 SIG_DFL:使用信号默认的行为,原本的行为 回调函数:去执行操作 返回值: 成功:返回上一次注册的信号处理函数的地址,第一次调用返回NULL 失败:返回SIG_ERR,设置errno 回调函数: -需要程序员自己实现,提前准备好,函数的类型根据实际需要 -不是程序员调用,而是当信号产生的时候,内核自动调用 */ int sigaction(int signum,const struct sigaction *act,struct sigaciton *oldact); #include \u003csignal.h\u003e int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); - 功能:检查或者改变信号的处理。信号捕捉 - 参数: - signum : 需要捕捉的信号的编号或者宏值(信号的名称) - act :捕捉到信号之后的处理动作 - oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL - 返回值: 成功 0 失败 -1 struct sigaction { // 函数指针,指向的函数就是信号捕捉到之后的处理函数 void (*sa_handler)(int); // 不常用 void (*sa_sigaction)(int, siginfo_t *, void *); // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。 sigset_t sa_mask; // 使用哪一个信号处理对捕捉到的信号进行处理 // 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction int sa_flags; // 被废弃掉了 void (*sa_restorer)(void); }; ","date":"2023-03-01","objectID":"/%E4%BF%A1%E5%8F%B7/:3:2","tags":["Linux多进程开发"],"title":"信号","uri":"/%E4%BF%A1%E5%8F%B7/"},{"categories":["Linux多进程开发"],"content":"信号集 1.许多信号相关的系统调用都需要能够表示一组不同信号,多个信号可使用一个称之为信号集的数据结构表示,其系统数据类型为sigset_t。 2.在PCB中有两个非常重要的信号集,一个称之为’阻塞信号集’,另一个称之为’未决信号集’。这两个信号集都是内核是由位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改。 3.信号的’未决’是一种状态,指的是从信号的产生到信号被处理前的这一段时间。 4.信号的’阻塞’是一个开关动作,指的是阻止信号被处理,而不是阻止信号产生。 5.信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感操作。 以下信号集相关的函数都是对自定义的信号集进行操作。 int sigemptyset(sigset_t *set); - 功能:清空信号集中的数据,将信号集中的所有的标志位置为0 - 参数:set,传出参数,需要操作的信号集 - 返回值:成功返回0, 失败返回-1 int sigfillset(sigset_t *set); - 功能:将信号集中的所有的标志位置为1 - 参数:set,传出参数,需要操作的信号集 - 返回值:成功返回0, 失败返回-1 int sigaddset(sigset_t *set, int signum); - 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号 - 参数: - set:传出参数,需要操作的信号集 - signum:需要设置阻塞的那个信号 - 返回值:成功返回0, 失败返回-1 int sigdelset(sigset_t *set, int signum); - 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号 - 参数: - set:传出参数,需要操作的信号集 - signum:需要设置不阻塞的那个信号 - 返回值:成功返回0, 失败返回-1 int sigismember(const sigset_t *set, int signum); - 功能:判断某个信号是否阻塞 - 参数: - set:需要操作的信号集 - signum:需要判断的那个信号 - 返回值: 1 : signum被阻塞 0 : signum不阻塞 -1 : 失败 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); int sigpending(sigset_t *set); int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); - 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换) - 参数: - how : 如何对内核阻塞信号集进行处理 SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变 假设内核中默认的阻塞信号集是mask, mask | set SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞 mask \u0026= ~set SIG_SETMASK:覆盖内核中原来的值 - set :已经初始化好的用户自定义的信号集 - oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL - 返回值: 成功:0 失败:-1 设置错误号:EFAULT、EINVAL int sigpending(sigset_t *set); - 功能:获取内核中的未决信号集 - 参数:set,传出参数,保存的是内核中的未决信号集中的信息。 ","date":"2023-03-01","objectID":"/%E4%BF%A1%E5%8F%B7/:3:3","tags":["Linux多进程开发"],"title":"信号","uri":"/%E4%BF%A1%E5%8F%B7/"},{"categories":["Linux多进程开发"],"content":"SIGCHLD信号 1.SIGCHLD信号产生的条件 1.1 子进程终止时 1.2 子进程接收到SIGSTOP信号停止时 1.3 子进程处在停止态,接受SIGCONT后唤醒时 2 以上三种条件都会给父进程发送SIGCHLD信号,父进程默认忽略该信号 ","date":"2023-03-01","objectID":"/%E4%BF%A1%E5%8F%B7/:3:4","tags":["Linux多进程开发"],"title":"信号","uri":"/%E4%BF%A1%E5%8F%B7/"},{"categories":["Linux多进程开发"],"content":"内存映射 内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。 ","date":"2023-03-01","objectID":"/%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84/:0:0","tags":["Linux多进程开发"],"title":"内存映射","uri":"/%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84/"},{"categories":["Linux多进程开发"],"content":"内存映射相关系统调用 #include \u003csys/mman.h\u003e void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset); int munmap(void *addr, size_t length); ","date":"2023-03-01","objectID":"/%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84/:1:0","tags":["Linux多进程开发"],"title":"内存映射","uri":"/%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84/"},{"categories":["Linux多进程开发"],"content":"使用父子进程通信 /* #include \u003csys/mman.h\u003e void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 功能:将一个文件或者设备的数据映射到内存中 参数: -void *addr:NULL 由内核指定 -size_t lenght:映射的数据的长度,这个值不能为0,建议使用文件的长度 int port:申请的内存映射区的操作权限 PROT_EXEC Pages may be executed. PROT_READ Pages may be read. PROT_WRITE Pages may be written. PROT_NONE Pages may not be accessed. int flag: - MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步 - MAP——PRIVATE:映射区的数据会自动和磁盘文件不同步 int fd: 需要映射的文件的文件描述符 注意:通过open得到 文件的大小不能为0,open指定的权限不能和port冲突 port:PROT_READ open:只读/只写 prot:PROT_READ | PROT_WRITE open open:读写 off_t offset : 偏移量,一般不用,必须指定4K的整数倍,0 表示从头 返回值: 成功:返回首地址 错误:返回MAP_FAILED 并且set errno int munmap(void *addr, size_t length); -功能:释放内存映射 -参数: -addr : 要释放的内存的地址 length: 释放的内存的大小,要和mmap里的length大小相同 */ #include \u003csys/mman.h\u003e #include \u003cstdio.h\u003e #include \u003csys/types.h\u003e #include \u003cfcntl.h\u003e #include \u003cunistd.h\u003e #include \u003cstring.h\u003e #include \u003cstdlib.h\u003e #include \u003csys/wait.h\u003e int main(){ //1.打开一个文件 int fd = open (\"test.txt\",O_RDWR); //获取文件大小 int size = lseek(fd,0,SEEK_END); //创建内存映射区 void *addr = mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); if(addr == MAP_FAILED){ perror(\"mmap\"); exit(0); } //创建子进程 pid_t pid = fork(); if(pid\u003e0){ //父 //回收 wait(NULL); //定义数组 char buffer[64]; strcpy(buffer,(char * )addr); printf(\"read date : %s\\n\",buffer); }else if(pid == 0){ //子 strcpy((char *)addr,\"你好\"); } //关闭 munmap(addr,size); return 0; } ","date":"2023-03-01","objectID":"/%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84/:1:1","tags":["Linux多进程开发"],"title":"内存映射","uri":"/%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84/"},{"categories":["Linux多进程开发"],"content":"无关系进程通信 process1 #include \u003csys/mman.h\u003e #include \u003csys/types.h\u003e #include \u003csys/stat.h\u003e #include \u003cfcntl.h\u003e #include \u003cstdio.h\u003e #include \u003cstdlib.h\u003e #include \u003cstring.h\u003e #include \u003cunistd.h\u003e int main(){ //执行没有关系的两个文件的读写操作 //打开文件 int fd1 = open(\"test1.txt\",O_RDWR); int size1 = lseek(fd1,0,SEEK_END); //创建内存映射区 void * ptr1 = mmap(NULL,size1,PROT_READ|PROT_WRITE,MAP_SHARED,fd1,0); if(ptr1==MAP_FAILED){ perror(\"mmap\"); exit(0); } //执行写入数据 strcpy((char *)ptr1,\"呵呵呵呵呵呵\"); munmap(ptr1,size1); } process2 #include \u003csys/mman.h\u003e #include \u003csys/types.h\u003e #include \u003csys/stat.h\u003e #include \u003cfcntl.h\u003e #include \u003cstdio.h\u003e #include \u003cstdlib.h\u003e #include \u003cstring.h\u003e #include \u003cunistd.h\u003e int main(){ //执行没有关系的两个文件的读写操作 //打开文件 int fd1 = open(\"test1.txt\",O_RDONLY); int size1 = lseek(fd1,0,SEEK_END); //创建内存映射区 void * ptr1 = mmap(NULL,size1,PROT_READ,MAP_SHARED,fd1,0); if(ptr1==MAP_FAILED){ perror(\"mmap\"); exit(0); } //执行读 char buffer[128]; memset(buffer,'0',128); strcpy(buffer,(char * )ptr1); printf(\"recv data :%s\\n\",buffer); munmap(ptr1,size1); } ","date":"2023-03-01","objectID":"/%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84/:1:2","tags":["Linux多进程开发"],"title":"内存映射","uri":"/%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84/"},{"categories":["Linux多进程开发"],"content":"思考问题 1.如果对mmap的返回值(ptr)做++操作,mmap是否能够成功? void * ptr - mmap(..) ptr++;//可以进程操作 munmap(ptr,len);//错误,需要保存之前的地址 2.如果open时O_RDONLY,mmap时port参数指定PORT_READ|PORT_WRITE会如何? 错误,会返回MAP_FAILED 不可以执行写权限 3.如果文件偏移量为1000 偏移量必须是4 * 1024的整数倍,一般情况错误返回MAP_FAILED 4.mmap什么情况下调用失败? 第二个参数length: = 0,会调用失败 第三个参数PORT权限,如果只是指定了PORT_WRITE也会失败,一般需要指定PORT_READ|PORT_WRITE 如果权限大于open的权限,也会失败 5.可以open的时候O_CREAT一个新文件来创建映射区么? 可以的,但是创建的文件的大小如果为0的话,肯定不行 可以对新的文件进行拓展,lseek();truncate() 6.mmap后关闭文件描述符,对mmap有无影响? int fd = open(...) void* ptr = mmap(..,..,..,fd,..); close(fd) 映射区还是存在的 7.对ptr越界操作会如何 void * ptr = mmap(NULL,100,...); 会以内存的分页的大小,来指定大小 越界操作的是非法的内存,会产生段错误 ","date":"2023-03-01","objectID":"/%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84/:2:0","tags":["Linux多进程开发"],"title":"内存映射","uri":"/%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84/"},{"categories":["Linux多进程开发"],"content":"实现文件拷贝 /* 使用内存映射拷贝文件 思路: 1.对原始文件进行内存映射 2.创建新文件,lseek,truncate 3.把新文件的数据映射到内存中 4.通过内存拷贝,将第一个文件的内存数据拷贝到第二个文件的内存中 5.释放资源 */ #include \u003csys/mman.h\u003e #include \u003csys/types.h\u003e #include \u003csys/stat.h\u003e #include \u003cfcntl.h\u003e #include \u003cstdio.h\u003e #include \u003cstdlib.h\u003e #include \u003cstring.h\u003e #include \u003cunistd.h\u003e int main(){ //1.对原始文件进行内存映射 int fd = open(\"english.txt\",O_RDWR); if(fd==-1){ perror(\"open\"); exit(0); } //创建新的文件 int fd2 = open(\"copy.txt\",O_RDWR|O_CREAT,0664); if(fd2==-1){ perror(\"open\"); exit(0); } //获取源文件的大小 int size = lseek(fd,0,SEEK_END); //对新创建的文件进行拓展 // lseek(fd2,size,SEEK_SET); truncate(\"copy.txt\",size); write(fd2,\" \",1); //内存映射 void * ptr1 = mmap(NULL,size,PROT_READ,MAP_SHARED,fd,0); void * ptr2 = mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); if(ptr1==MAP_FAILED||ptr2==MAP_FAILED){ perror(\"mmap\"); exit(0); } //内存拷贝 memcpy(ptr2,ptr1,size); //关闭 munmap(ptr1,size); munmap(ptr2,size); close(fd2); close(fd); return 0; } ","date":"2023-03-01","objectID":"/%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84/:2:1","tags":["Linux多进程开发"],"title":"内存映射","uri":"/%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84/"},{"categories":["Linux多进程开发"],"content":"进程间通讯概念 1.进程是一个独立的资源分配单元,不同的进程(用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源 2.但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信 3.进程间通信的目的 3.1数据传输:一个进程需要将它的数据发送给另一个进程 3.2通知时间:一个进程需要向另一个或一组进程发送信息,通知他们发生了某种时间(如进程终止时通知父进程) 3.3资源共享:多个进程之间共享同样的资源,为了做到这一点,需要内核提供互斥和同步机制 3.4进程控制:游戏进程希望完全控制另一个进程的执行(如DEBUG),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变. ","date":"2023-03-01","objectID":"/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E6%A6%82%E5%BF%B5/:0:0","tags":["Linux多进程开发"],"title":"进程间通信概念","uri":"/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E6%A6%82%E5%BF%B5/"},{"categories":["Linux多进程开发"],"content":"同一主机进程间通信 Unix进程间通信方式:1.匿名管道2.有名管道3.信号 SystemV进程间通信方式:1.消息队列2.共享内存3.信号量 POSIX进程间通信方式:1.消息队列2.共享内存3.信号量 ","date":"2023-03-01","objectID":"/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E6%A6%82%E5%BF%B5/:1:0","tags":["Linux多进程开发"],"title":"进程间通信概念","uri":"/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E6%A6%82%E5%BF%B5/"},{"categories":["Linux多进程开发"],"content":"匿名管道 1.管道也叫匿名管道,他是UNIX系统IPC(进程间通信)最古老的形式,所有UNIX系统都支持这种通信机制 2.统计一个目录中文件的数目命令:ls|wc -l,为了执行该命令,shell创建了两个进程来分别执行ls和wc 管道的特点 1.管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同 2.管道拥有文件的特质:读操作.写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据,可以按照操作文件的方式对管道进行操作。 3.一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少 4.通过管道传递的数据是顺序的,从管道中读取的字节的顺序和它们被写入管道的顺序是一样的。 管道的特点 1.在管道中的数据传递方向是单向的,一端用于写入,一端用于读取,管道时半双工的 2.从管道读数据是一次操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用lseek()来随机访问数据 3.匿名管道只能在具有公共先祖的进程(父进程子进程,兄弟进程)之间使用。 匿名管道的使用 1.创建匿名管道 #include \u003cunistd.h\u003e int pipe(int pipefd(2)); 2.查看管道缓冲大小命令 ulimit -a 3.查看管道缓冲大小的函数 #include \u003cunistd.h\u003e long fpathconf(int fd,int name); example int pipe(int pipefd(2)); /* #include \u003cunistd.h\u003e int pipe(int pipefd[2]); 作用:创建一个匿名管道 参数: int pipefd[2]:这个数组是一个传出参数 pipefd[0]:对应的是读端 pipefd[1]:对应的是写端 返回值: success:0 fail:-1 and set errno 管道默认是阻塞的,如果管道中没有数据 read阻塞,如果管道满了,write阻塞 注意:匿名管道只能用于有关系的进程间通信 */ #include \u003cunistd.h\u003e #include \u003csys/types.h\u003e #include \u003cstdio.h\u003e #include \u003cstdlib.h\u003e #include \u003cstring.h\u003e //子进程发送数据给父进程 int main(){ //管道需要在fork之前创建管道 int pipefd[2]; int ret = pipe(pipefd); if(ret==-1){ perror(\"pipe\"); exit(0); } //创建子进程 pid_t pid =fork(); //判断 if(pid \u003e 0){ //父进程 printf(\"parent process pid %d\\n\",getpid()); char buf[1024]={0}; while(1){ int len = read(pipefd[0],buf,sizeof(buf)); printf(\"parent recv %s, pid %d\\n\",buf,getpid()); char * str = \"Hello I m parent\"; write(pipefd[1],str,strlen(str)); sleep(1); } }else if(pid == 0){ printf(\"child process, pid %d\\n\",getpid()); while(1){ //从管道的读取端读取数据 char * str = \"Hello I m child\"; write(pipefd[1],str,strlen(str)); sleep(1); char buf[1024]={0}; int len = read(pipefd[0],buf,sizeof(buf)); printf(\"child recv %s, pid %d\\n\",buf,getpid()); } } return 0; } example long fpathconf(int fd,int name); /* #include \u003cunistd.h\u003e long fpathconf(int fd, int name); long pathconf(const char *path, int name); */ #include \u003cunistd.h\u003e #include \u003csys/types.h\u003e #include \u003cstdio.h\u003e #include \u003cstdlib.h\u003e int main(int argc ,char *argv[]){ int pipefd[2]; int ret = pipe(pipefd); if(ret==-1){ perror(\"pipe\"); exit(0); } pid_t pid = fork(); if(pid\u003e0){ //father process printf(\"father process pid = %d\\n\",getpid()); printf(\"pipe read buffer size = %ld,pipe write buffer size = %ld\\n\",fpathconf(pipefd[0],_PC_PIPE_BUF),fpathconf(pipefd[1],_PC_PIPE_BUF)); }else if(pid==0){ printf(\"child process pid = %d\\n\",getpid()); printf(\"pipe read buffer size = %ld,pipe write buffer size = %ld\\n\",fpathconf(pipefd[0],_PC_PIPE_BUF),fpathconf(pipefd[1],_PC_PIPE_BUF)); } return 0; } 由于上述代码使用sleep控制输出,才没有导致单一进程读写管道内数据。基本上管道只作为单一方向通信时使用。其使用的模型分为三种,如下图所示 所以,如果我们想要双方通信的话,可以选择模式3,关闭写方读通道,关闭读方写通道。具体代码如下: /* #include \u003cunistd.h\u003e int pipe(int pipefd[2]); 作用:创建一个匿名管道 参数: int pipefd[2]:这个数组是一个传出参数 pipefd[0]:对应的是读端 pipefd[1]:对应的是写端 返回值: success:0 fail:-1 and set errno 管道默认是阻塞的,如果管道中没有数据 read阻塞,如果管道满了,write阻塞 注意:匿名管道只能用于有关系的进程间通信 */ #include \u003cunistd.h\u003e #include \u003csys/types.h\u003e #include \u003cstdio.h\u003e #include \u003cstdlib.h\u003e #include \u003cstring.h\u003e //子进程发送数据给父进程 int main(){ //管道需要在fork之前创建管道 int pipefd[2]; int ret = pipe(pipefd); if(ret==-1){ perror(\"pipe\"); exit(0); } //创建子进程 pid_t pid =fork(); //判断 char buf[1024]={0};//buffer if(pid \u003e 0){ //父进程读 printf(\"parent process pid %d\\n\",getpid()); //关闭写 close(pipefd[1]); while(1){ int len = read(pipefd[0],buf,sizeof(buf)); printf(\"parent recv %s, pid %d\\n\",buf,getpid()); memset(buf,'\\0',sizeof(buf)); sleep(1); } }else if(pid == 0){ //子进程写 printf(\"child process, pid %d\\n\",getpid()); //关闭读 close(pipefd[0]); char *str = \"我爱洗澡皮肤好好,噢噢噢噢!\"; while(1){ write(pipefd[1],str,strlen(str)); sleep(1); } } return 0; } 案例 实现ps aux | grep xxx /* 实现ps aux | grep xxx 父子进程间通信 子进程执行ps aux,子进程结束之后,将数据发送给父进程 父进程获取到数据,过滤 pipe() fork() execlp() 子进程讲标准输出stdout_fileno 重定向到管道的写端 dup */ #include \u003cunistd.h\u003e #include \u003csys/types.h\u003e #include \u003cstdio.h\u003e #include \u003cstdlib.h\u003e #include \u003cstring.h\u003e #include \u003cregex.h\u003e #include \u003cwait.h\u003e int main(int argc,char * argv[]){ //先创建管道 int pipefd[2]; int ret = pipe(pipefd); if(ret==-1){ perror(\"pipe\"); exit(0); } /","date":"2023-03-01","objectID":"/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E6%A6%82%E5%BF%B5/:1:1","tags":["Linux多进程开发"],"title":"进程间通信概念","uri":"/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E6%A6%82%E5%BF%B5/"},{"categories":["Linux多进程开发"],"content":"有名管道 1.匿名管道,由于没有名字,只能用于亲缘关系的进程间通信,为了克服这一点,提出了有名管道(FIFO),也叫命名管道,FIFO文件 2.有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的。这样及时与FIFO的创建进程不存在亲缘关系的进程,也可以访问该路径,就能够彼此通过FIFO通信。 3.一旦打开了FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的IO系统调用(read,write,close)。与管道一样,FIFO也有一个写入端和一个读取端。并且从个管道中读取数据的顺序与写入的顺序是一样的。FIFO的名称也由此而来,First in First out 区别 有名管道(FIFO)和匿名管道(pipe)有一些特点相同,不一样的地方在于: 1.FIFO在文件系统中作为一个特殊文件存在,但FIFO中的内容却存放在内存中 2.当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用. 3.FIFO有名字,不相干的进程可以打开有名管道进行通信 有名管道的使用 1.通过命令创建有名管道 mkfifo 名字 2.通过函数创建 #include \u003csys/types/h\u003e #inclued \u003csys/stat.h\u003e int mkfifo(const char *pathname,mode_t mode); 3.一旦使用mkfifo创建了一个FIFO,就可以使用open打开,使用IO函数操作 4.不支持lseek()等文件定位操作 ","date":"2023-03-01","objectID":"/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E6%A6%82%E5%BF%B5/:1:2","tags":["Linux多进程开发"],"title":"进程间通信概念","uri":"/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E6%A6%82%E5%BF%B5/"},{"categories":["hugo使用","Git使用"],"content":"关于Hugo在GitAction部署期间的问题 参参照网站: 1.利用GitHub Action实现Hugo博客在GitHub Pages自动部署(Access day:2023/02/28) 2.Hugo + GitHub Action,搭建你的博客自动发布系统(Access day:2023/02/28) ","date":"2023-02-28","objectID":"/gitaction_hugo/:0:0","tags":["hugo"],"title":"Hugo + GitHub Action + 腾讯云 搭建你的博客自动发布系统","uri":"/gitaction_hugo/"},{"categories":["hugo使用","Git使用"],"content":"hugo使用请参照 创建Blog流程 ","date":"2023-02-28","objectID":"/gitaction_hugo/:1:0","tags":["hugo"],"title":"Hugo + GitHub Action + 腾讯云 搭建你的博客自动发布系统","uri":"/gitaction_hugo/"},{"categories":["hugo使用","Git使用"],"content":"搭建流程(域名),其他可参考上述网站 1.进入腾讯云购买个人域名 2.进入腾讯云的管理节点,点击自己购买的域名,选择添加记录,并选择记录类型CNAME,将记录值修改为自己的github.io,如下图: 3.进入github,在自己的github.io仓库下进入Settings,并在侧边栏找到Pages,修改Custom Domain,将其修改为自己购买的域名。如下图 4.修改个人博客的配置文件(config.yml),将其中的baseUrl设置为自己的域名。如下图: ","date":"2023-02-28","objectID":"/gitaction_hugo/:2:0","tags":["hugo"],"title":"Hugo + GitHub Action + 腾讯云 搭建你的博客自动发布系统","uri":"/gitaction_hugo/"},{"categories":["hugo使用","Git使用"],"content":"问题1 hugo调用的主题是一个Git仓库 如果hugo调用的主题是一个Git仓库的,可以使用以下命令,将主题的Git仓库作为你的仓库的子目录,它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。 git submodule add https://github.com/dillonzq/LoveIt.git themes/LoveIt 默认情况下,子模块会将子项目放到一个与仓库同名的目录中,即“XXX”。 如果你想要放到其他地方,那么可以在命令结尾添加一个不同的路径。如果这时运行 git status,注意到新的.gitmodules文件。该配置文件保存了项目URL与已经拉取的本地目录之间的映射。 $ git status On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use \"git reset HEAD \u003cfile\u003e...\" to unstage) new file: .gitmodules new file: XXX .gitmodules的配置具体为下: [submodule \"themes/LoveIt\"] path = themes/LoveIt url = https://github.com/dillonzq/LoveIt.git branch = release ","date":"2023-02-28","objectID":"/gitaction_hugo/:3:0","tags":["hugo"],"title":"Hugo + GitHub Action + 腾讯云 搭建你的博客自动发布系统","uri":"/gitaction_hugo/"},{"categories":["hugo使用","Git使用"],"content":"问题2 未知但解决的问题 错误为以下问题 /usr/bin/git push origin main fatal: unable to access 'https://x-access-token:***@github.com/zxlkgf/zxlkgf.github.io.git/': URL using bad/illegal format or missing URL Error: Action failed with \"The process '/usr/bin/git' failed with exit code 128 原先在YML中使用的是PERSONAL_TOKEN,不知道为什么报错,原因目前未知。 查看以下网页,发现可以改名尝试。 GitHub Actions: actions/checkout で、repository not found エラーが出るときの回避策 遂改为ZXL_BLOG_TOKEN之后问题解决。 YML文件如下 name: deploy on: push: workflow_dispatch: schedule: # Runs everyday at 8:00 AM - cron: \"0 0 * * *\" jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: submodules: true fetch-depth: 0 - name: Setup Hugo uses: peaceiris/actions-hugo@v2 with: hugo-version: \"latest\" extended: true - name: Build Web run: hugo - name: Deploy Web uses: peaceiris/actions-gh-pages@v3 with: PERSONAL_TOKEN: ${{ secrets.ZXL_BLOG_TOKEN }} EXTERNAL_REPOSITORY: zxlkgf/zxlkgf.github.io PUBLISH_BRANCH: main PUBLISH_DIR: ./public commit_message: ${{ github.event.head_commit.message }} ","date":"2023-02-28","objectID":"/gitaction_hugo/:4:0","tags":["hugo"],"title":"Hugo + GitHub Action + 腾讯云 搭建你的博客自动发布系统","uri":"/gitaction_hugo/"},{"categories":["Linux编程入门"],"content":"exec函数族介绍 1.exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件 2.exec函数组的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段,堆栈等已经被新的内容取代了,只留下进程ID等表面上的信息仍保持原样,只有调用失败了,它们才会返回-1,从原程序的调用点接着往下执行 ","date":"2023-02-07","objectID":"/exec%E5%87%BD%E6%95%B0%E6%97%8F/:0:0","tags":["Linux编程入门"],"title":"exec函数族","uri":"/exec%E5%87%BD%E6%95%B0%E6%97%8F/"},{"categories":["Linux编程入门"],"content":"exec函数族 int execl(const char *pathname, const char arg, … / (char *) NULL */); int execlp(const char *file, const char arg, …/ (char *) NULL */); int execle(const char *pathname, const char arg, …/, (char *) NULL, char *const envp[] */); int execv(const char *pathname, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[],char *const envp[]); l(list) 参数地址列表,以空指针结尾 v(vector) 存有各参数地址的指针数组的地址 p(path) 按PATH环境变量指定的目录搜索可执行文件 e(environment) 存有环境变量字符串地址的指针数组的地址 ","date":"2023-02-07","objectID":"/exec%E5%87%BD%E6%95%B0%E6%97%8F/:1:0","tags":["Linux编程入门"],"title":"exec函数族","uri":"/exec%E5%87%BD%E6%95%B0%E6%97%8F/"},{"categories":["进程","Linux"],"content":"进程 ","date":"2023-02-05","objectID":"/%E8%BF%9B%E7%A8%8B/:0:0","tags":["进程"],"title":"进程","uri":"/%E8%BF%9B%E7%A8%8B/"},{"categories":["进程","Linux"],"content":"查看进程 ps aux/ajx a:显示终端上所有进程,包括其他用户的进程 u:显示进程的详细信息 x:显示没有控制终端的进程 j:列出与作业息息相关的信息 ","date":"2023-02-05","objectID":"/%E8%BF%9B%E7%A8%8B/:1:0","tags":["进程"],"title":"进程","uri":"/%E8%BF%9B%E7%A8%8B/"},{"categories":["进程","Linux"],"content":"进程相关STAT参数 D 不可中断 R 正在运行,或在队列中的进程 S 休眠状态 T 停止或被追踪 Z 僵尸进程 W 进入内存交换(kneral2.6开始无效) X 死掉的进程 \u003c 高优先级 N 低优先级 s 包含子进程 + 位于前台的进程组 ","date":"2023-02-05","objectID":"/%E8%BF%9B%E7%A8%8B/:2:0","tags":["进程"],"title":"进程","uri":"/%E8%BF%9B%E7%A8%8B/"},{"categories":["进程","Linux"],"content":"进程相关命令 top 可以再使用top命令上加-G来指定显示信息更新的时间间隔,在top命令执行后,可以按一下按键显示的结果进行排序: M 根据内存使用量排序 P 根据CPU占有率进行排序 T 根据进程运行时间长短排序 U 根据用户名来筛选 K 输入指定的PID杀死线程 ","date":"2023-02-05","objectID":"/%E8%BF%9B%E7%A8%8B/:3:0","tags":["进程"],"title":"进程","uri":"/%E8%BF%9B%E7%A8%8B/"},{"categories":["进程","Linux"],"content":"杀死进程 kill [-signal] pid kill -l 列出所有信号 kill -SIGKILL 进程ID kill -9 进程ID killall name 根据进程名杀死进程 ","date":"2023-02-05","objectID":"/%E8%BF%9B%E7%A8%8B/:4:0","tags":["进程"],"title":"进程","uri":"/%E8%BF%9B%E7%A8%8B/"},{"categories":["进程","Linux"],"content":"进程号和相关函数 1.每个进程都由进程号来标识,其类型为pid_t(整型),进程号的范围为0-32767,进程号总是唯一的,但可以重复使用 2.任何进程(除了init进程)都是由另一个进程创建,该进程成为被创建进程的父进程,对于的进程号为父进程号(PPID) 3.进程组是一个或多个进程的集合,他们之间互相关联,进程组可以接受同一终端的各种信号,关联的进程有一个进程组号(PGID),默认情况下,当前的进程号会当做当前的进程组号 4.进程号和进程组的相关函数 pid_t getpid(void); pid_t getppid(void); pid_t getpgid(void); ","date":"2023-02-05","objectID":"/%E8%BF%9B%E7%A8%8B/:5:0","tags":["进程"],"title":"进程","uri":"/%E8%BF%9B%E7%A8%8B/"},{"categories":["进程","Linux"],"content":"孤儿进程 1.父进程允许结束,但是子进程还在运行,这样的进程成为孤儿进程 2.每当出现一个孤儿进程的时候,内存就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。 3.因此孤儿进程并不是什么危害 ","date":"2023-02-05","objectID":"/%E8%BF%9B%E7%A8%8B/:6:0","tags":["进程"],"title":"进程","uri":"/%E8%BF%9B%E7%A8%8B/"},{"categories":["进程","Linux"],"content":"僵尸进程 1.每个进程结束之后,,都会释放自己地址空间中的用户数据,内核区的pcb没有办法自己去释放需要父进程去释放 2.进程终止时,父进程尚未回收,子进程残留资源(pcb)存放于内核中,变成僵尸进程 3.僵尸进程不可被kill -9杀死 4.这样会导致一个问题 ,父进程不调用wait()或者waitpid()的话,那么保留的那段信息不会被释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的僵尸进程产生,,将因为没有可以使用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害 ","date":"2023-02-05","objectID":"/%E8%BF%9B%E7%A8%8B/:7:0","tags":["进程"],"title":"进程","uri":"/%E8%BF%9B%E7%A8%8B/"},{"categories":["进程","Linux"],"content":"进程回收 1.在每个进程退出的时候,内核释放进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息,这些信息主要指向控制块PCB的信息 2.父进程可以通过调用wait或者waitpid得到它的退出状态同时彻底清除掉这个进程。 3.wait()和waitpid()函数功能一样,区别在于,wait()函数会阻塞,waitpid()可以设置不阻塞,waitpid()可以指定等待哪个子进程结束。 4.注意 一次wait或waitpid调用只能清理一个子进程,清理多个子进程需要循环 ","date":"2023-02-05","objectID":"/%E8%BF%9B%E7%A8%8B/:8:0","tags":["进程"],"title":"进程","uri":"/%E8%BF%9B%E7%A8%8B/"},{"categories":["进程","Linux"],"content":"退出信息相关宏函数 WIFEXITED(status) 非0 进程正常退出 WEXITSTATUS(status) 如果宏为真,获取进程退出的状态 WIFSIGNALED(status) 非0 进程异常退出 WTERMSIG(status) 如果上宏为真,获取使进程停止的信号编号 WIFSTOPPED(status) 非0 进程处于暂停 WSTOPSIG(status) 如果上宏为真,获取使进程暂停的信号的编号 WIFCONTINUED(status) 非0 进程暂停后已经继续运行 ","date":"2023-02-05","objectID":"/%E8%BF%9B%E7%A8%8B/:9:0","tags":["进程"],"title":"进程","uri":"/%E8%BF%9B%E7%A8%8B/"},{"categories":["Linux多进程开发"],"content":"进程的创建 ","date":"2023-02-05","objectID":"/%E8%BF%9B%E7%A8%8B%E5%88%9B%E5%BB%BA/:0:0","tags":["Linux多进程开发"],"title":"进程创建","uri":"/%E8%BF%9B%E7%A8%8B%E5%88%9B%E5%BB%BA/"},{"categories":["Linux多进程开发"],"content":"01进程创建 系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建子进程,形成进程树结构模型 #include \u003csys/types.h\u003e #include \u003cunistd.h\u003e pid_t fork(void); 返回值: 成功:子进程中返回0,父进程中返回子进程ID 失败:返回-1; 失败原因: 1.当前系统的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN 2.系统内存不足,这时errno的值为ENOMEM ","date":"2023-02-05","objectID":"/%E8%BF%9B%E7%A8%8B%E5%88%9B%E5%BB%BA/:1:0","tags":["Linux多进程开发"],"title":"进程创建","uri":"/%E8%BF%9B%E7%A8%8B%E5%88%9B%E5%BB%BA/"},{"categories":["Linux多进程开发"],"content":"fork()进程创建总结 首先父进程执行到fork的时候会创建子进程,fork后会给父子进程分别返回一个pid号(父进程fork后返回的pid是子进程的pid,子进程的pid为0),此时系统会将父进程的用户区数据和内核数据区拷贝过来生成一个虚拟地址空间供子进程使用,之后,如果没有执行写操作的时候,父子进程共同指向一个虚拟地址空间,但是一旦发生内存写操作,就会生成新的内存空间将父子进程的变量分开,防止内存碰撞。 ","date":"2023-02-05","objectID":"/%E8%BF%9B%E7%A8%8B%E5%88%9B%E5%BB%BA/:2:0","tags":["Linux多进程开发"],"title":"进程创建","uri":"/%E8%BF%9B%E7%A8%8B%E5%88%9B%E5%BB%BA/"},{"categories":["Linux多进程开发"],"content":"GDB多进程调试 使用GDB调试的时候,GDB默认只能跟踪一个进程,可以再fork函数调用之前,通过指令设置GDB调试工具跟踪父进程或者跟踪子进程,默认跟踪父进程 设置调试父进程或者子进程: set follow-fork–mode [parent (默认)|child] 设置调试模式:set detach-on-fork [on|off] 默认为on,表示调试当前进程的时候,其他的进程可以运行,如果为off,调试当前进程的时候,其他进程会被GDB挂起 查看调试进程:info inferiors 切换当前调试进程: inferior id 使进程脱离GDB调试:detach inferiors id ","date":"2023-02-04","objectID":"/gdb%E5%A4%9A%E7%BA%BF%E7%A8%8B%E8%B0%83%E8%AF%95/:0:0","tags":["Linux多进程开发"],"title":"GDB多线程调试","uri":"/gdb%E5%A4%9A%E7%BA%BF%E7%A8%8B%E8%B0%83%E8%AF%95/"},{"categories":["Linux编程入门"],"content":"1.什么是GDB ","date":"2023-02-04","objectID":"/gdb/:0:0","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"GDB是由GNU软件系统社区提供的调试工具,同GCC配套组成的一套完整的开发环境,GDB是Linux和许多类Unix系统中的标准开发环境。 一般来说,GDB主要帮助你完成下面四个方面的功能: 1.启动程序,可以按照自定义的要求随心所欲的允许程序 2.可让被调试的程序在所指定的调置的断点处停住 3.可程序被停止住,可以检查此程序中所发生的事 4,可以改变程序,讲一个BUG产生的影响修正从而测试其他BUG ","date":"2023-02-04","objectID":"/gdb/:1:0","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"准备工作 通常,在为调试而编译时,我们会关掉编译器的优化选项(’-o’),并打开调试选项(’-g’)。另外,’-Wall’在尽量不影响程序行为的情况下选项打开所有的warning,也可以发现许多问题,避免一些不必要的BUG gcc -g -Wall program.c -o program ‘-g’ 选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证gdb能找到源文件 ","date":"2023-02-04","objectID":"/gdb/:2:0","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"命令 ","date":"2023-02-04","objectID":"/gdb/:3:0","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"启动和退出 gdb 可执行程序 quit ","date":"2023-02-04","objectID":"/gdb/:3:1","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"给程序设置参数/获取设置参数 set args 10 20 show args ","date":"2023-02-04","objectID":"/gdb/:3:2","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"GDB使用帮助 help ","date":"2023-02-04","objectID":"/gdb/:3:3","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"常看当前文件代码 list/l (从默认位置显示) list/l 行号 (从制定的行显示) list/l 函数名(从指定的函数显示) ","date":"2023-02-04","objectID":"/gdb/:3:4","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"查看非当前文件的代码 list/l 文件名:行号 list/l 文件名:函数名 ","date":"2023-02-04","objectID":"/gdb/:3:5","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"设置显示的行数 show list/listsize set list/listsize 行数 ","date":"2023-02-04","objectID":"/gdb/:3:6","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"GDB命令–断点操作 ","date":"2023-02-04","objectID":"/gdb/:4:0","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"设置断点 b/break 行号 b/break 函数名 b/break 文件名:行号 b/break 文件名:函数 ","date":"2023-02-04","objectID":"/gdb/:4:1","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"查看断点 i/info b/break ","date":"2023-02-04","objectID":"/gdb/:4:2","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"删除断点 d/del/delete 断点编号 ","date":"2023-02-04","objectID":"/gdb/:4:3","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"设置断点无效 dis/disable 断点编号 ","date":"2023-02-04","objectID":"/gdb/:4:4","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"断点生效 ena/enable 断点编号 ","date":"2023-02-04","objectID":"/gdb/:4:5","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"设置条件断点(一般用在循环的位置) b/break 10 if i==5 ","date":"2023-02-04","objectID":"/gdb/:4:6","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"GDB命令–调试命令 ","date":"2023-02-04","objectID":"/gdb/:5:0","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"允许GDB程序 start (程序停在第一行) run (遇到断点才停) ","date":"2023-02-04","objectID":"/gdb/:5:1","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"继续允许,到下一个断点停 c/continue ","date":"2023-02-04","objectID":"/gdb/:5:2","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"向下执行一行代码(不会进入函数体) n/next ","date":"2023-02-04","objectID":"/gdb/:5:3","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"变量操作 p/print 变量名 (打印变量值) ptype 变量名 (打印变量类型) ","date":"2023-02-04","objectID":"/gdb/:5:4","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"向下单步调试(遇到函数进入函数体) s/step finish(跳出函数体) ","date":"2023-02-04","objectID":"/gdb/:5:5","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"自动变量操作 display num(自动打印制定变量的值) i/info display undisplay 编号 ","date":"2023-02-04","objectID":"/gdb/:5:6","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"其他操作 set var 变量名=变量值 until (跳出循环) ","date":"2023-02-04","objectID":"/gdb/:5:7","tags":["Linux编程入门"],"title":"GDB调试","uri":"/gdb/"},{"categories":["Linux编程入门"],"content":"什么是Makefile 1.一个工程中的源码不计其数,其按照类型,功能,模块分别放在若干个目录中,Makefile文件定义了一系列的规则来制定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为Makefile文件就像个Shell脚本一样,也可以执行操作系统命令 2.Makefile带来的好处就是’自动化编译’,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率,make是一个命令工具,是一个解释Makefile文件中指令的命令工具,一般来说,大多数的IDE都有这个命令 Makefile文件命名和规则 ","date":"2023-02-03","objectID":"/makefile/:0:0","tags":["Linux编程入门"],"title":"Makefile","uri":"/makefile/"},{"categories":["Linux编程入门"],"content":"文件命名 makefile 或者Makefile ","date":"2023-02-03","objectID":"/makefile/:1:0","tags":["Linux编程入门"],"title":"Makefile","uri":"/makefile/"},{"categories":["Linux编程入门"],"content":"Makefile规则 一个Makefile文件中可以有一个或者多个规则 目标 …:依赖 … 命令(Shell 命令) … 目标:最终要生成的文件(伪目标除外) 依赖:生成目标所需要的文件或者目标 命令:通过执行命令对依赖操作生成目标(命令前必须Tal缩进) 工作原理 1.命令在执行之前,需要先检查规则中的依赖是否存在 1.1 如果存在 则执行命令 1.2 如果不存在,向下检查其他规则,检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该条规则的命令 2.检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间 2.1 如果依赖的时间比目标的时间晚,则需要重新生成目标 2.2 如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被执行 变量 1.自定义变量 变量名=变量值 var=hello 2.预定义变量 AR : 归档维护程序的名称 默认值是ar CC : C编译器的名称 默认值是cc CXX : C++编译器的名称,默认值是g++ $@ : 目标的完整名称 $\u003c : 第一个依赖文件的名称 $^ : 所有的依赖文件 3.获取变量的值 $(变量名) 模式匹配 %.o:%.c -%:通配符,匹配一个字符串 两个%匹配的是同一个字符串 %.c:%.c gcc -c %\u003c -o $@ 函数1 $(wilecard PATTERN..) 功能:获取制定目录下制定类型的文件列表 参数:PATTERN 指的是某个获多个目录下的对应的某种类型的文件,如果有多个目录,一般使用空格间隔 返回:得到的若干个文件的文件列表,文件名之间使用空格间隔 例子:$(wildcard .c ./sub/.c) 返回值格式: a.c b.c…. 函数2 $(patsubst ,,) 功能:查找中的单词(单词以’空格’,‘Tab’或‘回车’‘换行‘分割)是否符合模式,如果匹配的话,则以替换 可以包括通配符’%’,表示任意长度的字符串,如果中也包含’%’,那么中的这个’%‘将是中的那个’%‘所代表的的字符串 返回:函数返回被替换过后的字符号串 例子:$(patsubst %.c,%.o,x.c bar.c) 返回格式:x.o bar.o ","date":"2023-02-03","objectID":"/makefile/:2:0","tags":["Linux编程入门"],"title":"Makefile","uri":"/makefile/"},{"categories":["Linux编程入门"],"content":"01动态库的制作 命名规则 Linux:libxxx.so lib:前缀(固定) xxx:库的名字 自己起 .so:后缀(固定) 在Linux下是一个可执行文件 Windows:libxxx.dll 02动态库的制作: gcc得到.o文件,得到和位置无关的代码 gcc -c -fpic/-fPIC a.c b.c gcc得到动态库 gcc -shared a.o b.o -o libcalc.so 03工作原理 ","date":"2023-02-02","objectID":"/%E5%8A%A8%E6%80%81%E5%BA%93/:0:0","tags":["Linux编程入门"],"title":"动态库","uri":"/%E5%8A%A8%E6%80%81%E5%BA%93/"},{"categories":["Linux编程入门"],"content":"静态库: gcc进行连接时,会把静态库中的代码打包到可执行程序中 ","date":"2023-02-02","objectID":"/%E5%8A%A8%E6%80%81%E5%BA%93/:1:0","tags":["Linux编程入门"],"title":"动态库","uri":"/%E5%8A%A8%E6%80%81%E5%BA%93/"},{"categories":["Linux编程入门"],"content":"动态库: gcc进行连接时,动态库的代码不会被打包到可执行程序中 程序启动之后,动态库会被动态加载到内存中,通过ldd(list dynamic dependencies)命令检查动态库依赖关系 ","date":"2023-02-02","objectID":"/%E5%8A%A8%E6%80%81%E5%BA%93/:2:0","tags":["Linux编程入门"],"title":"动态库","uri":"/%E5%8A%A8%E6%80%81%E5%BA%93/"},{"categories":["Linux编程入门"],"content":"04如何定位共享库文件呢? 当系统加载可执行代码时,能够知道其所依赖的库的名字,但是还需要知道绝对路径,此时就需要 系统的动态载入器获取该绝对路径。对于elf格式的可执行程序,是由ld-linux.so来完成的, 它先后搜索elf文件的 DT_RPATH段–\u003e环境变量LD_LIBRARY_PATH–\u003e/etc/ld.so.cache文件列表 –\u003e/lib/./usr/lib目录找到库文件后将其载入到内存 ","date":"2023-02-02","objectID":"/%E5%8A%A8%E6%80%81%E5%BA%93/:3:0","tags":["Linux编程入门"],"title":"动态库","uri":"/%E5%8A%A8%E6%80%81%E5%BA%93/"},{"categories":["Linux编程入门"],"content":"05解决动态库加载失败的问题 只需要在上述的环境变量LD_LIBRARY_PATH或者/etc/ld.so.cache文件列表 或者/lib/./usr/lib目录内,添加需要使用的动态库绝对路径即可 ","date":"2023-02-02","objectID":"/%E5%8A%A8%E6%80%81%E5%BA%93/:4:0","tags":["Linux编程入门"],"title":"动态库","uri":"/%E5%8A%A8%E6%80%81%E5%BA%93/"},{"categories":["Linux编程入门"],"content":"1.在PATH内添加LD_LIBRARY_PATH即可 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:[动态库的绝对路径] 但是由于配置的环境变量是临时的,终端关掉之后再打开就会失效,需要按照用户级别或者root级别配置 ","date":"2023-02-02","objectID":"/%E5%8A%A8%E6%80%81%E5%BA%93/:4:1","tags":["Linux编程入门"],"title":"动态库","uri":"/%E5%8A%A8%E6%80%81%E5%BA%93/"},{"categories":["Linux编程入门"],"content":"2.用户级别配置 在Home下使用vim .bashrc 在最底下添加 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:[动态库的绝对路径] 然后输入 source .bashrc或者 . .bashrc ","date":"2023-02-02","objectID":"/%E5%8A%A8%E6%80%81%E5%BA%93/:4:2","tags":["Linux编程入门"],"title":"动态库","uri":"/%E5%8A%A8%E6%80%81%E5%BA%93/"},{"categories":["Linux编程入门"],"content":"3.系统级别的配置 sudo vim /etc/profile 在最后一行插入export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:[动态库的绝对路径] 记得刷新profile ","date":"2023-02-02","objectID":"/%E5%8A%A8%E6%80%81%E5%BA%93/:4:3","tags":["Linux编程入门"],"title":"动态库","uri":"/%E5%8A%A8%E6%80%81%E5%BA%93/"},{"categories":["Linux编程入门"],"content":" 01什么是库 1.库文件是计算机上的一类文件,可以简单的把库文件看成代码仓库,它提供给使用者一些可以直接拿来用的变量,函数,类 2.库是特殊的程序,编写库的程序和编写一般的程序区别不大,只是库不能单独允许 3.库文件有两种,静态库和动态库(共享库),区别是:静态库在程序的连接阶段被复制到了程序中,动态库在连接阶段没有被复制到程序中,而是在程序运行时由系统加载到内存中供程序使用 4.库的好处:1.代码保密 2.方便部署和分发 02静态库的制作 ","date":"2023-02-02","objectID":"/%E9%9D%99%E6%80%81%E5%BA%93/:0:0","tags":["Linux编程入门"],"title":"静态库","uri":"/%E9%9D%99%E6%80%81%E5%BA%93/"},{"categories":["Linux编程入门"],"content":"命名规则; Linux;libxxx.a lib:前缀(固定) xxx:库的名字,自己起 .a :后缀(固定) Window;libxxx.lib 静态库的制作: 1.gcc获得.o文件 2.将.o文件打包。使用 ar工具(archive) ar rcs libxxx.a xxx.o xxx.o r - 将文件插入备存文件中 c - 简历备存文件 s - 索引 ","date":"2023-02-02","objectID":"/%E9%9D%99%E6%80%81%E5%BA%93/:1:0","tags":["Linux编程入门"],"title":"静态库","uri":"/%E9%9D%99%E6%80%81%E5%BA%93/"},{"categories":["Linux编程入门"],"content":" 什么是GCC 1.GCC原名为GUN C语言编译器 2.GCC是由GNU开发的编程语言编译器 3.GCC不仅支持C的多种方言,也可以区别不同C的标准,可以使用命令行选项控制编译器在翻译源码时应该遵守那个C标准。例如,当使用命令行‘-std=99’启动GCC时,编译器支持C99标准 4.安装命令 sudo apt install gcc g++ 5.查看版本 gcc/g++ -v/–version GCC工作流程 源代码-\u003e预处理后源代码-\u003e汇编代码-\u003e目标代码-\u003e连接并输出 .h—– .i ———–.s .cpp .c 指令 gcc编译选项 说明 -E 预处理制定的源文件,不进行编译 -s 编译制定的源文件,不进行汇编 -c 编译,汇编制定的源文件,但是不进行连接 -o [file1][file2] / [file2] -o [file1] 将文件file2编译成可执行文件file1 -I directory 制定include包含文件的搜索目录 -g 在编译的时候,生成调试信息,该程序可以被调试器调试 -D 在程序编译的时候,制定一个宏 -w 不生成任何警告 -Wal 生成警告信息 -On n的取值范围:0-3 编译器的优化选项的4个级别 -l 在程序编译的时候,使用指定的库 -L 指定编译的时候,搜索的库的路径 -fPIC/fpic 生成与位置无关的代码 -shared 生成共享目标文件,通常用在建立共享库时 -std 制定C方言:如:-std=c99 gcc和g++ ","date":"2023-02-01","objectID":"/gcc/:0:0","tags":["Linux编程入门"],"title":"GCC基础","uri":"/gcc/"},{"categories":["Linux编程入门"],"content":"gcc 和 g++都是GNU的一个编译器 误区1:gcc只能编译c代码,g++只能编译C++代码。两者都可以 1.1后缀为.c的,gcc会将它当成c程序,而g++当做c++程序 1.2后缀为.cpp的 两则都会认为是c++程序,c++的语法规则更加严谨一些 1.3编译阶段,g++会调用gcc,对c++代码,两者是等价的,但是因为gcc命令 不能自动和C++程序使用的库连接,所以通常用g++完成连接 ","date":"2023-02-01","objectID":"/gcc/:1:0","tags":["Linux编程入门"],"title":"GCC基础","uri":"/gcc/"},{"categories":["Linux编程入门"],"content":"误区2:gcc不会定义__cplusplus宏,而g++会 2.1 实际上,这个宏只是标志编译器将会把代码按照c还是c++语法解释 ","date":"2023-02-01","objectID":"/gcc/:2:0","tags":["Linux编程入门"],"title":"GCC基础","uri":"/gcc/"},{"categories":["Linux编程入门"],"content":"误区3:编译只能用gcc,连接只能用g++ 3.1:严格来说,这句话不算错误,但是它会混淆了概念,应该这样说:编译可以用gcc/g++,而连接可以用g++或者gcc -lstdc++ 3.2:gcc命令不能自动和C++程序使用的库连接在一起,所以通常会使用g++来完成连接。但是在编译阶段,g++会自动调用gcc,二者等价 ","date":"2023-02-01","objectID":"/gcc/:3:0","tags":["Linux编程入门"],"title":"GCC基础","uri":"/gcc/"},{"categories":["区域半径协议"],"content":"ZRP(区域半径协议)详解 ","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:0:0","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"1.默认配置参数Constants.h #ifndef _constants_h_ #define _constants_h_ // General OR Common Constants //DEBUG 参数 #define DEBUG 0 // 1 = Enabled; 0 = Disabled //Router_PORT #define ROUTER_PORT 0xff #define TRUE 1 #define FALSE 0 #define LINKUP 1 #define LINKDOWN 0 #define DEFAULT_STARTUP_JITTER 1 // 1sec #define DEFAULT_EXPIRATION_CHECK_PERIOD 1 // 1sec... #define MAX_SEQUENCE_ID 1000000 // For Packet Utils #define MIN_PACKET_DROP_TIME 3 // 3 sec For 1-hop communication [Not Used] #define MAX_PACKET_DROP_TIME 10 // 10 sec For multi-hop communication [Not Used] #define IERP_TTL 50 // Maximum-length for IERP route // NDP related Constants #define DEFAULT_BEACON_PERIOD 3 // 3 sec #define DEFAULT_BEACON_PERIOD_JITTER 1 // 1 sec #define DEFAULT_NEIGHBOR_ACK_TIMEOUT 2 // 2 sec [Within this much time ACK should come to me] #define DEFAULT_MAX_ACK_TIMEOUT 2 // How many ACK Timeout is needed to declare a Neighbor DOWN // IARP related Constants #define DEFAULT_MIN_IARP_UPDATE_PERIOD 3 // 3 sec [T_lsu] #define DEFAULT_MAX_IARP_UPDATE_PERIOD 10 // 10 sec [T_lsu] #define DEFAULT_LINK_LIFETIME 10 // 10sec #define DEFAULT_UPDATE_LIFETIME 30 // 30sec [For Control Flooding] // IERP related Constants #define DEFAULT_BRP_XMIT_POLICY 0 // 1: BRP_MULTICAST, 0: BRP_UNICAST #define IERP_REPLY_SNOOP 1 // 1: Enabled, 0: Disabled #define IERP_ERROR_SNOOP 1 // 1: Enabled, 0: Disabled #define IERP_XMIT_JITTER 1 // 1sec [Uniformly distributed] #define DEFAULT_QUERY_LIFETIME 30 // 30sec [For Control Flooding] #define DEFAULT_QUERY_RETRY_TIME 5 // 5sec #define DEFAULT_ROUTE_LIFETIME 120 // 120sec [IERP Route Reliability] #define DEFAULT_MAX_IERP_REPLY 3 // Destination can send maximum 5 replies... // ZRP related Constants #define ZRP_DEFAULT_HDR_LEN 10 // 10 bytes [refer to hdr_zrp struct] #define DEFAULT_ZONE_RADIUS 2 #define DEFAULT_SEND_BUFFER_SIZE 100 // 100 Packets #define DEFAULT_PACKET_LIFETIME 20 // 10 sec #define DEFAULT_INTERPKT_JITTER 1 // 1sec [Uniformly distributed] // Type-Defintions \u0026 Enumeration... typedef double Time; typedef int32_t Query_ID; // Types of ZRP packets... enum ZRPTYPE { NDP_BEACON, NDP_BEACON_ACK, IARP_UPDATE, IARP_DATA, IERP_REPLY, IERP_REQUEST, IERP_ROUTE_ERROR, IERP_DATA }; // BRP Xmit Policy... enum BRP_XMIT_POLICY { BRP_UNICAST, BRP_MULTICAST }; #endif ","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:1:0","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"2.NDP(邻居发现协议) ","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:2:0","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"2.1 NDPAgent的类 /* [SUB-SECTION-2.4]------------------------------------------------------------ * NDP AGENT:- NEIGHBOR DISCOVERY AGENT * NDP AGENT:- 邻居发现代理类 * ----------------------------------------------------------------------------- */ /*! \\class NDPAgent \\brief This class implements the Neighbor Discovery Protocol (NDP). */ class NDPAgent { public: // Data... ZRPAgent *agent_; // 指向ZRPAgent的指针 NeighborList neighborLst_; // 存储邻居信息的邻居链表 NDPBeaconTransmitTimer BeaconTransmitTimer_; // 发送邻居探索包的类 NDPAckTimer AckTimer_; // 发送包等待邻居接受并返回ACK的类,如果邻居超过时间没有返回ACK则处理邻居 int startup_jitter_; // 启动的时间 // 构造方法... /*! \\fn NDPAgent(ZRPAgent *) This constructor is called by the constructor of ZRP. It initializes different compenents in NDP. 这个构造方法在ZRP的构造方法中被调用。 */ NDPAgent(ZRPAgent *agent) : agent_(agent), BeaconTransmitTimer_(agent), AckTimer_(agent), startup_jitter_(DEFAULT_STARTUP_JITTER) {} // Methods... /*! \\fn void startUp() This is called by ZRPAgent::startUp() method. It does following tasks: \\n - Starts the Beacon-Transmit-Timer. \\n - Clears the Neighbor-List. 这个方法在ZRPAgent类的StartUp方法中被调用 并执行开启Beacon-Transmit-Timer 以及清空邻居节点链路表 */ void startUp(); /*! \\fn void recv_NDP_BEACON(Packet* p) This function is called whenever ZRP receives an NDP_BEACON packet. On receving 'NDP_BEACON', node sends 'NDP_BEACON_ACK' to the sender. But it does not add this neighbor, because node stores only symmetric links. 每当当前节点的ZRP代理人接收到NDP_BEACON packet,就会想发送这个NDP_BEACON packet的节点 发送NDP_ACK packet */ void recv_NDP_BEACON(Packet* p); /*! \\fn void recv_NDP_BEACON_ACK(Packet* p) This function is called whenever ZRP receives an NDP_BEACON_ACK packet. \\n - If it's a new neighbor, node adds the neighbor to its 'NEIGHBOR_TABLE' and notifies IARP to rebuild the 'INNER_ROUTING_TABLE' and 'PERIPHERAL_NODE_TABLE'. \\n - If it's an existing neighbor, it updates 'LAST_ACK_TIME' for that neighbor. 每当当前节点的ZRP代理类接收到NDP_BEACON_ACK packet的时候,就会判断当前发送这个包的节点 是不是新邻居节点,如果是新邻居节点,就将它加入到NEIGHBOR_TABLE邻居表中,并通知IARPAgent 代理人去重构它的INNER_ROUTING_TABLE和PERIPHERAL_NODE_TABLE。 如果是已经存在的邻居节点,则更新他的LAST_ACK_TIME */ void recv_NDP_BEACON_ACK(Packet* p); /*! \\fn void print_tables() This function prints the Neighbor-List. It also prints which Neighbors are UP and which are DOWN. 这个方法打印邻居节点的链表,并且哪个邻居节点处于linked-up,哪个节点处于linked-down */ void print_tables(); }; ","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:2:1","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"2.1.1 NDPAgent类内成员NeighborList类(邻居节点表类) 该表维护一个NeighborList(邻居节点表) /*! \\class NeighborList \\brief This class is the linked-list of Neighbor class. */ class NeighborList { public: Neighbor *head_; // 指向链表头部的指针 int numNeighbors_; // 链表内成员数量 // 构造方法... NeighborList() : head_(NULL), numNeighbors_(0) {} // 方法... // 将邻居节点加入 void addNeighbor(nsaddr_t addr, Time lastack, int linkStatus); // 查找 int findNeighbor(nsaddr_t addr, Neighbor **handle); // 判空 int isEmpty(); // 移除 void removeNeighbor(Neighbor *prev, Neighbor *toBeDeleted); // 移除所有链路状况为down的邻居节点 void purgeDownNeighbors(); // 清空链表 void freeList(); // 打印链表 void printNeighbors(); }; 2.1.1.1 NeighborList类内的成员类Neighbor类 /* * 该类存储邻居节点的详细信息 */ class Neighbor { public: nsaddr_t addr_; // 32 位的地址 Time lastack_; // 上一次接收到ACK的时间 int linkStatus_; // 链路状况 int AckTOCount_; // 超时的次数 Neighbor *next_; // next指针 // 构造方法... Neighbor() : addr_(-1), lastack_(-1), linkStatus_(LINKDOWN), next_(NULL) {} // Initialize with Invalid Entries Neighbor(nsaddr_t addr, Time lastack, int linkStatus) : addr_(addr), lastack_(lastack), linkStatus_(linkStatus), AckTOCount_(0), next_(NULL) {} }; 2.1.1.2 NeighborList类的addNeighbor()的实现 /* * 将单个邻居节点加入邻居链表中 * nsaddr_t addr: 32 位地址 * Time lastack: 接收ACK的时间 * int linkStatus: 链路状态 */ void NeighborList::addNeighbor(nsaddr_t addr, Time lastack, int linkStatus) { //创建新的邻居节点 Neighbor *newNb = new Neighbor(addr, lastack, linkStatus); if(newNb == NULL) { // 检查创建情况 printf(\"### Memory Allocation Error in [NeighborList::addNeighbor] ###\"); exit(0); } newNb-\u003enext_ = head_; // 头插法插入 head_ = newNb; numNeighbors_++; // ++链表内节点数量 } 2.1.1.3 NeighborList类的findNeighbor()的实现 /* * 查找邻居链表内的是否存在当前邻居节点 * 如果有 返回true 并且将其赋值给handle * 如果没有 返回false * nsaddr_t addr : 32位地址 * Neighbor **handle : handle指针 */ int NeighborList::findNeighbor(nsaddr_t addr, Neighbor **handle) { // 定义指向NeighborList的head的临时指针 Neighbor *cur = NULL; // 查找的返回值 int foundFlag; // 小心空链表 // [i\u003cnumNeighbors_] condition takes care for EMPTY List case... cur=head_;//头节点赋值给临时变量 foundFlag=FALSE;//先赋值为false //遍历循环查找节点 for(int i=0; i\u003cnumNeighbors_; i++) { if(addr == cur-\u003eaddr_) { foundFlag = TRUE; break; } cur=cur-\u003enext_; } if(foundFlag) { // 如果邻居节点存在 *handle = cur; // 将该邻居节点的地址赋值给handle return TRUE; // 返回true } return FALSE; // 邻居未找到 返回false } 2.1.1.4 NeighborList类的isEmpty()实现 /* * 如果为空返回true * 如果为空返回false */ int NeighborList::isEmpty() { if(numNeighbors_ == 0) { return TRUE; } else { return FALSE; } } 2.1.1.5 NeighborList类的removeNeighbor()的实现 /* Remove a Neighbor specified by the pointer in argument. * 移除参数中指定的邻居节点 * Neighbor *prev: 被删除节点的前置节点 * Neighbor *toBeDeleted: 被删除节点 */ void NeighborList::removeNeighbor(Neighbor *prev, Neighbor *toBeDeleted) { // [Case-1]: This is a head that needs to be deleted // 如果前置节点为空,并且被删除的节点不是头节点,则报错退出 if(prev == NULL ) { if(toBeDeleted != head_) { printf(\"### Logical Error in [NeighborList::removeNeighbor] ###\"); exit(0); } //否则,删除节点就是头节点 head_ = head_-\u003enext_; // 让head指针 指向head的next指针 delete toBeDeleted; // 删除节点空间 numNeighbors_--; //邻居表内成员-- return; } // [Case-2]: 通常的case prev-\u003enext_ = toBeDeleted-\u003enext_; // 前节点的next指向删除节点的next delete toBeDeleted; //删除想要删除的节点 numNeighbors_--; //邻居表内成员-- } 2.1.1.6 NeighborList类的purgeDownNeighbors()的实现 /* Remove all Neighbors for which the linkStatus_ is DOWN. * 删除所有链路状态位DOWN的节点 */ void NeighborList::purgeDownNeighbors() { //如果邻居表为空 if(numNeighbors_ == 0) { return; // Nothing to do } // [case-1]: Leaving the 1st case(head_ case) // Neighbor *prev, *cur, *toBeDeleted; prev = head_; cur = head_-\u003enext_; //循环遍历,只要下一个节点不为空 for(;cur!=NULL;) { // 如果下一个节点的链路状态位LINKDOWN if(cur-\u003elinkStatus_ == LINKDOWN) { // Delete the Neighbor prev-\u003enext_ = cur-\u003enext_;//前置节点的next指向被删除节点的next toBeDeleted = cur;//cur赋值给需要删除的节点的指针 cur = cur-\u003enext_;//cur指向下一个节点 delete toBeDeleted;//删除被删除节点的空间 numNeighbors_--;//邻居表内成员-- } else { prev = cur; // 前节点等于下一个节点 cur = cur-\u003enext_;//下一个节点指向下一个节点的next } } // [case-2]: head_ case... // 头节点特判 if(head_!=NULL) { //如果头节点为LINKDOWN if(head_-\u003elinkStatus_ == LINKDOWN) { toB","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:2:2","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"2.1.2 NDPAgent类内成员NDPBeaconTransmitTimer类 该类定时发送邻居节点的探测包 /* [SUB-SECTION-2.2]------------------------------------------------------------ * BEACON-TRANSMIT TIMER:- PERIODIC BEACON XMISSION * ----------------------------------------------------------------------------- */ /*! \\class NDPBeaconTransmitTimer \\brief This class implements a BEACON Transmit Timer, \\who periodically sends beacons to NEIGHBOR nodes. */ class NDPBeaconTransmitTimer : public Handler { public: // Data... ZRPAgent *agent_; // 指向ZRPAgent的指针 Event intr_; int beacon_period_; // 发包时间间隔 int beacon_period_jitter_; // Jitter added to Inter-Beacon-Time int neighbor_ack_timeout_; // Only for Logging为了日志打印 // 构造器... NDPBeaconTransmitTimer(ZRPAgent* agent) : agent_(agent), beacon_period_(DEFAULT_BEACON_PERIOD), beacon_period_jitter_(DEFAULT_BEACON_PERIOD_JITTER), neighbor_ack_timeout_(DEFAULT_NEIGHBOR_ACK_TIMEOUT) {} void handle(Event*); // function handling the event void start(double thistime); }; 2.1.2.1 NDPBeaconTransmitTimer类的start()的实现 /* Starts BeaconTransmitTimer, called by startUp(), delayed by 'thistime'. * 被NDPAgent的startUp方法调用 * thistime:为延迟时间 */ void NDPBeaconTransmitTimer::start(double thistime) { //将其提交给ns2的Scheduler去计划启动 Scheduler::instance().schedule(this, \u0026intr_, thistime ); } 2.1.2.1 NDPBeaconTransmitTimer类的handle()的实现 /* Broadcasts a Beacon. * 广播邻居查找 */ void NDPBeaconTransmitTimer::handle(Event* e) { // [Task-1]: 创建一个广播信标 // 包的指针 Packet* p = NULL; //调用方法创建一个NDP_BEACON,IP为IP_BROADCAST,TIME TO LIVE = 1 p = (agent_-\u003epktUtil_).pkt_create(NDP_BEACON, IP_BROADCAST, 1); // last arg is TTL=1 //调用pktUtil_的广播方法 (agent_-\u003epktUtil_).pkt_broadcast(p, 0.00); // broadcast pkt if(DEBUG) { // [Log: XMIT_NDP_BEACON] Time now = Scheduler::instance().clock(); // get the time hdr_zrp *hdrz = HDR_ZRP(p); // access ZRP part of pkt header printf(\"\\n_%d_ [%6.6f] | XMIT_NDP_BEACON | -S %d -Nb BROADCAST | -SEQ %d | \", agent_-\u003emyaddr_, now, hdrz-\u003esrc_, hdrz-\u003eseq_); agent_-\u003eprint_tables(); printf(\"\\n\"); } // [Log: End] // [Task-2]: 开启ACKTimer (agent_-\u003endpAgt_).AckTimer_.start(); // [Task-3]: Schedule next scan in BEACON_PERIOD + Jitter sec // 将其交给Schedule,延迟时间BEACON_PERIOD + Jitter sec之后再次调用 Scheduler::instance().schedule(this, \u0026intr_, beacon_period_ + Random::uniform(beacon_period_jitter_)); } ","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:2:3","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"2.1.3 NDPAgent类内成员NDPAckTimer类 /* [SUB-SECTION-2.3]------------------------------------------------------------ * ACK-TIMEOUT TIMER:- ACK-TIMEOUT CHECKING * ----------------------------------------------------------------------------- */ /*! \\class NDPAckTimer \\brief This class implements a Acknoledgement TimeOut Timer, who checks that ACK is received or not for sent beacon. \\该类为一个超时类,检查有没有接收到ACK */ class NDPAckTimer : public Handler { public: ZRPAgent *agent_; // 指向ZRP-Agent的指针 Event intr_; int neighbor_ack_timeout_; // ACK送信超时时间 // Constructor... NDPAckTimer(ZRPAgent* agent) : agent_(agent), neighbor_ack_timeout_(DEFAULT_NEIGHBOR_ACK_TIMEOUT) {} void handle(Event*); // function handling the event void start(); }; 2.1.3.1 NDPAckTimer类方法start()的实现 /* [SUB-SECTION-1.3]-------------------------------------------------------------------------------------------------------- * ACK-TIMEOUT TIMER:- ACK-TIMEOUT CHECKING * ------------------------------------------------------------------------------------------------------------------------- */ /* Schedule timeout at neighbor_ack_timeout_, * just after sending a Beacon. * 在发送NDP Beacon之后启动 */ void NDPAckTimer::start() { Scheduler::instance().schedule(this, \u0026intr_, neighbor_ack_timeout_); } 2.1.3.2 NDPAckTimer类方法handle()的实现 /* * Mark LINKDOWN for un-ACKed neighbors. * If any of them found DOWN then notify IARP to rebuild routing table. * 为未确认ACK的邻居设置LinkStatus为LinkDOWN * 一旦发现LinkDown的邻居节点,就通知IARP重新创建路径表 */ void NDPAckTimer::handle(Event* e) { // [Task-1]: Check if Neighbor Table is Empty... //检查邻居链表是否为空 if((agent_-\u003endpAgt_).neighborLst_.isEmpty()) { if(DEBUG) { // [Log: NDP_ISOLATED_NODE ] Time now = Scheduler::instance().clock(); // get the time printf(\"\\n_%d_ [%6.6f] | NDP_ISOLATED_NODE | No NDP_BEACON_ACK Detected | \", agent_-\u003emyaddr_, now); agent_-\u003eprint_tables(); printf(\"\\n\"); } // [Log: End] return;//什么都不做 } // [Task-2]: Drop timed-out neighbors \u0026 inform IARP about it... // 删除超时的邻居节点,并且通知IARP Time now = Scheduler::instance().clock(); // 获取时间 Neighbor *prev, *curNb; //定义存储邻居节点的前置指针和临时指针 prev = NULL; // 必须置为 NULL //指向当前ZRPAgent的NDPAgent下的邻居表的头节点 curNb = (agent_-\u003endpAgt_).neighborLst_.head_; //循环检查链表内邻居节点的ACK时间 for(; curNb!=NULL; ) { // [SubTask-2.1]: Check for Ack Timeout of Neighbor // 如果当前时间-邻居节点的最近一次时间超过Ack超时时间 if((now - curNb-\u003elastack_) \u003e neighbor_ack_timeout_) { // 检查当前节点的超时次数 // [SubTask-2.2]: Check for How many TimeOuts have occured with this Neighbor //超时次数先++ curNb-\u003eAckTOCount_++; //检查是否超多最大超时次数 if(curNb-\u003eAckTOCount_ \u003e= DEFAULT_MAX_ACK_TIMEOUT) { // 将该节点的LinKStates设置为DOWN,并且通知IARP // [SubTask-2.3]: Detected a DOWN Neighbor - Notify IARP \u0026 Take appropriate actions //将IARP内部的updateSendFlag_设置为true,为下次更新做准备 (agent_-\u003eiarpAgt_).updateSendFlag_ = TRUE; // Set the Update at Next Timer-Event //定义LinkState的临时handleToFoundLS LinkState *handleToFoundLS = NULL; //定义查找flag int foundFlag; //从当前ZRPAgent的IARPAgent的LinKState链表内查询 foundFlag = (agent_-\u003eiarpAgt_).lsLst_.findLink(curNb-\u003eaddr_, agent_-\u003emyaddr_, \u0026handleToFoundLS); //如果查到了 if( foundFlag == TRUE ) { handleToFoundLS-\u003eisup_ = LINKDOWN; // 将link-stauts设置为DOWN (agent_-\u003eiarpAgt_).buildRoutingTable(); // 重构 Routing Table } // [SubTask-2.4]: Set the Link-Status of Neighbor to LINKDOWN... // [DOWN Neighbors are deleted after sending IARP_UPDATE - I didn't forget that :) ] //将该当邻居节点的linkStatus设置为DOWN curNb-\u003elinkStatus_ = LINKDOWN; } if(DEBUG) { // [Log: NDP_BEACON_ACK_TIMEOUT] Time now = Scheduler::instance().clock(); // get the time printf(\"\\n_%d_ [%6.6f] | NDP_BEACON_ACK_TIMEOUT | -S %d -Nb %d | -TimeOut %d | \", agent_-\u003emyaddr_, now, agent_-\u003emyaddr_, curNb-\u003eaddr_, neighbor_ack_timeout_); agent_-\u003eprint_tables(); printf(\"\\n\"); } // [Log: End] } //继续遍历 prev = curNb; // Advance the Pointers curNb=curNb-\u003enext_; } } ","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:2:4","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"2.1.4 NDPAgnet类内方法startUp() /* [SUB-SECTION-1.4]-------------------------------------------------------------------------------------------------------- * NDP AGENT:- NEIGHBOR DISCOVERY AGENT * ------------------------------------------------------------------------------------------------------------------------- */ /*开启邻居节点信标探测,清空链表 */ /*! \\fn void startUp() This is called by ZRPAgent::startUp() method. It does following tasks: \\n - Starts the Beacon-Transmit-Timer. \\n - Clears the Neighbor-List. 这个方法在ZRPAgent类的StartUp方法中被调用 并执行开启Beacon-Transmit-Timer 以及清空邻居节点链路表 */ void NDPAgent::startUp() { // [Task-1]: Start the Timers... [We donot need to start ACK timer- It is started by BTTimer] // 开启BeaconTransitTimer 我们不需要开启ACKtimer 他在BeaconTransmitTimer_之后被启动 double startUpJitter; startUpJitter = Random::uniform(startup_jitter_); BeaconTransmitTimer_.start(startUpJitter); // [Task-2]: Clear the NeighborList... // 清空邻居链表 if(!neighborLst_.isEmpty()) { neighborLst_.freeList(); } } ","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:2:5","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"2.1.5 NDPAgnet类内方法recv_NDP_BEACON(Packet* p) 在ZRPAgent::recv()方法中被使用 /* 接收到NDP_BEACON的时候,返回NDP_BEACON_ACK*/ void NDPAgent::recv_NDP_BEACON(Packet* p) { // [Assumption]:We are looking only for symmetric links. // [Assumption]:我们只寻找对称链接 // On receving NDP_BEACON, I donot consider the sender as a Neighbor. // 当接收到NDP_BEACON的时候,我们不讨论他是否是邻居 if(DEBUG) { // [Log: RECV_NDP_BEACON] Time now = Scheduler::instance().clock(); // get the time hdr_zrp *hdrz = HDR_ZRP(p); // access ZRP part of pkt header printf(\"\\n_%d_ [%6.6f] | RECV_NDP_BEACON | -S %d -Nb %d | -SEQ %d | \", agent_-\u003emyaddr_, now, hdrz-\u003esrc_, agent_-\u003emyaddr_, hdrz-\u003eseq_); agent_-\u003eprint_tables(); printf(\"\\n\"); } // [Log: End] // [Task-1]: Create an ACK packet and Send it... // 创建ACK包并且发送 Packet *pnew = NULL;//创建新的包的临时变量 hdr_zrp *hdrz = HDR_ZRP(p);//从接收到的包里分离出zrp的包头 //创建包,类型NDP_BEACON_ACK,目标为发送节点,TIME TO LIVE = 1 pnew = (agent_-\u003epktUtil_).pkt_create(NDP_BEACON_ACK, hdrz-\u003esrc_, 1); // Unicast the packet (单播包) (agent_-\u003epktUtil_).pkt_send(pnew, hdrz-\u003esrc_, 0.00); if(DEBUG) { // [Log: XMIT_NDP_BEACON_ACK] Time now = Scheduler::instance().clock(); // get the time hdr_zrp *hdrznew = HDR_ZRP(pnew); // access ZRP part of pkt header printf(\"\\n_%d_ [%6.6f] | XMIT_NDP_BEACON_ACK | -S %d -Nb %d | -SEQ %d | \", agent_-\u003emyaddr_, now, hdrznew-\u003edest_, hdrznew-\u003esrc_, hdrznew-\u003eseq_); agent_-\u003eprint_tables(); printf(\"\\n\"); } // [Log: End] // [Task-2]: Drop the NDP_BEACON just Received... //丢弃掉已经接收到的NDP_BEACON (agent_-\u003epktUtil_).pkt_drop(p); } ","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:2:6","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"2.1.6 NDPAgnet类内方法recv_NDP_BEACON_ACK(Packet* p) /* On receiving ACK, Update the Neighbor-table and IARP-(topology \u0026 routing)-table if required. */ //收到 ACK 后,如果需要,更新邻居表和 IARP(拓扑和路由)表 void NDPAgent::recv_NDP_BEACON_ACK(Packet* p) { // [Task-1]: Check if Neighbor exists - If not, add it \u0026 Notify IARP... Time now = Scheduler::instance().clock(); // 获取当前时间 hdr_zrp *hdrz = HDR_ZRP(p);// 获取ZRP的包头 Neighbor *handleToFoundNb = NULL;//定义查找邻居节点的指针 //在邻居节点内部查找该邻居是否存在 int foundNeighbor = neighborLst_.findNeighbor(hdrz-\u003esrc_, \u0026handleToFoundNb); //如果该邻居存在 if(foundNeighbor == TRUE) { if(DEBUG) { // [Log: RECV_NDP_BEACON_ACK] Time now = Scheduler::instance().clock(); // get the time printf(\"\\n_%d_ [%6.6f] | RECV_NDP_BEACON_ACK | -S %d -Nb %d | -SEQ %d | \" \"Existing Neighbor Detected | \", agent_-\u003emyaddr_, now, hdrz-\u003edest_, hdrz-\u003esrc_, hdrz-\u003eseq_); agent_-\u003eprint_tables(); printf(\"\\n\"); } // [Log: End] //将其最后一次ack设置为当前时间 handleToFoundNb-\u003elastack_ = now; //将其linkStatus_状态设置为UP handleToFoundNb-\u003elinkStatus_ = LINKUP; //将其ACK未确认次数归零 handleToFoundNb-\u003eAckTOCount_ = 0; } else { // 如果为查找到,意味着发现新邻居 if(DEBUG) { // [Log: RECV_NDP_BEACON_ACK] Time now = Scheduler::instance().clock(); // get the time printf(\"\\n_%d_ [%6.6f] | RECV_NDP_BEACON_ACK | -S %d -Nb %d | -SEQ %d | \" \"New Neighbor Detected | \", agent_-\u003emyaddr_, now, hdrz-\u003edest_, hdrz-\u003esrc_, hdrz-\u003eseq_); agent_-\u003eprint_tables(); printf(\"\\n\"); } // [Log: End] //在邻居表中添加新邻居信息 neighborLst_.addNeighbor(hdrz-\u003esrc_, now, LINKUP); // Notify IARP to send the next Update [Change of Neighbor-Table Detected] //通知IARP 在下次更新的时候更新路径表 (agent_-\u003eiarpAgt_).updateSendFlag_ = TRUE; // Set the Update at Next Timer-Event } // [Task-2]: Update IARP... // 更新IARP // 定义LinkState的临时存储指针 LinkState *handleToFoundLS = NULL; //查询Link状态 int LSFoundFlag = (agent_-\u003eiarpAgt_).lsLst_.findLink(hdrz-\u003esrc_, agent_-\u003emyaddr_, \u0026handleToFoundLS); if( LSFoundFlag == FALSE ) { //如没查询到,则加入该链路信息 (agent_-\u003eiarpAgt_).lsLst_.addLink(hdrz-\u003esrc_, agent_-\u003emyaddr_, hdrz-\u003eseq_, LINKUP, now+(agent_-\u003eiarpAgt_).linkLifeTime_); (agent_-\u003eiarpAgt_).buildRoutingTable(); // 重构 Routing Table //从 Send_Buffer 中查找有无需要发送的数据包 agent_-\u003eroute_SendBuffer_pkt(); } else { // 如果链路查找到了,更新其内属性 handleToFoundLS-\u003eisup_ = LINKUP; handleToFoundLS-\u003eexpiry_ = now+(agent_-\u003eiarpAgt_).linkLifeTime_; } // [Task-3]: 丢弃该包 (agent_-\u003epktUtil_).pkt_drop(p); } ","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:2:7","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"3. IARP(主动协议) ","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:3:0","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"3.1 IARPAgent ZRP协议内部维护区域半径的主动协议 /* [SUB-SECTION-3.7]------------------------------------------------------------ * IARP AGENT:- INTRA-ZONE ROUTING AGENT * ----------------------------------------------------------------------------- */ /*! \\class IARPAgent \\brief This class implements the IntrAzone Routing Protocol (IARP). \\该类包含了IARP协议 */ class IARPAgent { public: // Data... ZRPAgent *agent_; // 指向ZRPAgent的指针 int updateSendFlag_;// 初期化为TRUE [定期更新被发送 // 如果它为True,每次更新都被发送 // 如果它为False // 表... LinkStateList lsLst_; // 拓扑表 PeripheralNodeList pnLst_; //外围节点表 InnerRouteList irLst_; //主动路径表 IARPUpdateDetectedList upLst_; // 检测到更新缓存 int linkLifeTime_; // DEFAULT_LINK_LIFETIME int updateLifeTime_;// DEFAULT_UPDATE_LIFETIME // Timers... IARPPeriodicUpdateTimer PeriodicUpdateTimer_;//定期更新计时器 IARPExpirationTimer ExpirationTimer_; //定期判断超时器 int startup_jitter_; // For starting the Timers // 构造器... /*! \\fn IARPAgent(ZRPAgent *) 该构造函数由ZRP的构造函数调用。它初始化IARP 中的不同组件。 */ IARPAgent(ZRPAgent *agent) : agent_(agent), updateSendFlag_(TRUE), linkLifeTime_(DEFAULT_LINK_LIFETIME), updateLifeTime_(DEFAULT_UPDATE_LIFETIME), PeriodicUpdateTimer_(agent), ExpirationTimer_(agent), startup_jitter_(DEFAULT_STARTUP_JITTER) {} // 方法... /*! \\fn void startUp() 这是由 ZRPAgent::startUp() 方法调用的。它执行以下任务: \\n - 启动 Periodic-Update-Timer 和 Expiration-Timer。 - 清除 LinkState-List、PeripheralNode-List、Inner-Route-List \u0026 Detected-Update-List. */ void startUp(); // 启动Timers \u0026清空Lists(Tables) /*! \\fn void buildRoutingTable() 该函数基于创建Link-State-List(链路状况表)来创建 Inner-Route-List(内部路由路径表) \u0026 Peripheral-Node-List(外围节点表) 执行以下步骤。 -# 清空已存在的 Inner-Route-List \u0026 Peirpheral-Node-List. -# 清除 expired(过期) links \u0026 DOWN links. -# 从现有链路状态列表生成 BFS 树. -# 基于该 BFS 树生成内部路由列表。插入所有叶子外围节点列表中的节点。 */ void buildRoutingTable();// 创建基于Topology-Table 的 Routing-Table /*! \\fn void addRouteInPacket(nsaddr_t dest, Packet *p) This function adds the Inner-route for node 'dest' in packet 'p' if available. 该方法判断数据包P是否可用,如果可用,则将其添加到inner-route */ void addRouteInPacket(nsaddr_t dest, Packet *p); /*! \\fn void recv_IARP_UPDATE(Packet* p) 只要 ZRP 收到 IARP_UPDATE 数据包,就会调用此函数。 执行以下任务- - 如果是一个新的iarp_update那么, - 节点更新它自己的 'LINK_STATE_TABLE', - 如果 'LINK_STATE_TABLE' 被改变,那么, 就重构以下两个 'INNER_ROUTING_TABLE' and 'PERIPHERAL_NODE_TABLE', - 将此更新缓存在“UPDATE_DETECT_TABLE”中以控制泛洪, - 如果 TTL 不等于 0,则重新广播它. - 如果它是一个已经收到的 iarp_update 那么, - 节点丢弃此更新 */ void recv_IARP_UPDATE(Packet* p); // 接收IARP更新信息 /*! \\fn void recv_IARP_DATA(Packet* p) 只要 ZRP 收到 IARP_DATA 数据包,就会调用此函数。 执行以下任务 - - 如果该包的目标节点与当前节点相匹配,想消息发送到上一层 - 否则 - 转发该包 */ void recv_IARP_DATA(Packet* p); // Receiving IARP DATA(Local Traffic) /*! \\fn void print_tables() 该方法打印Peripheral-Node-List \u0026 Inner-Route-List. */ void print_tables(); // Print all Tables }; 3.1.1 IARPAgent类内成员LinkStateList类 //该类维护一个拓扑链表 class LinkStateList { public: // Data... LinkState *head_; //表头 int numLinks_; //内部成员数量 // 构造方法... LinkStateList() : head_(NULL), numLinks_(0) {} // 方法... void addLink(nsaddr_t src, nsaddr_t dest, int seq, int isup, Time expiry); int findLink(nsaddr_t src, nsaddr_t dest, LinkState **handle); int isEmpty(); void removeLink(LinkState *prev, LinkState *toBeDeleted); int purgeLinks(); void printLinks(); void freeList(); }; //该类维护单个拓扑信息 class LinkState { public: // Data... nsaddr_t src_; // 32 bits的源节点 nsaddr_t dest_; // 32 bits的目标节点 int seq_; // sequence number int isup_; // Link State LINKUP[1]/LINKDOWN[0](状态) Time expiry_; // Link Expiry Time (更新时间) LinkState *next_; // Pointer to Next Element (下一个指针) // 构造方法... LinkState() : src_(-1), dest_(-1), seq_(-1), isup_(-1), expiry_(-1.0), next_(NULL) {} LinkState(nsaddr_t src, nsaddr_t dest, int seq, int isup, Time expiry) { src_ = src; dest_ = dest; seq_ = seq; isup_ = isup; expiry_ = expiry; next_ = NULL; } }; 3.1.2 IARPAgent类内成员PeripheralNodeList类 PeripheralNodeList类及其内部的PeripheralNode类 //维护单个边界节点的类 class PeripheralNode { public: // Data... nsaddr_t addr_; // 节点的地址 int coveredFlag_; // Only For IERP PeripheralNode *next_; // 指向下一个元素的指针 // 构造方法... PeripheralNode","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:3:1","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"4.IERP (被动协议) ","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:4:0","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"4.1 IERPAgent类 该类为ZRP的被动协议控制类 /* [SUB-SECTION-4.5]------------------------------------------------------------ * IERP AGENT:- INTER-ZONE ROUTING AGENT * ----------------------------------------------------------------------------- */ class IERPAgent { //私有方法 private: void addMyAddressToRoute(Packet *pold, Packet *pnew); void markCoveredPN(DetectedQuery *queryInCache, nsaddr_t lastBC); //公共方法 public: // Data... ZRPAgent *agent_; //指向ZRPAgent的指针 int brpXmitPolicy_; //边界广播flag(0: UNICAST, 1: MULTICAST). // Lists... DetectedQueryList dqLst_; //检查到查询的表 SentQueryList sqLst_; //它保留所有已发送的路由请求。 int queryLifeTime_; // DEFAULT_QUERY_LIFETIME 默认询问生存时间 int routeLifeTime_; // DEFAULT_ROUTE_LIFETIME 默认路由生存时间 int queryRetryTime_; // DEFAULT_QUERY_RETRY_TIME 默认询问再发时间 // Timers... //过期检查计时器:- 用于 IERP 请求的过期 IERPExpirationTimer ExpirationTimer_; int startup_jitter_; // For starting the Timers // 构造器... IERPAgent(ZRPAgent *agent) : agent_(agent), brpXmitPolicy_(DEFAULT_BRP_XMIT_POLICY), queryLifeTime_(DEFAULT_QUERY_LIFETIME), routeLifeTime_(DEFAULT_ROUTE_LIFETIME), queryRetryTime_(DEFAULT_QUERY_RETRY_TIME), ExpirationTimer_(agent), startup_jitter_(DEFAULT_STARTUP_JITTER) {} // Methods... void startUp(); void recv_IERP_ROUTE_REQUEST_UNI(Packet* p); void recv_IERP_ROUTE_REQUEST_MC(Packet* p); void recv_IERP_ROUTE_REPLY(Packet* p); void recv_IERP_ROUTE_ERROR(Packet* p); void recv_IERP_DATA(Packet* p); int addLinkStateFromRoute(nsaddr_t *route, int size); int removeLinkStateFromBrokenRoute(nsaddr_t lnkSrc, nsaddr_t lnkDest); void print_tables(); }; 4.1.1 IERPAgent类内私有方法addMyAddressToRoute() /* 2. Appends own address to the route. */ //将自己的地址附加到路由 void IERPAgent::addMyAddressToRoute(Packet *pold, Packet *pnew) { //旧的zrp的包头的指针 hdr_zrp *hdrzold = HDR_ZRP(pold); //新的zrp的包头的指针 hdr_zrp *hdrznew = HDR_ZRP(pnew); //路由长度的临时变量 int routeLen; //设置新的路有长度的变量 routeLen = hdrzold-\u003eroutelength_ + 1; // For My Address // [Task-1]: Allocate memory to store route // 创建内部的存储空间 大小为routeLen (agent_-\u003epktUtil_).pkt_add_ROUTE_space(pnew, routeLen); // [Task-2]: Copy 1st part of route //将旧的路由线路拷贝到新的空间 for(int i=0; i\u003chdrzold-\u003eroutelength_; i++) { hdrznew-\u003eroute_[i] = hdrzold-\u003eroute_[i]; // ['0' to 'hdrzold-\u003eroutelength_ - 1'] } // 加入自己的地址 // [Task-3]: Copy 2nd part of route hdrznew-\u003eroute_[routeLen-1] = agent_-\u003emyaddr_; //修改路有的长度 // [Task-4]: Update Route-Length hdrznew-\u003eroutelength_ = routeLen; // Also Route-length is set. } 4.1.2 IERPAgent类内私有方法markCoveredPN() 不是很明白该方法 /* 3. Marks all covered peripheral nodes based on Last bordercaster Info. */ // 根据 Last bordercaster Info 标记所有覆盖的外围节点。 void IERPAgent::markCoveredPN(DetectedQuery *queryInCache, nsaddr_t lastBC) { //周边节点的存储指针 PeripheralNode *cur = NULL; // 将其指向检测查询缓存的外围节点的head cur = (queryInCache-\u003epnLst_).head_; // InnerRoute的临时变量 InnerRoute *handleToFoundRoute = NULL; // flag int foundRouteFlag; // for(int i=0; i\u003c(queryInCache-\u003epnLst_).numPerNodes_; i++) { foundRouteFlag = (agent_-\u003eiarpAgt_).irLst_.findRoute(cur-\u003eaddr_, \u0026handleToFoundRoute); if(foundRouteFlag == TRUE) { // Route to Peripheral node exists [It must exist] if(handleToFoundRoute-\u003enextHop_ == lastBC) { cur-\u003ecoveredFlag_ = TRUE; // Mark this Node } } cur = cur-\u003enext_; // Advance the Pointer } } 4.1.3 IERPAgent类内成员DetectedQueryList类 //检测到查询的单个存储类 class DetectedQuery { public: // Data... nsaddr_t src_; // 32 bits 源节点 nsaddr_t dest_; // 32 bits 目标节点 int queryID_; // Query ID Time expiry_; // Expiry of this detected Entry 超时时间 PeripheralNodeList pnLst_; // Query-Coverage Info 查询覆盖信息 //是否需要发送 或者 泛洪该查询信息的flag int querySentFlag_; // Whether the query is Sent/Forwarded or Not int totalReplySent_; //已为此查询发送了多少答复。 DetectedQuery *next_; // 指向下一个Query // 构造方法... DetectedQuery() : src_(-1), dest_(-1), queryID_(-1), expiry_(-1.0), querySentFlag_(0), totalReplySent_(0), next_(NULL) {} DetectedQuery(nsaddr_t src, nsaddr_t dest, int queryID, Time expiry, PeripheralNodeList *curPNLst) { src_ = src; dest_ = dest; queryID_ = queryID; expiry_ = expiry; querySentFlag_ = FALSE; next_ = NULL","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:4:1","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"5.ZRP(ZONE ROUTING PROTOCOL) ","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:5:0","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"5.1 ZRP的包头类 /* [SECTION-5]------------------------------------------------------------------ * 1) ZRP HEADER STRUCTURE * [SUB-SECTION-5.1]------------------------------------------------------------ * ZRP HEADER STRUCTURE:- * ----------------------------------------------------------------------------- */ class LSU; // Pre-Declaration //zrp的包头 struct hdr_zrp { // Common Attributes to All packets... [Default Size = 10 bytes] int zrptype_; // Packet type [1 byte] Time pktsent_; // Packet Sending time [Logically not sent] int radius_; // Sender's Zone Radius (Maybe Useful in Future)[Not Used] int seq_; // Sequence number of a packet [1 byte] nsaddr_t src_; // 32-bit Address [4 bytes] nsaddr_t dest_; // 32-bit Address [4 bytes] // IARP Specific... [IARP size = 2 + 5*links] int forwarded_; // TRUE if forwarded before [1 byte] LSU *links_; // pointer to link state list [5 bytes per link - address(4) + link(1)] int numlinks_; // Number of links in packet [1 byte] // IERP Specific... [IERP size (for REQUEST) = 7 + 4*routelength] // [ (for REPLY) = 3 + 4*routelength] // [ (for ERROR) = 2 + 5(for link info) + 4*routelength] // [ (for DATA) = 2 + 4*routelength] nsaddr_t *mcLst_; // List of addresses to relay(Multicast) the [Not Used] // route-request query int mcLstSize_; // Size of above list [Not Used] nsaddr_t lastbc_; // 32-bit Address [Not Used much] [REQUEST - 4 bytes] nsaddr_t *route_; // pointer to route list data [REQUEST/REPLY/ERROR/DATA 4 bytes per address] int routelength_; // Route Length of IERP route stored in route_ [REQUEST/REPLY/ERROR/DATA 1 byte] int routeindex_; // Pointer to a current route node in route_ [REQUEST/REPLY/ERROR/DATA 1 byte] int queryID_; // IERP query id counter [REQUEST/REPLY 1 byte] // this is where the original data for upper layer pkts // is saved while ZRP routes pkt, at dest this is placed // back into hdrip-\u003edport(), ie this is part of encapsulated data int enc_dport_; int enc_daddr_; packet_t enc_ptype_; // Calculate the size of the header inline int size() { int s=0; switch(zrptype_) { case NDP_BEACON: s = ZRP_DEFAULT_HDR_LEN; break; case NDP_BEACON_ACK: s = ZRP_DEFAULT_HDR_LEN; break; case IARP_UPDATE: s = ZRP_DEFAULT_HDR_LEN + 2 + 5*numlinks_; break; case IERP_REPLY: s = ZRP_DEFAULT_HDR_LEN + 3 + 4*routelength_; break; case IERP_REQUEST: s = ZRP_DEFAULT_HDR_LEN + 7 + 4*routelength_; break; case IERP_ROUTE_ERROR: s = ZRP_DEFAULT_HDR_LEN + 7 + 4*routelength_; break; case IARP_DATA: case IERP_DATA: s = ZRP_DEFAULT_HDR_LEN + 2 + 4*routelength_; break; } return s; } // Packet header access functions static int offset_; inline static int\u0026 offset() { return offset_; } inline static hdr_zrp* access(const Packet* p) { return (hdr_zrp*) p-\u003eaccess(offset_); } }; //链路状态 class LSU { public: nsaddr_t src_; nsaddr_t dest_; int isUp_; // Constructors... LSU() : src_(-1), dest_(-1), isUp_(LINKDOWN) {} // Invalid Entries... LSU(nsaddr_t src, nsaddr_t dest, int isUp) : src_(src), dest_(dest), isUp_(isUp) {} }; 5.1.1 ZRP的包头类的内部方法 /* A supporting function used in finding the ZRP header in a packet. */ int hdr_zrp::offset_; static class ZRPHeaderClass : public PacketHeaderClass { public: ZRPHeaderClass() : PacketHeaderClass(\"PacketHeader/ZRP\", sizeof(hdr_zrp)) { bind_offset(\u0026hdr_zrp::offset_); } void export_offsets() { field_offset(\"zrptype_\", OFFSET(hdr_zrp, zrptype_)); field_offset(\"src_\", OFFSET(hdr_zrp, src_)); field_offset(\"dest_\", OFFSET(hdr_zrp, dest_)); field_offset(\"seq_\", OFFSET(hdr_zrp, seq_)); field_offset(\"queryID_\", OFFSET(hdr_zrp, queryID_)); field_offset(\"lastbc_\", OFFSET(hdr_zrp, lastbc_)); } }class_zrp_hdr; /* A binding between ZRP and TCL, you will most likely not change this. */ static class ZRPClass : public TclClass { public: ZRPClass() : TclClass(\"Agent/ZRP\") {} TclObject* create(int argc, const char*const* argv) { return(new ZRPAgent((nsaddr_t) atoi(argv[4]))); // Tcl code will attach me and then pass in addr to me // see tcl/lib/ns-lib.tcl, under create-zrp-agent, // is done by \"set ragen","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:5:1","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"5.2 ZRP的PacketUtil类 /* [SUB-SECTION-5.2]------------------------------------------------------------ * PACKET UTILITIES CLASS:- PACKET RELATED UTILITIES * ----------------------------------------------------------------------------- */ class PacketUtil { public: // Data... int seq_; ZRPAgent *agent_; int startup_jitter_; PacketUtil(ZRPAgent *agent) : seq_(0), agent_(agent), startup_jitter_(DEFAULT_STARTUP_JITTER) {} // Methods... Packet* pkt_create(ZRPTYPE zrp_type, nsaddr_t addressee, int ttl); void pkt_copy(Packet *pfrom, Packet *pto); void inc_seq() { seq_++; if (seq_ \u003e MAX_SEQUENCE_ID) seq_ = 1;} void pkt_send(Packet *p, nsaddr_t addressee, Time delay); void pkt_broadcast(Packet *p, Time randomJitter); void pkt_add_LSU_space(Packet *p, int size); void pkt_free_LSU_space(Packet *p); void pkt_add_ROUTE_space(Packet *p, int size); void pkt_free_ROUTE_space(Packet *p); void pkt_add_ADDRESS_space(Packet *p, int size); void pkt_free_ADDRESS_space(Packet *p); int pkt_AmIMultiCastReciver(Packet *p, nsaddr_t addressToCheck); int pkt_AmIOnTheRoute(Packet *p, nsaddr_t addressToCheck); void pkt_print_links(Packet *p); void pkt_print_route(Packet *p); void pkt_drop(Packet *p); }; 5.2.1 PacketUtil类的内部方法pkt_create() /* 1. Creates a fresh new packet and initializes as much as it knows how. */ Packet* PacketUtil::pkt_create(ZRPTYPE zrp_type, nsaddr_t addressee, int ttl) { Time now = Scheduler::instance().clock(); // get the time Packet *p = agent_-\u003eMyallocpkt(); // fresh new packet hdr_ip *hdrip = HDR_IP(p); hdr_cmn *hdrc = HDR_CMN(p); hdr_zrp *hdrz = HDR_ZRP(p); // Common Header hdrc-\u003eptype() = PT_ZRP; // ZRP pkt type hdrc-\u003enext_hop() = addressee; hdrc-\u003edirection() = hdr_cmn::DOWN; // Sending packets DOWN hdrc-\u003eaddr_type_ = NS_AF_NONE; hdrc-\u003esize() = IP_HDR_LEN; // set default packet size // IP Header hdrip-\u003ettl() = ttl; hdrip-\u003esaddr() = agent_-\u003emyaddr_; // source address hdrip-\u003esport() = ROUTER_PORT; // source port hdrip-\u003edaddr() = addressee; // dest address hdrip-\u003edport() = ROUTER_PORT; // dest port // ZRP Header hdrz-\u003ezrptype_ = zrp_type; // which zrp pkt am I? hdrz-\u003epktsent_ = now; // Packet Sending Time hdrz-\u003eradius_ = agent_-\u003eradius_;// Sender's Radius hdrz-\u003eseq_ = seq_; // copy from gobal sequence counter hdrz-\u003eforwarded_ = 0; // have not been forwarded before hdrz-\u003esrc_ = agent_-\u003emyaddr_; // I am originator, this is used by NDP/IARP hdrz-\u003edest_ = addressee; // dest address hdrz-\u003elinks_ = NULL; // Nothing in IARP Update hdrz-\u003enumlinks_ = 0; // \" \" hdrz-\u003emcLst_ = NULL; // Addresses to Multicast hdrz-\u003emcLstSize_ = 0; // Number of addresses to Multicast hdrz-\u003elastbc_ = -1; // '-1' is an invalid address hdrz-\u003eroute_ = NULL; // Nothing in IERP Route hdrz-\u003eroutelength_ = 0; // \" \" hdrz-\u003erouteindex_ = 0; // where in the route list am I sending ? hdrz-\u003equeryID_ = -1; // Invalid Query ID inc_seq(); // increments global sequence counter return(p); } 5.2.2 PacketUtil类的内部方法pkt_copy() /* 2. Copy an entire packet to another. */ void PacketUtil::pkt_copy(Packet *pfrom, Packet *pto) { hdr_cmn *hdrcfrom = HDR_CMN(pfrom); hdr_ip *hdripfrom = HDR_IP(pfrom); hdr_zrp *hdrzfrom = HDR_ZRP(pfrom); hdr_cmn *hdrcto = HDR_CMN(pto); hdr_ip *hdripto = HDR_IP(pto); hdr_zrp *hdrzto = HDR_ZRP(pto); // Common Header hdrcto-\u003edirection() = hdrcfrom-\u003edirection(); hdrcto-\u003eptype() = hdrcfrom-\u003eptype(); hdrcto-\u003enext_hop() = hdrcfrom-\u003enext_hop(); hdrcto-\u003eaddr_type_ = hdrcfrom-\u003eaddr_type_; hdrcto-\u003esize() = hdrcfrom-\u003esize() ; // IP Header hdripto-\u003ettl() = hdripfrom-\u003ettl(); hdripto-\u003esaddr() = hdripfrom-\u003esaddr(); hdripto-\u003esport() = hdripfrom-\u003esport(); hdripto-\u003edaddr() = hdripfrom-\u003edaddr(); hdripto-\u003edport() = hdripfrom-\u003edport(); // ZRP Header hdrzto-\u003ezrptype_ = hdrzfrom-\u003ezrptype_; hdrzto-\u003epktsent_ = hdrzfrom-\u003epktsent_; hdrzto-\u003eradius_ = hdrzfrom-\u003eradius_; hdrzto-\u003eseq_ = hdrzfrom-\u003eseq_; hdrzto-\u003eforwarded_ = hdrzfrom-\u003eforwarded_; hdrzto-\u003esrc_ = hdrzfrom-\u003esrc_; hdrzto-\u003edest_ = hdrzfrom-\u003edest_; hdrzto-\u003enumlinks_ = hdrzfrom-\u003enumlinks_; hdrzto-\u003emcLstSize_ = hdrzfrom-\u003emcLstSize_; hdr","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:5:2","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"5.3 ZRP的sendBuffer类 该类存储还未发送的包 /* [SUB-SECTION-5.3]------------------------------------------------------------ * SEND BUFFER:- BUFFER TO STORE UN-DELIVERED UPPER-LAYER PACKETS * ----------------------------------------------------------------------------- */ class SendBufferEntry { public: Packet *pkt_; // Packet to Send from Upper-Layer nsaddr_t dest_; // Address of Destination Time expiry_; // Time to expire the packet SendBufferEntry *next_; // Link to next entry in the list SendBufferEntry() : pkt_(NULL), dest_(-1), expiry_(0.00), next_(NULL) {} SendBufferEntry(Packet *pkt, nsaddr_t dest, Time expiry) : pkt_(pkt), dest_(dest), expiry_(expiry), next_(NULL) {} }; class SendBuffer { public: ZRPAgent *agent_; SendBufferEntry *head_; int numPackets_; SendBuffer(ZRPAgent *agent) : agent_(agent), head_(NULL), numPackets_(0) {} // Methods... void addPacket(Packet *pkt, nsaddr_t dest, Time expiry); void purgeExpiredPackets(); void freeList(); }; 5.3.1 sendBuffer类内部方法addPacket() /* 1. Add Packet at the head. */ void SendBuffer::addPacket(Packet *pkt, nsaddr_t dest, Time expiry) { SendBufferEntry *newPkt = new SendBufferEntry(pkt, dest, expiry); // Create a new Entry if(newPkt == NULL) { // Check for Allocation Error printf(\"### Memory Allocation Error in [SendBuffer::addPacket] ###\"); exit(0); } newPkt-\u003enext_ = head_; // Made necessary Joinings head_ = newPkt; numPackets_++; // Increment Number of Packets } 5.3.2 sendBuffer类内部方法purgeExpiredPackets() /* 2. Remove all Expired-Packets. */ void SendBuffer::purgeExpiredPackets() { if(numPackets_ == 0) { return; // Nothing to do } // [case-1]: Leaving the 1st case(head_ case) SendBufferEntry *prev, *cur; prev = head_; cur = head_-\u003enext_; Time now = Scheduler::instance().clock(); // get the time for(int i=1; cur!=NULL; i++) { if(cur-\u003eexpiry_\u003cnow) { // Delete the Expired-Packet prev-\u003enext_ = cur-\u003enext_; SendBufferEntry *toBeDeleted = cur; cur = cur-\u003enext_; assert(toBeDeleted-\u003epkt_ != NULL); agent_-\u003eMydrop(toBeDeleted-\u003epkt_, DROP_RTR_QTIMEOUT); // drops the packet //Packet::free(toBeDeleted-\u003epkt_); // Not needed anymore delete toBeDeleted; numPackets_--; // Decrement Number of Packets } else { prev = cur; // Advance the Pointers cur = cur-\u003enext_; } } // [case-2]: head_ case... if(head_!=NULL) { if(head_-\u003eexpiry_\u003cnow) { SendBufferEntry *toBeDeleted = head_; head_ = head_-\u003enext_; assert(toBeDeleted-\u003epkt_ != NULL); agent_-\u003eMydrop(toBeDeleted-\u003epkt_, DROP_RTR_QTIMEOUT); // drops the packet //Packet::free(toBeDeleted-\u003epkt_); // Not needed anymore delete toBeDeleted; numPackets_--; // Decrement Number of Packets } } } 5.3.3 sendBuffer类内部方法freeList() /* 3. Empty the Buffer. */ void SendBuffer::freeList() { if(numPackets_==0) { return; } SendBufferEntry *cur, *toBeDeleted; cur = head_; for(int i=0; i\u003cnumPackets_; i++) { toBeDeleted = cur; assert(toBeDeleted-\u003epkt_ != NULL); Packet::free(toBeDeleted-\u003epkt_); // Not needed anymore cur = cur-\u003enext_; delete toBeDeleted; } head_ = NULL; numPackets_ = 0; } ","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:5:3","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["区域半径协议"],"content":"5.4 ZRPAgent类 /* [SUB-SECTION-5.4]------------------------------------------------------------ * ZRP AGENT:- ZONE ROUTING AGENT * ----------------------------------------------------------------------------- */ class ZRPAgent : public Agent { public: // Tcl Related... char* myid_; // (char *)myid_ is string equiv of (int)myaddr_ PriQueue* ll_queue; Trace* tracetarget; MobileNode* node_; NsObject* port_dmux_; // Common Data... nsaddr_t myaddr_; // My-own-address int radius_; // Zone-Radius int tx_; // total pkts transmitted by agent int rx_; // total pkts received by agent int queryID_; // IERP query IDs // General Objects... SendBuffer sendBuf_; PacketUtil pktUtil_; NDPAgent ndpAgt_; IARPAgent iarpAgt_; IERPAgent ierpAgt_; // Constructors... ZRPAgent(); // Default Constructor - (1) ZRPAgent(nsaddr_t id); // Parameterized Constructor - (2) // Methods... // 1. General... void startUp(); int command (int argc, const char*const* argv); void recv(Packet * p, Handler *); void route_pkt(Packet* p, nsaddr_t dest); void route_SendBuffer_pkt(); void sendPacketUsingIARPRoute(Packet *p, nsaddr_t dest, Time delay); int initialized() { return 1 \u0026\u0026 target_; } void print_tables(); // 2. Mac Failed... void mac_failed(Packet *p); // 3. Methods for Packet Handling Packet* Myallocpkt() { Packet *p = allocpkt(); // fresh new packet return (p); } void Mydrop(Packet *p, const char *s) { drop(p, s); } void XmitPacket(Packet *p, Time randomJitter) { Scheduler \u0026 s = Scheduler::instance(); s.schedule(target_, p, randomJitter); } }; 5.4.1 ZRPAgent类内方法startUp() /* 3. Start Up Function(s). */ void ZRPAgent::startUp() { // Clear the send buffer... sendBuf_.freeList(); // Start all Sub-Agents... ndpAgt_.startUp(); iarpAgt_.startUp(); ierpAgt_.startUp(); } 5.4.2 ZRPAgent类内方法command() /* 4. TCL interface. */ int ZRPAgent::command (int argc, const char*const* argv) { Time now = Scheduler::instance().clock(); // get the time // [First set: if argc == 2] if (argc == 2) { // No argument from TCL if (strcmp (argv[1], \"start\") == 0) { // Init ZRP Agent startUp(); return (TCL_OK); } else if (strcasecmp (argv[1], \"ll-queue\") == 0) { // who is my ll //[There is no argv[2] here ??] if (!(ll_queue = (PriQueue *) TclObject::lookup (argv[2]))) { fprintf (stderr, \"ZRP_Agent: ll-queue lookup \" \"of %s failed\\n\", argv[2]); return TCL_ERROR; } return TCL_OK; } // [Second set: if argc == 3] } else if (argc == 3) { // One argument from TCL // [Parameter 1 - RADIUS] if (strcasecmp (argv[1], \"radius\") == 0) { // change the radius, takes (int) num hops int temp; temp = atoi(argv[2]); if (temp \u003e 0) { // don't change radius unless input is valid value printf(\"_%2d_ [%6.6f] | Radius change from %d to %d \",myaddr_, now, radius_, temp); print_tables(); printf(\"\\n\"); radius_ = temp; } return TCL_OK; } // [Parameter 2 - MIN_BEACON_PERIOD] if (strcasecmp (argv[1], \"beacon_period\") == 0) { // change beacon period, takes (int) number of secs int temp; temp = atoi(argv[2]); if (temp \u003e 0) { // don't change unless input is valid value printf(\"_%2d_ [%6.6f] | Beacon period change from %d to %d \", myaddr_,now, ndpAgt_.BeaconTransmitTimer_.beacon_period_, temp); print_tables(); printf(\"\\n\"); ndpAgt_.BeaconTransmitTimer_.beacon_period_ = temp; } return TCL_OK; }/* // change beacon period jitter, takes (int) number of secs if (strcasecmp (argv[1], \"beacon_period_jitter\") == 0) { int temp; temp = atoi(argv[2]); if (temp \u003e 0) { // don't change unless input is valid value printf(\"_%2d_ [%6.6f] | Beacon period jitter change from %d to %d \", myaddr_,now, beacon_period_jitter_, temp); print_tables(); printf(\"\\n\"); beacon_period_jitter_ = temp; } return TCL_OK; } // change neighbor timeout, takes (int) number of secs if (strcasecmp (argv[1], \"neighbor_timeout\") == 0) { int temp; temp = atoi(argv[2]); if (temp \u003e 0) { // don't change unless input is valid value printf(\"_%2d_ [%6.6f] | Neighbor timeout change from %d to %d \", myaddr_,now, neighbor_timeout_, temp); print_tables(); printf(\"\\n\"); neighbor_timeout","date":"2022-12-02","objectID":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/:5:4","tags":["ZRP"],"title":"区域半径协议ZRP","uri":"/%E5%8C%BA%E5%9F%9F%E5%8D%8A%E5%BE%84%E5%8D%8F%E8%AE%AEzrp/"},{"categories":["算法"],"content":"差分数组 如果给你一个包含5000万个元素的数组,然后会有频繁区间修改操作,那什么是频繁的区间修改操作呢?比如让第1个数到第1000万个数每个数都加上1,而且这种操作时频繁的。 此时你应该怎么做?很容易想到的是,从第1个数开始遍历,一直遍历到第1000万个数,然后每个数都加上1,如果这种操作很频繁的话,那这种暴力的方法在一些实时的系统中可能就拉跨了。 因此,今天的主角就出现了——差分数组。 ","date":"2022-11-16","objectID":"/%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:0:0","tags":["差分数组"],"title":"差分数组","uri":"/%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["算法"],"content":"1.定义 差分数组就是与原数组同样大小的数组 原数组记为:a[] 差分数组记为:d[] 公式: 1.i=1时 d[i]=a[i] 2.i\u003e1时,d[i]=a[i]-a[i-1] 恢复公式 a[i]=d[i]+a[i-1] index 0 1 2 3 4 原数组a 1 3 7 5 2 差分d 1 2 4 -2 -3 sum.d 1 3 7 5 2 ","date":"2022-11-16","objectID":"/%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:0","tags":["差分数组"],"title":"差分数组","uri":"/%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["算法"],"content":"2.差分数组的应用:区间的快速加减 对区间[L,R]统一加上X,只需要在差分数组d[L]+X,d[R+1]-X即可 例如,对区间[1,3]加3 index 0 1 2 3 4 原数组 1 3 7 5 2 原差分d 1 2 4 -2 -3 减差分 1 5 4 -2 0 还原数组 1 6 10 8 2 ","date":"2022-11-16","objectID":"/%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:2:0","tags":["差分数组"],"title":"差分数组","uri":"/%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["算法"],"content":"1.前缀和 ","date":"2022-11-16","objectID":"/%E5%89%8D%E7%BC%80%E5%92%8C/:0:0","tags":["前缀和"],"title":"前缀和","uri":"/%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["算法"],"content":"1.问题 输入一个长度为n的整数数列, 接下来在输入m个询问, 每个询问都输入一对l(left),r(right) ","date":"2022-11-16","objectID":"/%E5%89%8D%E7%BC%80%E5%92%8C/:1:0","tags":["前缀和"],"title":"前缀和","uri":"/%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["算法"],"content":"2.输入格式 第一行输入长度n和询问m 第二行包含n个整数 接下来的m行,每行包含两个整数l(left)和r(right),表示查询区间范围 ","date":"2022-11-16","objectID":"/%E5%89%8D%E7%BC%80%E5%92%8C/:2:0","tags":["前缀和"],"title":"前缀和","uri":"/%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["算法"],"content":"3.输出格式 共m行,每行表示一个结果 ","date":"2022-11-16","objectID":"/%E5%89%8D%E7%BC%80%E5%92%8C/:3:0","tags":["前缀和"],"title":"前缀和","uri":"/%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["算法"],"content":"4.数据范围 1≤l≤r≤n 1≤n,m≤100000 −1000≤数列中元素的值≤1000 ","date":"2022-11-16","objectID":"/%E5%89%8D%E7%BC%80%E5%92%8C/:4:0","tags":["前缀和"],"title":"前缀和","uri":"/%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["算法"],"content":"5.输入样例: 输入n,m : 5 3 输入n个整数 : 2 1 3 6 4 输入询问: 1 2 1 3 2 4 ","date":"2022-11-16","objectID":"/%E5%89%8D%E7%BC%80%E5%92%8C/:5:0","tags":["前缀和"],"title":"前缀和","uri":"/%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["算法"],"content":"6.输出样例: 3 6 10 以上是一道最纯粹的前缀和的问题 接下来,讨论一下什么是前缀和 2.什么是前缀和 我们先给出一个数组: 1,2,3,4,5,6,7,8,9 那么这个数组的前缀和即为: 1,3,6,10,15,21,28,36,45 前缀和就是从第1个数到当前数组的区间和 为了更好的了解前缀和 我们把上述内容放入数组中进行表示 数组a[]表示原数组: a[9]={1,2,3,4,5,6,7,8,9}; 数组res[]表示原数组的前缀和数组: res[9]={1,3,6,10,15,21,28,36,45}; 前缀和与原数组之间的代数关系 res[0] = a[0] = 1 res[1] = a[0] + a[1] = 1 + 2 res[2] = a[0] + a[1] + a[2] = 1 + 2 + 3 res[3] = a[0] + a[1] + a[2] + a[3] = 1 + 2 + 3 + 4 ............ res[8] = a[0] + a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7] + a[8] = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 = 45 res[i-1] = a[0] + a[1] + ....... + a[i-1] (0\u003c=i \u003c 9) ","date":"2022-11-16","objectID":"/%E5%89%8D%E7%BC%80%E5%92%8C/:6:0","tags":["前缀和"],"title":"前缀和","uri":"/%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["算法"],"content":"前缀和的计算与输出 int a[9]={1,2,3,4,5,6,7,8,9}; int res[] = new int[a.length]; res[0] = a[0]; for(int i = 1; i \u003c n ; i++){ res[i] = a[i]+res[i-1]; } ","date":"2022-11-16","objectID":"/%E5%89%8D%E7%BC%80%E5%92%8C/:7:0","tags":["前缀和"],"title":"前缀和","uri":"/%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["算法"],"content":"前缀和的优势:以(o1)的时间复杂度得到某块区间的总和 那么如何求出某块区间的和呢? 我们用L和R表示区间的左端点和右端点 res[L]和res[R]表示左右端点 res[L]=a[0]+a[1]+a[2]+...+a[L] res[R]=a[0]+a[1]+a[2]+...+a[L]+a[L+1]+a[L+2]+...+a[R] (L\u003c=R) 我们要求的是[L,R]之间的和 即a[L]+a[L+1]+a[L+2]+...+a[R] 即res[R]-res[L-1] ","date":"2022-11-16","objectID":"/%E5%89%8D%E7%BC%80%E5%92%8C/:8:0","tags":["前缀和"],"title":"前缀和","uri":"/%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["算法"],"content":"问题的解答 #include \u003ciostream\u003e using namespace std; //定义范围 +10为防止数据溢出 const int N = 100010; int n, m; int a[N], s[N]; int main(){ //输入数组范围,查询次数 cin \u003e\u003e n \u003e\u003e m; //计算前缀和数组 for (int i = 1; i \u003c= n; i ++ ) { cin \u003e\u003e a[i], s[i] = s[i - 1] + a[i]; } //遍历查询 while (m -- ){ int l, r; cin \u003e\u003e l \u003e\u003e r; cout \u003c\u003c s[r] - s[l - 1] \u003c\u003c endl; } return 0; } ","date":"2022-11-16","objectID":"/%E5%89%8D%E7%BC%80%E5%92%8C/:9:0","tags":["前缀和"],"title":"前缀和","uri":"/%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["Git使用"],"content":"1.什么是.gitignore 在一些项目中,我们不想让本地仓库的所有文件都上传到远程仓库, 比如:万恶的.DS_Store ","date":"2022-11-14","objectID":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/:1:0","tags":["Git"],"title":"关于.gitignore详解","uri":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/"},{"categories":["Git使用"],"content":"2.使用规则 ","date":"2022-11-14","objectID":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/:2:0","tags":["Git"],"title":"关于.gitignore详解","uri":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/"},{"categories":["Git使用"],"content":"2.1 注释 # this is .gitignore file. # 以下是忽略的文件 .DS_Store 上述例子中,#开头的就是注释 ","date":"2022-11-14","objectID":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/:2:1","tags":["Git"],"title":"关于.gitignore详解","uri":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/"},{"categories":["Git使用"],"content":"2.2 忽略特定的后缀 # 忽略后缀为exe的文件 *.exe ","date":"2022-11-14","objectID":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/:2:2","tags":["Git"],"title":"关于.gitignore详解","uri":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/"},{"categories":["Git使用"],"content":"2.3 忽略整个文件夹 如果我们想要忽略某个路径下文件夹的所有内容 我们可以在该文件夹路径的后面加入/,如下 folder/ ","date":"2022-11-14","objectID":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/:2:3","tags":["Git"],"title":"关于.gitignore详解","uri":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/"},{"categories":["Git使用"],"content":"2.4 忽略文件和目录 例如:folder 表示忽略folder文件和folder目录 会搜索多级目录,找到所有名为folder的文件 folder ","date":"2022-11-14","objectID":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/:2:4","tags":["Git"],"title":"关于.gitignore详解","uri":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/"},{"categories":["Git使用"],"content":"2.5 !表示取反 如果我们想要保留src/lib下的man.txt 那么我们可以写下如下内容 !man.txt ","date":"2022-11-14","objectID":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/:2:5","tags":["Git"],"title":"关于.gitignore详解","uri":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/"},{"categories":["Git使用"],"content":"2.6 glob通配符 常用的通配符 (1) * :可以代表一切的字符串,可以是0个也可以是任意多个字符 (2) ? :匹配除了’/‘以外的任意一个字符 (3) []:匹配多个列表中的字符 例如想要清除所有.txt文件 *.txt 如果想要清除目标文档下的所有.json文件 src/*.json ","date":"2022-11-14","objectID":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/:2:6","tags":["Git"],"title":"关于.gitignore详解","uri":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/"},{"categories":["Git使用"],"content":"2.7 双星号 斜杠后面紧跟两个连续的星号**,表示多级目录 src/**/folder ","date":"2022-11-14","objectID":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/:2:7","tags":["Git"],"title":"关于.gitignore详解","uri":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/"},{"categories":["Git使用"],"content":"3 其他规则 (1) .gitignore 文件也会上传到远程仓库 (2) 如果本地仓库已被跟踪,那么即使在.gitignore中设置了忽略,也不起作用 ","date":"2022-11-14","objectID":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/:3:0","tags":["Git"],"title":"关于.gitignore详解","uri":"/%E5%85%B3%E4%BA%8Egitignore%E8%AF%A6%E8%A7%A3/"},{"categories":["hugo使用"],"content":"使用hugo创建Blog ","date":"2022-11-13","objectID":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/:0:0","tags":["hugo"],"title":"创建Blog流程","uri":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/"},{"categories":["hugo使用"],"content":"1.创建流程 ","date":"2022-11-13","objectID":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/:1:0","tags":["hugo"],"title":"创建Blog流程","uri":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/"},{"categories":["hugo使用"],"content":"1.1 下载 hugo //使用homebrew下载hugo brew install hugo ","date":"2022-11-13","objectID":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/:1:1","tags":["hugo"],"title":"创建Blog流程","uri":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/"},{"categories":["hugo使用"],"content":"1.2 快速创建 hugo new site mysite ","date":"2022-11-13","objectID":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/:1:2","tags":["hugo"],"title":"创建Blog流程","uri":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/"},{"categories":["hugo使用"],"content":"1.3 下载主题 //在刚刚创建的mysite/themes文件下打开终端 //LoveIt为当前作者选择的主题 git clone https://github.com/dillonzq/LoveIt.git LoveIt ","date":"2022-11-13","objectID":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/:1:3","tags":["hugo"],"title":"创建Blog流程","uri":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/"},{"categories":["hugo使用"],"content":"1.4 阅读主题demo或者文档 ","date":"2022-11-13","objectID":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/:1:4","tags":["hugo"],"title":"创建Blog流程","uri":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/"},{"categories":["hugo使用"],"content":"1.5 快速创建写作文档 执行以下命令,在mysite/content下创建posts/my-first-post.md文档 hugo new posts/my-first-post.md 按照想要你想要编辑的方式,去改写my-first-post.md文档 在启用drafts参数的条件下开启Hugo内置的服务器。 hugo server -D ","date":"2022-11-13","objectID":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/:1:5","tags":["hugo"],"title":"创建Blog流程","uri":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/"},{"categories":["hugo使用"],"content":"1.6 上传github 在github创建后缀为github.io的仓库 将hugo的baseUrl设置为GitHub仓库的地址 并将主题设置为当前主题 hugo --theme=LoveIt --baseUrl=\"http://zxlkgf.github.io/\" --buildDrafts 再将public文件夹上传到github即可 ","date":"2022-11-13","objectID":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/:1:6","tags":["hugo"],"title":"创建Blog流程","uri":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/"},{"categories":["hugo使用"],"content":"1.7 hugo基本使用 使用方法: hugo hugo [flags] hugo [command] hugo [command] [flags] 例如 command: new 为你的站点创建新的内容 server 一个高性能的web服务器 节选的 flags: -D, –buildDrafts 包括被标记为draft的文章 -E, –buildExpired 包括已过期的文章 -F, –buildFuture 包括将在未来发布的文章 例子: hugo -D 生成静态文件并包括draft为true的文章 hugo new post/new-content.md 新建一篇文章 hugo new site mysite 新建一个称为mysite的站点 hugo server –buildExpired 启动服务器并包括已过期的文章 ","date":"2022-11-13","objectID":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/:1:7","tags":["hugo"],"title":"创建Blog流程","uri":"/%E4%BD%BF%E7%94%A8hugo%E5%88%9B%E5%BB%BAblog/"},{"categories":["SpringBoot","Mybatis"],"content":"Kaptcha ","date":"2022-10-23","objectID":"/springboot%E9%A1%B9%E7%9B%AE-kaptcha/:0:0","tags":["computerStore"],"title":"SpringBoot项目-Kaptcha","uri":"/springboot%E9%A1%B9%E7%9B%AE-kaptcha/"},{"categories":["SpringBoot","Mybatis"],"content":"1.1 Kaptcha简介 Kaptcha 是一个扩展自simplecaptcha的验证码库,默认情况下,Kaptcha非常易于设置和使用,并且默认输出会产生一个很难验证的验证码。默认情况下,它生成的验证码看起来与上面的非常相似。如果您想更改输出的外观,则有几个配置选项,并且该框架是模块化的,因此您可以编写自己的变形代码。 参考资料:( kaptcha验证码使用) ","date":"2022-10-23","objectID":"/springboot%E9%A1%B9%E7%9B%AE-kaptcha/:1:0","tags":["computerStore"],"title":"SpringBoot项目-Kaptcha","uri":"/springboot%E9%A1%B9%E7%9B%AE-kaptcha/"},{"categories":["SpringBoot","Mybatis"],"content":"1.2 Kaptcha详细配置表 2 Maven依赖 \u003c!-- 验证码 --\u003e \u003cdependency\u003e \u003cgroupId\u003ecom.baomidou\u003c/groupId\u003e \u003cartifactId\u003ekaptcha-spring-boot-starter\u003c/artifactId\u003e \u003cversion\u003e1.1.0\u003c/version\u003e \u003c/dependency\u003e 3.创建配置类 package com.zxl.store.config; import com.google.code.kaptcha.impl.DefaultKaptcha; import com.google.code.kaptcha.util.Config; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Properties; /** * @author zxl * @version 1.0 * @description: kaptcha配置类 * @date 2022/10/30 */ @Slf4j @Configuration public class KaptchaConfig { //kaptcha @Bean public DefaultKaptcha getKaptcheCode() { DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); // 创建properties Properties properties = new Properties(); //是否有边框 NO properties.setProperty(\"kaptcha.border\", \"no\"); //字体颜色 black properties.setProperty(\"kaptcha.textproducer.font.color\", \"black\"); //图片宽度 100 properties.setProperty(\"kaptcha.image.width\", \"100\"); //图片高度 36 properties.setProperty(\"kaptcha.image.height\", \"36\"); //字体大小 30px properties.setProperty(\"kaptcha.textproducer.font.size\", \"30\"); //图片样式 阴影 properties.setProperty(\"kaptcha.obscurificator.impl\", \"com.google.code.kaptcha.impl.ShadowGimpy\"); //session key = code properties.setProperty(\"kaptcha.session.key\", \"code\"); //干扰实现类 properties.setProperty(\"kaptcha.noise.impl\", \"com.google.code.kaptcha.impl.NoNoise\"); //背景渐变颜色 开始颜色 properties.setProperty(\"kaptcha.background.clear.from\", \"232,240,254\"); //背景渐变颜色 结束颜色 properties.setProperty(\"kaptcha.background.clear.to\", \"232,240,254\"); //验证码长度 properties.setProperty(\"kaptcha.textproducer.char.length\", \"4\"); //字体 properties.setProperty(\"kaptcha.textproducer.font.names\", \"彩云,宋体,楷体,微软雅黑\"); //设置参数 Config config = new Config(properties); defaultKaptcha.setConfig(config); return defaultKaptcha; } } 创建控制类 package com.zxl.store.controller; import com.google.code.kaptcha.Constants; import com.google.code.kaptcha.Producer; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.imageio.ImageIO; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; /** * @author zxl * @version 1.0 * @description: kaptcha的控制层,kaptcha调用 * @date 2022/10/30 */ @Slf4j @RestController @RequestMapping(\"/kaptcha\") public class KaptchaController { @Autowired private Producer producer; @GetMapping(\"/kaptcha-image\") public void getKaptchaImage(HttpServletRequest request, HttpServletResponse response) throws Exception { //禁止server缓存 response.setDateHeader(\"Expires\", 0); //设置标准的http/1.1 no-cache headers response.setHeader(\"Cache-Control\", \"no-store, no-cache, must-revalidate\"); // 设置IE扩展 HTTP/1.1 no-cache headers (use addHeader) response.addHeader(\"Cache-Control\", \"post-check=0, pre-check=0\"); // 设置标准 HTTP/1.0 不缓存图片 response.setHeader(\"Pragma\", \"no-cache\"); // 返回一个 jpeg 图片,默认是text/html(输出文档的MIMI类型) response.setContentType(\"image/jpeg\"); // 为图片创建文本 String capText = producer.createText(); // 输出验证码 log.info(\"******************当前验证码为:{}******************\", capText); // 将验证码存于session中 request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, capText); // 创建带有文本的图片 BufferedImage bi = producer.createImage(capText); ServletOutputStream out = response.getOutputStream(); // 向页面输出验证码 ImageIO.write(bi, \"jpg\", out); try { // 清空缓存区 out.flush(); } finally { // 关闭输出流 out.close(); } } } ","date":"2022-10-23","objectID":"/springboot%E9%A1%B9%E7%9B%AE-kaptcha/:2:0","tags":["computerStore"],"title":"SpringBoot项目-Kaptcha","uri":"/springboot%E9%A1%B9%E7%9B%AE-kaptcha/"},{"categories":["SpringBoot","Mybatis"],"content":"AOP ","date":"2022-10-22","objectID":"/springboot%E9%A1%B9%E7%9B%AE-aop/:1:0","tags":["computerStore"],"title":"SpringBoot项目-AOP","uri":"/springboot%E9%A1%B9%E7%9B%AE-aop/"},{"categories":["SpringBoot","Mybatis"],"content":"1 Spring AOP AOP:面向切面(Aspect)编程。AOP并不是Spring框架的特性,只是Spring很好的支持了AOP。 如果需要在处理每个业务时,都执行特定的代码,则可以假设在整个数据处理流程中存在某个切面,切面中可以定义某些方法,当处理流程执行到切面时,就会自动执行切面中的方法。最终实现的效果就是:只需要定义好切面方法,配置好切面的位置(连接点),在不需要修改原有数据处理流程的代码的基础之上,就可以使得若干个流程都执行相同的代码。 ","date":"2022-10-22","objectID":"/springboot%E9%A1%B9%E7%9B%AE-aop/:1:1","tags":["computerStore"],"title":"SpringBoot项目-AOP","uri":"/springboot%E9%A1%B9%E7%9B%AE-aop/"},{"categories":["SpringBoot","Mybatis"],"content":"2 切面方法 1.切面方法的访问权限是public。 2.切面方法的返回值类型可以是void或Object,如果使用的注解是@Around时,必须使用Object作为返回值类型,并返回连接点方法的返回值;如果使用的注解是@Before或@After等其他注解时,则自行决定。 3.切面方法的名称可以自定义。 4.切面方法的参数列表中可以添加ProceedingJoinPoint接口类型的对象,该对象表示连接点,也可以理解调用切面所在位置对应的方法的对象,如果使用的注解是@Around时,必须添加该参数,反之则不是必须添加。 ","date":"2022-10-22","objectID":"/springboot%E9%A1%B9%E7%9B%AE-aop/:1:2","tags":["computerStore"],"title":"SpringBoot项目-AOP","uri":"/springboot%E9%A1%B9%E7%9B%AE-aop/"},{"categories":["SpringBoot","Mybatis"],"content":"3 统计业务方法执行时长 1.在使用Spring AOP编程时,需要先在pom.xml文件中添加两个关于AOP的依赖aspectjweaver和aspectjtools。 \u003cdependency\u003e \u003cgroupId\u003eorg.aspectj\u003c/groupId\u003e \u003cartifactId\u003easpectjweaver\u003c/artifactId\u003e \u003c/dependency\u003e \u003cdependency\u003e \u003cgroupId\u003eorg.aspectj\u003c/groupId\u003e \u003cartifactId\u003easpectjtools\u003c/artifactId\u003e \u003c/dependency\u003e 2.在com.cy.store.aop包下创建TimerAspect切面类,在类之前添加@Aspect和@Component注解修饰。 package com.cy.store.aop; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class TimerAspect { } 3.在类中添加切面方法around(ProceedingJoinPoint pjp)。 public Object around(ProceedingJoinPoint pjp) throws Throwable { // 记录起始时间 long start = System.currentTimeMillis(); // 执行连接点方法,即切面所在位置对应的方法。本项目中表示执行注册或执行登录等 Object result = pjp.proceed(); // 记录结束时间 long end = System.currentTimeMillis(); // 计算耗时 System.err.println(\"耗时:\" + (end - start) + \"ms.\"); // 返回连接点方法的返回值 return result; } 4.最后需要在方法之前添加@Around注解,以配置连接点,即哪些方法需要应用该切面。 @Around(\"execution(* com.zxl.store.service.impl.*.*(..))\") 5.启动项目,在前端浏览器访问任意一个功能模块进行功能的测试。 ","date":"2022-10-22","objectID":"/springboot%E9%A1%B9%E7%9B%AE-aop/:1:3","tags":["computerStore"],"title":"SpringBoot项目-AOP","uri":"/springboot%E9%A1%B9%E7%9B%AE-aop/"},{"categories":["SpringBoot","Mybatis"],"content":"9 商品搜索 ","date":"2022-10-20","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E6%90%9C%E7%B4%A2/:1:0","tags":["computerStore"],"title":"SpringBoot项目-商品搜索","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E6%90%9C%E7%B4%A2/"},{"categories":["SpringBoot","Mybatis"],"content":"9.1 关于商品的模糊搜索 9.1.1 后端-持久层 1.编写sql SELECT id,title,sell_point,price,image FROM t_product WHERE STATUS = 1 AND title LIKE '%${title}%' ORDER BY priority DESC; 2.编写Mapper接口的抽象方法 /** * 按照输入的标题查找 * @param title * @return */ List\u003cProduct\u003e findProductByTitle(String title); 3.编写Mapper接口的映射文件 \u003c!-- List\u003cProduct\u003e findProductByTitle(String title);--\u003e \u003cselect id=\"findProductByTitle\" resultType=\"com.zxl.store.entity.Product\"\u003e SELECT id,title,sell_point,price,image FROM t_product WHERE STATUS = 1 AND title LIKE '%${title}%' ORDER BY priority DESC \u003c/select\u003e ","date":"2022-10-20","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E6%90%9C%E7%B4%A2/:1:1","tags":["computerStore"],"title":"SpringBoot项目-商品搜索","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E6%90%9C%E7%B4%A2/"},{"categories":["SpringBoot","Mybatis"],"content":"9.1.2 后端-业务层 1.处理异常 2.编写业务层抽象方法 /** * 按照标题查询 * @param title * @param pageNum * @param pageSize * @return */ PageInfo\u003cProduct\u003e findProductByTitle(String title,Integer pageNum, Integer pageSize); 3.编写业务层逻辑 @Override public PageInfo\u003cProduct\u003e findProductByTitle(String title, Integer pageNum, Integer pageSize) { //开启分页功能 PageHelper.startPage(pageNum,pageSize); //查询结果 List\u003cProduct\u003e res = productMapper.findProductByTitle(title); //返回结果 return new PageInfo\u003c\u003e(res); } ","date":"2022-10-20","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E6%90%9C%E7%B4%A2/:1:2","tags":["computerStore"],"title":"SpringBoot项目-商品搜索","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E6%90%9C%E7%B4%A2/"},{"categories":["SpringBoot","Mybatis"],"content":"9.1.3 后端-控制层 1.处理异常 2.设计请求 请求路径:/product/findWithTitle 请求参数:Integer pageNum,Integer pageSize,String title 请求类型:get 响应类型:JsonResult\u003cPageInfo\u003c Product» 3.处理请求,编写控制层方法 @RequestMapping(value = \"/{pageNum}/{pageSize}/{title}\",method = RequestMethod.GET) public JsonResult\u003cPageInfo\u003cProduct\u003e\u003e findWithTitle(@PathVariable(\"pageNum\") Integer pageNum, @PathVariable(\"pageSize\") Integer pageSize, @PathVariable(\"title\") String title){ PageInfo\u003cProduct\u003e res = productService.findProductByTitle(title, pageNum, pageSize); return new JsonResult\u003c\u003e(OK,res); } 4.前端页面 具体请参考github \u003cscript type=\"text/javascript\"\u003e //获取标题 var title = getOne(); title = title.substring(0,title.indexOf(\"\u0026\")); function getUrlParam(name) { var reg = new RegExp(\"(^|\u0026)\" + name + \"=([^\u0026]*)(\u0026|$)\"); var r = window.location.search.substr(1).match(reg); if (r != null) return unescape(r[2]); return null; } var num = getUrlParam(\"pageNum\"); //为了查询设置的全局参数 var pageNum = 0; var pageSize = 0; var prePage = 0; var nextPage = 0; var navigatepageNums = []; //记录fid var fid = 0; //根据分页条的选择的页数进行查询 function PaginationListSelect(num,size,res) { //进行查询的关键字 let title = $(\"#search\").val() llocation.href = \"/web/search.html?title=\" + title + \"\u0026pageNum=\" + num + \"\u0026pageSize=\" + size; //填充分页条信息 addPaginationData(res); } //按照标题查询数据 function searchByTitle(pageNum,pageSize,title){ $.ajax({ url:\"/product/\" + pageNum + \"/\" + pageSize + \"/\" + title, dataType:\"json\", success:function (res) { if(res.state==200){ if(res.data.list.length!=0){ $(\"#resultTest\").html(\"\\\"\"+ title + \"\\\"\"); addData(res); }else{ $(\"#resultTest\").html(\"\\\"\"+ title + \"\\\"\"); $(\"#errmsg\").html(\"目前该商品并不在库\"); } } } }); } //填充数据 function addData(res){ //清空目标内容 $(\"#productList\").empty(); $(\"#PaginationList\").empty(); let productListStr = \"\"; //获取分页信息中商品的长度 let dataLength = res.data.list.length; //填充商品到信息页面 for (let i = 0; i \u003c dataLength; i++) { let product = res.data.list[i] productListStr = \"\u003cdiv class=\\\"col-md-3\\\"\u003e\" + \"\u003cdiv class=\\\"goods-panel\\\"\u003e\" + \"\u003cimg src=..\" + product.image + \"collect.png\" + \" class=\\\"img-responsive\\\" /\u003e\" + \"\u003cp\u003e¥\" + product.price + \".00\" + \"\u003c/p\u003e\" + \"\u003cdiv class=\\\"text-row-3\\\"\u003e\" + \"\u003ca href=product.html?pid=\" + product.id + \"\u003e\u003csmall\u003e\" + product.title + \"\u003c/small\u003e\u003c/a\u003e\u003c/div\u003e\" + \"\u003cspan style='padding-right: 10px'\u003e\" + \"\u003ca href='javascript:void(0)' onclick='addToCollect(#{id})' id='product#{num}' class='btn btn-default btn-xs add-fav'\u003e\u003cspan class='fa fa-heart-o'\u003e\u003c/span\u003e加入收藏\u003c/a\u003e\" + \"\u003c/span\u003e\" + \"\u003cspan style='padding-right: 10px'\u003e\" + \"\u003ca href='javascript:void(0)' onclick='addCollectToCart(#{id},#{price})' class=\\\"btn btn-default btn-xs add-cart\\\"\u003e\u003cspan class=\\\"fa fa-cart-arrow-down\\\"\u003e\u003c/span\u003e加入购物车\u003c/a\u003e\" + \"\u003c/span\u003e\u003c/div\u003e\u003c/div\u003e\" productListStr = productListStr.replaceAll(\"#{id}\",product.id); productListStr = productListStr.replaceAll(\"#{price}\",product.price); $(\"#productList\").append(productListStr) } //填充分页条信息 addPaginationData(res); } //填充分页条信息 function addPaginationData(res) { //重新填充分页条 //将数据返回的部分需要数据进行填充至全局参数 pageNum = res.data.pageNum //当前页 pageSize = res.data.pageSize //每页显示数 prePage = res.data.prePage //上一页 nextPage = res.data.nextPage //下一页 navigatepageNums = res.data.navigatepageNums //分页栏的数字 let firstPage = \"\u003ca id='first' href='#' onclick='PaginationListSelect(prePage,pageSize)' style='padding-right: 8px'\u003e上一页\u003c/a\u003e\" let lastPage = \"\u003ca id='end' href='javascript:void(0)' onclick='PaginationListSelect(nextPage,pageSize)' style='padding-right: 8px'\u003e下一页\u003c/a\u003e\" let PaginationListStr = \"\"; //判断是否是第一页 if (res.data.isFirstPage){ //为true表示当前是第一页 firstPage = \"\u003ca id='first' href='javascript:void(0)' \" + \"style='opacity: 0.2;padding-right: 8px;color: black'\u003e上一页\u003c/a\u003e\" PaginationListStr += firstPage; }else { //为false表示当前不是第一页 PaginationListStr += firstPage; } //填充分页的页码数 for (let i = 0; i \u003c navigatepageNums.length; i++) { //当前页的页码 let nowNum = navigatepageNums[i] if (nowNum === pageNum){ //相等表示i的次数和当前也相同,对页数显示做变化 PaginationListStr += \"\u003ca href='javascript:void(0)' \" + \"style='padding-right: 8px;color: black' disabled='disabled'\u003e\" + \"【\" + nowNum + \"","date":"2022-10-20","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E6%90%9C%E7%B4%A2/:1:3","tags":["computerStore"],"title":"SpringBoot项目-商品搜索","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E6%90%9C%E7%B4%A2/"},{"categories":["SpringBoot","Mybatis"],"content":"分页插件学习 1.github:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md 2.CSDN:https://blog.csdn.net/m0_48736673/article/details/124805124 ","date":"2022-10-19","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/:0:0","tags":["computerStore"],"title":"SpringBoot项目-分页插件","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/"},{"categories":["SpringBoot","Mybatis"],"content":"1.使用步骤 ","date":"2022-10-19","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/:1:0","tags":["computerStore"],"title":"SpringBoot项目-分页插件","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/"},{"categories":["SpringBoot","Mybatis"],"content":"1.在pom.xml添加如下依赖 \u003cdependency\u003e \u003cgroupId\u003ecom.github.pagehelper\u003c/groupId\u003e \u003cartifactId\u003epagehelper-spring-boot-starter\u003c/artifactId\u003e \u003cversion\u003e最新版本\u003c/version\u003e \u003c!--\u003cversion\u003e1.2.3\u003cversion\u003e--\u003e \u003c/dependency\u003e ","date":"2022-10-19","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/:1:1","tags":["computerStore"],"title":"SpringBoot项目-分页插件","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/"},{"categories":["SpringBoot","Mybatis"],"content":"2.在springboot中添加部分配置 在application.properties中添加配置 #pagehelper配置 pagehelper.helper-dialect=mysql pagehelper.reasonable=true pagehelper.support-methods-arguments=true pagehelper.params=count=countSql ","date":"2022-10-19","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/:1:2","tags":["computerStore"],"title":"SpringBoot项目-分页插件","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/"},{"categories":["SpringBoot","Mybatis"],"content":"3.注册一个PageHelper类 @Bean public PageHelper pageHelper() { PageHelper pageHelper = new PageHelper(); Properties properties = new Properties(); properties.setProperty(\"offsetAsPageNum\", \"true\"); properties.setProperty(\"rowBoundsWithCount\", \"true\"); properties.setProperty(\"reasonable\", \"true\"); properties.setProperty(\"dialect\", \"mysql\"); pageHelper.setProperties(properties); return pageHelper; } ","date":"2022-10-19","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/:1:3","tags":["computerStore"],"title":"SpringBoot项目-分页插件","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/"},{"categories":["SpringBoot","Mybatis"],"content":"4.业务层或者需要使用的层设置开启 @Override public PageInfo\u003cFavorite\u003e findFavorite(Integer uid, Integer pageNum, Integer pageSize) { //开启分页功能 //pageNum是当前页,pageSize是每页显示的数据量 PageHelper.startPage(pageNum,pageSize); // List\u003cFavorite\u003e favorites = favoriteMapper.findFavoritesByUidAndStatus(uid, 1); // PageInfo\u003cFavorite\u003e pageInfo = new PageInfo\u003c\u003e(favorites); return pageInfo; } ","date":"2022-10-19","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/:1:4","tags":["computerStore"],"title":"SpringBoot项目-分页插件","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/"},{"categories":["SpringBoot","Mybatis"],"content":"5.控制层返回查询结果即可 @RequestMapping(value = \"/findFavorites\",method = RequestMethod.GET) public JsonResult\u003cPageInfo\u003cFavorite\u003e\u003e findFavorites(HttpSession session, Integer pageNum, Integer pageSize){ Integer uid = getUserIdFromSession(session); PageInfo\u003cFavorite\u003e favorites = favoriteService.findFavorite(uid, pageNum, pageSize); return new JsonResult\u003c\u003e(OK,favorites); } ","date":"2022-10-19","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/:1:5","tags":["computerStore"],"title":"SpringBoot项目-分页插件","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/"},{"categories":["SpringBoot","Mybatis"],"content":"部分参数的解释 1.dialect:默认情况下会使用 PageHelper 方式进行分页, 如果想要实现自己的分页逻辑,可以实现 Dialect(com.github.pagehelper.Dialect)接口 然后配置该属性为实现类的全限定名称。 2.reasonable:分页合理化参数,默认值为false。当该参数设置为true时, pageNum\u003c=0 时会查询第一页,pageNum\u003epages(超过总数时),会查询最后一页。 默认false时,直接根据参数进行查询。 ","date":"2022-10-19","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/:2:0","tags":["computerStore"],"title":"SpringBoot项目-分页插件","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/"},{"categories":["SpringBoot","Mybatis"],"content":"pageInfo参数大全 //当前页 private int pageNum; //每页的数量 private int pageSize; //当前页的数量 private int size; //由于startRow和endRow不常用,这里说个具体的用法 //可以在页面中\"显示startRow到endRow 共size条数据\" //当前页面第一个元素在数据库中的行号 private int startRow; //当前页面最后一个元素在数据库中的行号 private int endRow; //总记录数 private long total; //总页数 private int pages; //结果集(每页显示的数据) private List\u003cT\u003e list; //第一页 private int firstPage; //前一页 private int prePage; //是否为第一页 private boolean isFirstPage = false; //是否为最后一页 private boolean isLastPage = false; //是否有前一页 private boolean hasPreviousPage = false; //是否有下一页 private boolean hasNextPage = false; //导航页码数 private int navigatePages; //导航页第一页 private int navigateFirstPage; //导航页第二页 private int navigateLastPage; //所有导航页号 private int[] navigatepageNums; ","date":"2022-10-19","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/:2:1","tags":["computerStore"],"title":"SpringBoot项目-分页插件","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6/"},{"categories":["SpringBoot","Mybatis"],"content":"8 收藏管理 ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:0:0","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.1 创建实体类 CREATE TABLE t_favorites( fid INT PRIMARY KEY AUTO_INCREMENT COMMENT '收藏商品在数据表的id', uid INT COMMENT '归属的用户id', pid INT COMMENT '归属的商品id', image VARCHAR(255) COMMENT '商品图片保存地址', price BIGINT COMMENT '商品的价格', title VARCHAR(255) COMMENT '商品的标题', sell_point VARCHAR(255) COMMENT '商品的卖点', status INT COMMENT '商品的状态' ); ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:1:0","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.2 创建实体类 /** * @author zxl * @description 收藏实体类 * @date 2022/11/6 */ @Data @AllArgsConstructor @NoArgsConstructor public class Favorite extends BaseEntity { private Integer fid; private Integer uid; private Integer pid; private String image; private Long price; private String title; private String sellPoint; private Integer status; } ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:2:0","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.3 加入收藏 ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:3:0","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.3.1 后端-持久层 1.编写sql //插入收藏 insert into t_favorite(除了fid)values(值) //判断是否存在该收藏 SELECT * FROM t_favorite WHERE uid = ? AND pid = ? 2.编写Mapper接口 /** * 根据pid和uid查询收藏商品是否在库 * @param uid * @param pid * @return */ Favorite findFavoriteByPidAndUid(Integer uid,Integer pid); /** * 插入收藏 * @param favorite 收藏数据 * @return */ Integer addFavorite(Favorite favorite); 3.编写映射文件 \u003cmapper namespace=\"com.zxl.store.mappers.FavoriteMapper\"\u003e \u003c!-- Integer addFavorite(Favorite favorite);--\u003e \u003cinsert id=\"addFavorite\" parameterType=\"com.zxl.store.entity.Favorite\" useGeneratedKeys=\"true\" keyProperty=\"fid\"\u003e INSERT INTO t_favorite( uid, pid, image, price, title, sell_point, status ) VALUES ( #{uid}, #{pid}, #{image}, #{price}, #{title}, #{sellPoint}, #{status} ) \u003c/insert\u003e \u003c!-- Favorite findFavoriteByPidAndUid(Integer uid,Integer pid);--\u003e \u003cselect id=\"findFavoriteByPidAndUid\" resultType=\"com.zxl.store.entity.Favorite\"\u003e SELECT * FROM t_favorite WHERE uid = #{uid} AND pid = #{pid} \u003c/select\u003e \u003c/mapper\u003e 4.单元测试 ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:3:1","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.3.2 后端-业务层 1.编写异常 //收藏已经存在异常 public class FavoriteExistException extends ServiceException {} 2.编写业务层抽象方法 /** * @author zxl * @description 收藏业务层的接口 * @date 2022/11/6 */ public interface IFavoriteService { /** * 插入收藏 * @param uid * @param pid */ Integer addFavorite(Integer uid,Integer pid); } 3.编写实现逻辑 /** * @author zxl * @description 收藏接口的业务层实现类 * @date 2022/11/6 */ @Service public class IFavoriteServiceImpl implements IFavoriteService { @Autowired(required = false) private FavoriteMapper favoriteMapper; @Autowired private IProductService productService; @Override public Integer addFavorite(Integer uid, Integer pid) { //先判断是否已经存在收藏 Favorite res = favoriteMapper.findFavoriteByPidAndUid(uid, pid); //判断结果 if(res!=null){ throw new FavoriteExistException(\"该收藏品已经存在!\"); } //商品不存在的话,执行下一步 Favorite favorite = new Favorite(); //根据pid查询信息 Product p = productService.findProductById(pid); //加入数据 favorite.setUid(uid); favorite.setPid(pid); favorite.setImage(p.getImage()); favorite.setPrice(p.getPrice()); favorite.setTitle(p.getTitle()); favorite.setSellPoint(p.getSellPoint()); favorite.setStatus(p.getStatus()); //执行插入 Integer row = favoriteMapper.addFavorite(favorite); //判断插入结果 if(row!=1)throw new InsertException(\"插入收藏时出现未知错误\"); System.out.println(favorite.getFid()); return favorite.getFid(); } } 4.单元测试 ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:3:2","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.3.3 后端-控制层 1.处理异常 将异常交给全局处理 2.设计请求 请求路径:/favorite/addFavorite 请求类型:HttpSession session,Integer pid 请求方式:post 响应类型:JsonResult\u003c Integer\u003e 3.处理请求编写控制类 /** * @author zxl * @description 添加收藏的控制类 * @date 2022/11/6 */ @RestController @RequestMapping(\"/favorite\") public class FavoriteController extends BaseController { @Autowired private IFavoriteService favoriteService; @RequestMapping(value = \"/addFavorite\",method = RequestMethod.POST) public JsonResult\u003cInteger\u003e addFavorite(Integer pid, HttpSession session){ //uid Integer uid = getUserIdFromSession(session); //执行插入 Integer fid = favoriteService.addFavorite(uid, pid); return new JsonResult\u003c\u003e(OK,fid); } } 4.前端页面 //绑定收藏事件 $(\"#btn-add-to-collect\").click(function () { if (confirm(\"确定要将此商品加入收藏吗?\")) { $.ajax({ url: \"/favorite/addFavorite\", type: \"post\", data: {\"pid\": pid}, dataType: \"json\", success: function (res) { if (res.state == 200) { alert(\"收藏成功!\"); } else { alert(res.message); } }, error: function (err) { alert(\"服务器出现错误,加入购物车失败!\") } }) } }); ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:3:3","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.4 显示收藏 ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:4:0","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.4.1 后端-持久层 1.编写sql //根据用户uid和收藏商品的状态去查找 SELECT * FROM t_favorite WHERE uid = #{uid} AND status=#{status} 2.编写Mapper接口抽象方法 /** * 按照uid和status查找用户的收藏列表 * @param uid 用户的id * @param status 商品状态 * @return 返回结果集合 */ List\u003cFavorite\u003e findFavoritesByUidAndStatus(Integer uid,Integer status); 3.编写映射文件 \u003c!-- List\u003cFavorite\u003e findFavoritesByUidAndStatus(Integer uid,Integer status);--\u003e \u003cselect id=\"findFavoritesByUidAndStatus\"\u003e SELECT * FROM t_favortie WHERE uid = #{uid} AND status = 1; \u003c/select\u003e 4.单元测试 ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:4:1","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.4.2 后端-业务层 1.编写异常 2.编写业务层抽象方法 /** * 查找指定条件的收藏 * @param uid 用户uid * @param pageNum 当前页码 * @param pageSize 每页数据多少 * @return */ PageInfo\u003cFavorite\u003e findFavorite(Integer uid, Integer pageNum, Integer pageSize); 3.编写实现逻辑 @Override public PageInfo\u003cFavorite\u003e findFavorite(Integer uid, Integer pageNum, Integer pageSize) { //开启分页功能 //pageNum是当前页,pageSize是每页显示的数据量 PageHelper.startPage(pageNum,pageSize); // List\u003cFavorite\u003e favorites = favoriteMapper.findFavoritesByUidAndStatus(uid, 1); // PageInfo\u003cFavorite\u003e pageInfo = new PageInfo\u003c\u003e(favorites); return pageInfo; } 4.单元测试 ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:4:2","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.4.3 后端-控制层 1.处理异常 2.设计请求 请求路径:/favorite/findFavorites 请求类型:HttpSession session,Integer pageNum,Integer pageSize 请求方式:get 响应类型:JsonResult 3.处理请求 编写控制方法 //查询 @RequestMapping(value = \"/findFavorites\",method = RequestMethod.GET) public JsonResult\u003cPageInfo\u003cFavorite\u003e\u003e findFavorites(HttpSession session, Integer pageNum, Integer pageSize){ Integer uid = getUserIdFromSession(session); PageInfo\u003cFavorite\u003e favorites = favoriteService.findFavorite(uid, pageNum, pageSize); return new JsonResult\u003c\u003e(OK,favorites); } 4.前端页面 \u003cscript type=\"text/javascript\"\u003e //为了查询设置的全局参数 var pageNum = 0; var pageSize = 0; var prePage = 0; var nextPage = 0; var navigatepageNums = []; //根据分页条的选择的页数进行查询 function PaginationListSelect(num,size) { showCollectProduct(num,size) } function addDataToList(res) { //在填充数据之前必须先将这两个div标签内的所有元素情况,不然会出现叠加情况 $(\"#collectList\").empty(); $(\"#PaginationList\").empty(); let collectListStr = \"\"; //获取分页数据中的数据数量 let dataLength = res.data.list.length; //填充商品到页面 for(let i = 0;i\u003cdataLength;i++){ let favorite = res.data.list[i]; collectListStr = \"\u003cdiv class=\\\"col-md-3\\\"\u003e\" + \"\u003cdiv class=\\\"goods-panel\\\"\u003e\" + \"\u003cimg src=..\" + favorite.image + \"collect.png\" + \" class=\\\"img-responsive\\\" /\u003e\" + \"\u003cp\u003e¥\" + favorite.price + \".00\" + \"\u003c/p\u003e\" + \"\u003cdiv class=\\\"text-row-3\\\"\u003e\" + \"\u003ca href=product.html?pid=\" + favorite.pid + \"\u003e\u003csmall\u003e\" + favorite.title + \"\u003c/small\u003e\u003c/a\u003e\u003c/div\u003e\" + \"\u003cspan style='padding-right: 10px'\u003e\" + \"\u003ca href='javascript:void(0)' onclick='CancelCollect(#{fid})' class='btn btn-default btn-xs add-fav'\u003e\u003cspan class='fa fa-heart'\u003e\u003c/span\u003e取消收藏\u003c/a\u003e\" + \"\u003c/span\u003e\" + \"\u003cspan style='padding-right: 10px'\u003e\" + \"\u003ca href='javascript:void(0)' onclick='addCollectToCart(#{pid},#{price})' class=\\\"btn btn-default btn-xs add-cart\\\"\u003e\u003cspan class=\\\"fa fa-cart-arrow-down\\\"\u003e\u003c/span\u003e加入购物车\u003c/a\u003e\" + \"\u003c/span\u003e\u003c/div\u003e\u003c/div\u003e\"; collectListStr = collectListStr.replaceAll(\"#{fid}\",favorite.fid); collectListStr = collectListStr.replaceAll(\"#{pid}\",favorite.pid); collectListStr = collectListStr.replaceAll(\"#{price}\",favorite.price); $(\"#collectList\").append(collectListStr); } //重新填充分页条 // 将数据返回的部分需要数据进行填充至全局参数 pageNum = res.data.pageNum; //当前页 pageSize = res.data.pageSize; //每页显示数 prePage = res.data.prePage; //上一页 nextPage = res.data.nextPage; //下一页 navigatepageNums = res.data.navigatepageNums;//分页栏的数字 let firstPage = \"\u003ca id='first' href='#' onclick='PaginationListSelect(prePage,pageSize)' style='padding-right: 8px'\u003e上一页\u003c/a\u003e\"; let lastPage = \"\u003ca id='end' href='javascript:void(0)' onclick='PaginationListSelect(nextPage,pageSize)' style='padding-right: 8px'\u003e下一页\u003c/a\u003e\"; let PaginationListStr = \"\"; //判断是否是第一页 if (res.data.isFirstPage){ //为true表示当前是第一页 firstPage = \"\u003ca id='first' href='javascript:void(0)' \" + \"style='opacity: 0.2;padding-right: 8px;color: black'\u003e上一页\u003c/a\u003e\"; PaginationListStr += firstPage; }else { //为false表示当前不是第一页 PaginationListStr += firstPage; } //填充分页的页码数 for (let i = 0; i \u003c navigatepageNums.length; i++) { //当前页的页码 let nowNum = navigatepageNums[i]; if (nowNum === pageNum){ //相等表示i的次数和当前也相同,对页数显示做变化 PaginationListStr += \"\u003ca href='javascript:void(0)' \" + \"style='padding-right: 8px;color: black' disabled='disabled'\u003e\" + \"【\" + nowNum + \"】\" +\"\u003c/a\u003e\" }else { PaginationListStr += \"\u003ca href='javascript:void(0)' onclick='PaginationListSelect(#{nowNum},pageSize)' \" + \"style='padding-right: 8px'\u003e\" + nowNum +\"\u003c/a\u003e\" } PaginationListStr = PaginationListStr.replaceAll(\"#{nowNum}\",nowNum) } //判断是否是末页 if (!res.data.isLastPage){ //取反为false表示当前是末页 PaginationListStr += lastPage; }else { //为true表示当前是末页 lastPage = \"\u003ca id='end' href='javascript:void(0)' style='opacity: 0.2;padding-right: 8px;color: black'\u003e下一页\u003c/a\u003e\" PaginationListStr += lastPage; } //将拼接的str串插入指定id处 $(\"#PaginationList\").append(PaginationListStr); } function showCollectProduct(num,size){ $.ajax({ url: \"/favorite/findFavorites\", type: \"get\", data: \"pageNum=\" + num + \"\u0026pageSize=\" + size, dataType: \"json\", success: function (res) { if(res.data.list.length !== 0){ //代表有数据 //从showPageDataIntoHtml.js中导入的方法 addDataToList(res) }else { //代表没数据 alert(\"暂无收藏商","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:4:3","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.5 删除收藏 ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:5:0","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.5.1 后端-持久层 1.编写sql //取消收藏 DELETE FROM t_favorite WHERE fid = #{fid} 2.编写Mapper抽象方法 /** * 删除收藏 * @param fid 收藏id * @return */ Integer deleteCollect(Integer fid); 3.编写Mapper映射 \u003c!-- Integer deleteCollect(Integer fid);--\u003e \u003cdelete id=\"deleteCollect\"\u003e DELETE FROM t_favorite WHERE fid = #{fid} \u003c/delete\u003e 4.单元测试 ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:5:1","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.5.2 后端-业务层 1.编写异常 删除异常已经定义 2.编写业务层抽象方法 /** * 删除收藏 * @param fid */ void deleteCollect(Integer fid); 3.编写实现逻辑 @Override public void deleteCollect(Integer fid) { Integer row = favoriteMapper.deleteCollect(fid); if(row!=1)throw new DeleteException(\"删除收藏是出现错误\"); } 4.单元测试 ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:5:2","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.5.3 后端-控制层 1.处理异常 2.设计请求 请求路径:/favorite/cancelFavorite 请求类型:Integer fid 请求方式:post 响应类型:JsonResult 3.处理请求 编写控制方法 //删除 @RequestMapping(value = \"/cancelFavorite\",method = RequestMethod.POST) public JsonResult\u003cVoid\u003e cancelFavorite (Integer fid){ favoriteService.deleteCollect(fid); return new JsonResult\u003c\u003e(OK); } 4.前端页面 //取消收藏 function CancelCollect(fid) { if(confirm(\"确定取消收藏吗?\")){ $.ajax({ url:\"/favorite/cancelFavorite\", type: \"POST\", data:{\"fid\":fid}, dataType: \"json\", success:function (res) { if(res.state == 200){ alert(\"取消收藏成功\"); location.reload(); }else{ alert(\"取消收藏失败\"); } } }); } } ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:5:3","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.6 加入购物车 已经被实现,只需要调用即可 ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:6:0","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"8.6.1 前端页面 //加入购物车功能 function addCollectToCart(pid,price){ if (confirm(\"确定要将此商品加入购物车吗?\")){ $.ajax({ url: \"/cart/addCart\", type: \"post\", data: {pid:pid,price:price,num:1}, dataType: \"json\", success: function (res) { alert(\"已成功加入购物车,在购物车等您结算哟!\") }, error : function (err) { alert(\"服务器出现错误,加入购物车失败!\") } }) } } ","date":"2022-10-18","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/:6:1","tags":["computerStore"],"title":"SpringBoot项目-收藏管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E6%94%B6%E8%97%8F%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"订单管理 ","date":"2022-10-17","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/:0:0","tags":["computerStore"],"title":"SpringBoot项目-订单管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"7.1 创建数据表 CREATE TABLE t_order ( oid INT AUTO_INCREMENT COMMENT '订单id', uid INT NOT NULL COMMENT '用户id', aid INT NOT NULL COMMENT '收货地址id', recv_name VARCHAR(20) NOT NULL COMMENT '收货人姓名', recv_phone VARCHAR(20) COMMENT '收货人电话', recv_province VARCHAR(15) COMMENT '收货人所在省', recv_city VARCHAR(15) COMMENT '收货人所在市', recv_area VARCHAR(15) COMMENT '收货人所在区', recv_address VARCHAR(50) COMMENT '收货详细地址', total_price BIGINT COMMENT '总价', status INT COMMENT '状态:0-未支付,1-已支付,2-已取消,3-已关闭,4-已完成', order_time DATETIME COMMENT '下单时间', pay_time DATETIME COMMENT '支付时间', created_user VARCHAR(20) COMMENT '创建人', created_time DATETIME COMMENT '创建时间', modified_user VARCHAR(20) COMMENT '修改人', modified_time DATETIME COMMENT '修改时间', PRIMARY KEY (oid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE t_order_item ( id INT AUTO_INCREMENT COMMENT '订单中的商品记录的id', oid INT NOT NULL COMMENT '所归属的订单的id', pid INT NOT NULL COMMENT '商品的id', title VARCHAR(100) NOT NULL COMMENT '商品标题', image VARCHAR(500) COMMENT '商品图片', price BIGINT COMMENT '商品价格', num INT COMMENT '购买数量', created_user VARCHAR(20) COMMENT '创建人', created_time DATETIME COMMENT '创建时间', modified_user VARCHAR(20) COMMENT '修改人', modified_time DATETIME COMMENT '修改时间', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ","date":"2022-10-17","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/:1:0","tags":["computerStore"],"title":"SpringBoot项目-订单管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"7.2 创建实体类 /** * @author zxl * @description t_order表对应的实体类 * @date 2022/11/6 */ @Data @AllArgsConstructor @NoArgsConstructor public class Order extends BaseEntity{ private Integer oid; private Integer uid; private Integer aid; private String recvName; private String recvPhone; private String recvProvince; private String recvCity; private String recvArea; private String recvAddress; private Long totalPrice; private Integer status; private Date orderTime; private Date payTime; } /** * @author zxl * @description t_order_item表对应的实体类 * @date 2022/11/6 */ public class OrderItem extends BaseEntity { private Integer id; private Integer oid; private Integer pid; private String title; private String image; private Long price; private Integer num; } ","date":"2022-10-17","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/:2:0","tags":["computerStore"],"title":"SpringBoot项目-订单管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"7.3 创建订单 ","date":"2022-10-17","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/:3:0","tags":["computerStore"],"title":"SpringBoot项目-订单管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"7.3.1 后端-持久层 1.编写sql //在t_order表中插入数据 INSERT INTO t_order(oid除外)VALUES(字段的值) //在t_order_item中插入数据 INSERT INTO t_order_item (id除外)VALUES(字段的值) 2.编写Mapper接口的抽象方法 /** * @author zxl * @description 订单类的Mapper接口 * @date 2022/11/6 */ public interface OrderMapper { /** * 插入order * @param order 订单数据 * @return */ Integer insertOrder(Order order); /** * 插入order_item数据 * @param orderItem * @return */ Integer insertOrderItem(OrderItem orderItem); } 3.编写Mapper接口的映射文件 \u003cmapper namespace=\"com.zxl.store.mappers.OrderMapper\"\u003e \u003c!-- Integer insertOrder(Order order);--\u003e \u003cinsert id=\"insertOrder\" useGeneratedKeys=\"true\" keyProperty=\"oid\"\u003e insert into t_order(uid,aid,recv_name,recv_phone,recv_province, recv_city,recv_area,recv_address, total_price,status,order_time,pay_time, created_user,created_time,modified_user,modified_time) values( #{uid},#{aid},#{recvName},#{recvPhone},#{recvProvince},#{recvCity}, #{recvArea},#{recvAddress},#{totalPrice},#{status},#{orderTime}, #{payTime},#{createdUser},#{createdTime},#{modifiedUser},#{modifiedTime} ) \u003c/insert\u003e \u003c!-- Integer insertOrderItem(OrderItem orderItem);--\u003e \u003cinsert id=\"insertOrderItem\"\u003e insert into t_order_item(oid,pid,title,image,price,num, created_user,created_time,modified_user,modified_time) values(#{oid},#{pid},#{title},#{image},#{price}, #{num},#{createdUser},#{createdTime},#{modifiedUser},#{modifiedTime} ) \u003c/insert\u003e \u003c/mapper\u003e 单元测试 ","date":"2022-10-17","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/:3:1","tags":["computerStore"],"title":"SpringBoot项目-订单管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"7.3.2 后端业务层 1.编写异常 插入失败,已经被定义 2.编写业务层抽象方法 /** * 插入订单数据 * @param aid 收货地址id * @param uid 用户id * @param totalPrice 商品总价 * @param username 操作人 */ Order insertOrder(Integer aid,Integer uid,Long totalPrice,String username); /** * 添加orderItem * @param oid * @param cid * @param num * @param username */ void insertOrderItem(Integer oid,Integer cid,Integer num,String username); 3.实现逻辑 /** * @author zxl * @description 订单的业务层接口实现类 * @date 2022/11/6 */ @Service public class IOrderServiceImpl implements IOrderService { @Autowired(required = false) private OrderMapper orderMapper; @Autowired private IAddressService addressService; @Autowired private ICartService cartService; @Autowired private IProductService productService @Override public Order insertOrder(Integer aid, Integer uid, Long totalPrice, String username) { //根据控制层传入的aid进行查询 Address address = addressService.findAddressByAid(aid); //创建一个用于向持久层传输的Order实体类对象 Order order = new Order(); //补全order对象的空白字段 order.setUid(uid); order.setAid(aid); order.setRecvName(address.getName()); order.setRecvPhone(address.getPhone()); order.setRecvProvince(address.getProvinceName()); order.setRecvCity(address.getCityName()); order.setRecvArea(address.getAreaName()); order.setRecvAddress(address.getAddress()); order.setTotalPrice(totalPrice); order.setStatus(0); //表示未支付 Date createdTime = new Date(); order.setOrderTime(createdTime); order.setPayTime(null); order.setCreatedUser(username); order.setModifiedUser(username); order.setCreatedTime(createdTime); order.setModifiedTime(createdTime); //调用持久层进行插入 int result = orderMapper.insertOrder(order); if (result == 0){ throw new InsertException(\"服务器出现错误,创建订单失败\"); } //根据oid查询指定的订单,并返回给控制层 return orderMapper.findOrderByOid(order.getOid()); } @Override public void insertOrderItem(Integer oid, Integer cid, Integer num, String username) { //根据cid查询订单获取pid Cart cart = cartService.findCartByCid(cid); //取出pid的值 Integer pid = cart.getPid(); //根据pid查询商品信息 Product product = productService.findProductById(pid); //创建一个用于向持久层传输的OrderItem实体类对象 OrderItem orderItem = new OrderItem(); //补全orderItem对象的空白字段 orderItem.setOid(oid); orderItem.setPid(pid); orderItem.setTitle(product.getTitle()); orderItem.setImage(product.getImage()); orderItem.setPrice(product.getPrice()); orderItem.setNum(num); Date createdTime = new Date(); orderItem.setCreatedUser(username); orderItem.setCreatedTime(createdTime); orderItem.setModifiedUser(username); orderItem.setModifiedTime(createdTime); //调用持久层进行插入 int result = orderMapper.insertOrderItem(orderItem); if (result == 0){ throw new InsertException(\"服务器出现错误,创建订单失败\"); } } } 4.单元测试 ","date":"2022-10-17","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/:3:2","tags":["computerStore"],"title":"SpringBoot项目-订单管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"7.3.3 后端控制层 1.处理异常 2.设计请求 请求路径: /order/createOrder 请求参数:Integer aid,Long totalPrice,HttpSession session ②Integer oid,Integer cid,Integer pid,Integer num,HttpSession session 请求类型:post 响应类型:JsonResult\u003c Order\u003e 3.设计控制类 /** * @author zxl * @description * @date 2022/11/6 */ @RestController @RequestMapping(\"/Order\") public class OrderController extends BaseController { @Autowired private IOrderService orderService; /** * Description : 处理用户创建order订单的请求 * @date 2022/7/18 * @param aid 用户选中的地址aid * @param totalPrice 商品的总金额 * @param session 项目启动自动生成的session对象 * @return top.year21.computerstore.utils.JsonResult\u003cjava.lang.Void\u003e **/ @PostMapping(\"/createOrder\") public JsonResult\u003cOrder\u003e createOrder(Integer aid, Long totalPrice, HttpSession session){ //从session中取出用户名和uid Integer uid = getUserIdFromSession(session); String username = getUsernameFromSession(session); //调用业务层方法执行插入操作 Order order = orderService.insertOrder(aid,uid, totalPrice,username); return new JsonResult\u003c\u003e(OK,order); } } 4.前端页面 ","date":"2022-10-17","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/:3:3","tags":["computerStore"],"title":"SpringBoot项目-订单管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%AE%A2%E5%8D%95%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6 购物车 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:1:0","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.1 创建数据表 CREATE TABLE t_cart ( cid INT AUTO_INCREMENT COMMENT '购物车数据id', uid INT NOT NULL COMMENT '用户id', pid INT NOT NULL COMMENT '商品id', price BIGINT COMMENT '加入时商品单价', num INT COMMENT '商品数量', created_user VARCHAR(20) COMMENT '创建人', created_time DATETIME COMMENT '创建时间', modified_user VARCHAR(20) COMMENT '修改人', modified_time DATETIME COMMENT '修改时间', PRIMARY KEY (cid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:2:0","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.2 创建实体类 /** * @author zxl * @description 购物车商品信息实体类 * @date 2022/11/4 */ @Data @AllArgsConstructor @NoArgsConstructor public class Cart extends BaseEntity{ // cid INT AUTO_INCREMENT COMMENT '购物车数据id', // uid INT NOT NULL COMMENT '用户id', // pid INT NOT NULL COMMENT '商品id', // price BIGINT COMMENT '加入时商品单价', // num INT COMMENT '商品数量', // created_user VARCHAR(20) COMMENT '创建人', // created_time DATETIME COMMENT '创建时间', // modified_user VARCHAR(20) COMMENT '修改人', // modified_time DATETIME COMMENT '修改时间', private Integer cid; private Integer uid; private Integer pid; private Long price; private Integer num; } ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:3:0","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.3 加入购物车 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:4:0","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.3.1 后端-持久层 1.编写sql语句 //1.向购物车插入数据 INSERT INTO t_cart (aid除外) values (值列表) //2.当当前购物车内已经存在当前商品,则直接更换当前商品数量 UPDATE t_cart SET num = ? WHERE cid = ? //3.插入和更新的操作是取决于购物车是否有该商品 //所有需要查询商品是否存在 SELECT * FROM t_cart WHERE uid = ? AND cid = ? 2.编写Mapper接口和抽象方法 /** * @author zxl * @description 购物车持久层的Mapper接口 * @date 2022/11/4 */ public interface CartMapper { /** * 插入Cart数据 * @param cart 购物车数据 * @return */ Integer addCart(Cart cart); /** * 更新Cart内容 * @param cid 购物车id * @param num 更新物品数量 * @param modifiedUser 更新人 * @param modifiedTime 更新时间 * @return */ Integer updateCartInfo(Integer cid, Integer num, String modifiedUser, Date modifiedTime); /** * 按照用户的uid和商品的pid查找某条购物车数据 * @param uid 用户的uid * @param pid 商品的id * @return 返回购物车数据 */ Cart findCartByUidAndPid(Integer uid,Integer pid); } 3.编写Mapper接口的映射文件 \u003cmapper namespace=\"com.zxl.store.mappers.CartMapper\"\u003e \u003c!-- Integer addCart(Cart cart);--\u003e \u003cinsert id=\"addCart\" useGeneratedKeys=\"true\" keyProperty=\"cid\"\u003e INSERT INTO t_cart(uid,pid,price,num,created_user,created_time,modified_user,modified_time) VALUES (#{uid},#{pid},#{price},#{num},#{createdUser},#{createdTime},#{modifiedUser},#{modifiedTime}) \u003c/insert\u003e \u003c!-- Integer updateCartInfo(Integer cid, Integer num , String modifiedUser, Date modifiedTime);--\u003e \u003cupdate id=\"updateCartInfo\"\u003e UPDATE t_cart SET num = #{num},modified_user = #{modifiedUser},modified_time = #{modifiedTime} WHERE cid = #{cid} \u003c/update\u003e \u003c!-- Cart findCartByUidAndPid(Integer uid,Integer pid);--\u003e \u003cselect id=\"findCartByUidAndPid\" resultType=\"com.zxl.store.entity.Cart\"\u003e SELECT * FROM t_cart WHERE uid = #{uid} AND pid = #{pid} \u003c/select\u003e \u003c/mapper\u003e 4.单元测试 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:4:1","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.3.2 后端-业务层 1.规划异常 2.编写业务层接口和抽象方法 /** * 将商品加入购物车 * @param uid 用户id * @param pid 商品id * @param num 商品数量 * @param username 用户名 */ void addCart(Integer uid,Integer pid,Integer num,Integer username); 3.编写抽象方法的具体实现逻辑 /** * @author zxl * @description 购物车业务层接口的实现类 * @date 2022/11/4 */ @Service public class ICartServiceImpl implements ICartService { @Autowired(required = false) private CartMapper cartMapper; @Override public void addCart(Cart cart, String createdUser, Date createdTime, String modifiedUser, Date modifiedTime) { //查询当前商品是否在购物车存在 Integer pid = cart.getPid(); Integer uid = cart.getUid(); Cart destCart = cartMapper.findCartByUidAndPid(uid, pid); //判断查询结果 if(destCart==null){//如果不存在 //补全四个字段 cart.setCreatedUser(createdUser); cart.setModifiedUser(modifiedUser); cart.setCreatedTime(createdTime); cart.setModifiedTime(modifiedTime); //执行插入操作 Integer integer = cartMapper.addCart(cart); //判断插入结果 if(integer!=1){ throw new InsertException(\"插入购物车数据时遭遇未知异常\"); } }else{//表示该商品存在数据 //取出查询的数据数量 Integer destNum = destCart.getNum(); //取出新添加产品的数量 Integer cartNum = cart.getNum(); //计算结果 Integer num = destNum + cartNum; //设置需要更新的字段 cart.setNum(num); cart.setModifiedUser(modifiedUser); cart.setModifiedTime(modifiedTime); //执行更新操作 Integer row = cartMapper.updateCartInfo(cart); if(row != 1 ){ throw new UpdateException(\"更新购物车数据是遭遇未知异常\"); } } } } 4.单元测试 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:4:2","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.3.3 后端控制层 1.处理异常 插入异常和更新异常均已被处理 2.设计请求 请求地址:/cart/add_cart 请求参数:Integer pid,Integer price,Integer num, HttpSession session 请求类型:post 响应类型:JsonResult 3.处理请求 编写控制方法 /** * @author zxl * @description 处理购物车请求的控制类 * @date 2022/11/5 */ @RestController @RequestMapping(\"/cart\") public class CartController extends BaseController { @Autowired private ICartService cartService; @RequestMapping(value = \"/add_cart\",method = RequestMethod.POST) public JsonResult\u003cVoid\u003e addCart(Integer pid,Integer price,Integer num, HttpSession session){ //从session获取uid和username String username = getUsernameFromSession(session); Integer uid = getUserIdFromSession(session); Date currentTime = new Date(); //设置参数 Cart cart = new Cart(); cart.setUid(uid); cart.setPid(pid); cart.setPrice(Long.valueOf(price)); cart.setNum(num); System.out.println(cart); //执行插入操作 cartService.addCart(cart,username,currentTime,username,currentTime); return new JsonResult\u003c\u003e(OK); } } 4. 前端页面 //将物品加入购物车 function addProductToCart(){ $(\"#btn-add-cart\").click(function () { if(confirm(\"确定要将此商品加入购物车吗?\")){ let price = $(\"#product-price\").text(); let num = $(\"#num\").val(); alert(price + \":\"+num); $.ajax({ url:\"/cart/add_cart\", type:\"post\", data:{ \"pid\":pid, \"price\":price, \"num\":num }, dataType: \"json\", success:function (json) { if(json.state==200){ alert(\"已经成功加入购物车\"); }else{ alert(\"加入购物车失败\"); } }, }); } }); } ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:4:3","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.4 展示购物车 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:5:0","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.4.1 后端-持久层 1.创建多表查询的实体类 /** * @author zxl * @description Cart表和Product表联合查询的结果映射实体类 * @date 2022/11/5 */ @Data @AllArgsConstructor @NoArgsConstructor public class CartVo { private Integer cid; private Integer pid; private Integer uid; private Long price; private Integer num; private String title; private String image; private String realPrice; } 2. 编写sql语句 由于购物车展示需要显示: 商品名称(product) 单价(product,cart) 数量(cart) 金额(product) SELECT c.cid, c.uid, c.pid, c.price, c.num, p.title, p.image, p.price AS realPrice FROM t_cart c LEFT JOIN t_product p ON c.pid=p.id WHERE c.uid = #{uid} ORDER BY c.created_time DESC 3.编写对应的Mapper接口的抽象方法 /** * 按照用户uid 查询所有的购物车记录 * @param uid 用户uid * @return 返回购物车集合 */ List\u003cCartVo\u003e findAllCartByUid(Integer uid); 4.编写Mapper接口对于的映射文件 \u003c!-- List\u003cCartVo\u003e findAllCartByUid(Integer uid);--\u003e \u003cselect id=\"findAllCartByUid\" resultType=\"com.zxl.store.vo.CartVo\"\u003e SELECT c.cid, c.uid, c.pid, c.price, c.num, p.title, p.image, p.price AS realPrice FROM t_cart c LEFT JOIN t_product p ON c.pid=p.id WHERE c.uid = #{uid} ORDER BY c.created_time DESC \u003c/select\u003e 5.单元测试 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:5:1","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.4.2 后端-业务层 1.异常处理 2.编写业务层抽象方法 /** * 根据用户uid查询购物车数据 * @param uid 用户uid * @return */ List\u003cCartVo\u003e findAllCartByUid(Integer uid); 3.编写业务层方法的实现 @Override public List\u003cCartVo\u003e findAllCartByUid(Integer uid) { return cartMapper.findAllCartByUid(uid); } 4.单元测试 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:5:2","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.4.3 后端-控制层 1.异常处理 2.设计请求 请求地址:/cart/show_carts 请求参数:HttpSession session 请求类型:get 响应类型:JsonResult\u003c List\u003c Cart» 3.处理请求,编写控制方法 @RequestMapping(value = \"/show_carts\",method = RequestMethod.GET) public JsonResult\u003cList\u003cCartVo\u003e\u003e showCarts(HttpSession session){ //获取uid Integer uid = getUserIdFromSession(session); //获取数据 List\u003cCartVo\u003e data = cartService.findAllCartByUid(uid); //返回数据 return new JsonResult\u003c\u003e(OK,data); } 4.前端页面 \u003cscript type=\"text/javascript\"\u003e function showCarts(){ $.ajax({ url: \"/cart/show_carts\", type: \"get\", dataType: \"json\", success: function (res) { if (res.data.length !== 0){ //先清空列表 $(\"#cart-list\").empty(); for (let i = 0; i \u003c res.data.length; i++) { let cart = res.data[i]; let idNum = i; let image = \"..\" + cart.image + \"collect.png\"; let totalPrice = cart.price * cart.num let str = \"\u003ctr\u003e\" +\"\u003ctd\u003e\u003cinput onclick='checkOne()' id=cid\" + idNum + \" name='cids' value=\" + cart.cid + \" type='checkbox' class='ckitem' /\u003e\u003c/td\u003e\" + \"\u003ctd\u003e\u003cimg src=\" + image + \" class='img-responsive' /\u003e\u003c/td\u003e\" + \"\u003ctd\u003e\" + cart.title + \"\u003c/td\u003e\" + \"\u003ctd\u003e¥\u003cspan id=\"+ \"goodsPrice\" + idNum +\"\u003e\"+ cart.price + \"\u003c/span\u003e\u003c/td\u003e\" + \"\u003ctd\u003e\" + \"\u003cinput id=\" + \"countRec\" + idNum + \" type='button' value='-' class='num-btn' onclick='ajaxProductCountRec(#{idNum})' /\u003e\" + \"\u003cinput id=\" + \"goodsCount\"+ idNum + \" type='text' size='2' readonly='readonly' class='num-text' value=\" + cart.num + \"\u003e\" + \"\u003cinput id=\" + \"countAdd\" + idNum + \" class='num-btn' type='button' value='+' onclick='ajaxProductCountAdd(#{idNum})' /\u003e\" + \"\u003c/td\u003e\" + \"\u003ctd\u003e\u003cspan id=\" + \"goodsCast\" + idNum + \"\u003e¥\" + totalPrice + \"\u003c/span\u003e\u003c/td\u003e\" + \"\u003ctd\u003e\" + \"\u003cinput type='button' onclick='delCartItem(#{deletedId})' class='cart-del btn btn-default btn-xs' value='删除' /\u003e\" + \"\u003c/td\u003e\" + \"\u003c/tr\u003e\" //替换数字 str = str.replaceAll(\"#{idNum}\",idNum) str = str.replaceAll(\"#{deletedId}\",cart.cid) //在表格中插入数据 $(\"#cart-list\").append(str) // 计算商品总数量和总价格 totalNum += 1; countPrice = countPrice + totalPrice; } }else{ str = \"\u003ctr\u003e\u003ctd colspan='12' style='font-weight: bold;color: red;padding: 20px;font-size: medium'\u003e\" + \"购物车暂无商品,请先去添加商品\u003c/td\u003e\u003c/tr\u003e\" $(\"#cart-list\").empty().append(str) } }, error : function (err) { alert(\"服务器出现错误,查询失败!\") } ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:5:3","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.5 删除商品 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:6:0","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.5.1 后端-持久层 1.编写sql DELETE FROM t_cart WHERE cid = #{cid} 2.编写Mapper接口的抽象方法 /** * 按照购物车id 删除购物车内容物 * @param cid 购物车id * @return 返回影响行数 */ Integer deleteCartByCid(Integer cid); 3.编写Mapper接口的映射文件 \u003c!-- Integer deleteCartByCid(Integer cid);--\u003e \u003cdelete id=\"deleteCartByCid\"\u003e DELETE FROM t_cart WHERE cid = #{cid} \u003c/delete\u003e 4.单元测试 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:6:1","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.5.2 后端-业务层 1.处理异常 删除异常已经被定义 2.定义业务层抽象方法 /** * 按照cid删除购物车数据 * @param cid cid */ void deleteCartByCid(Integer cid); 3.定义抽象方法实现逻辑 @Override public void deleteCartByCid(Integer cid) { //删除 Integer row = cartMapper.deleteCartByCid(cid); //判断删除结果 if(row!=1){ throw new DeleteException(\"购物车数据删除异常!\"); } } 4.单元测试 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:6:2","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.5.3 后端-控制层 1.处理异常 2.\b请求设计 请求地址:/cart/delete_cart 请求参数:Integer cid 请求类型:post 响应类型:JsonResult 3,处理请求,编写控制层方法 @RequestMapping(value = \"/delete_cart\",method = RequestMethod.GET) public JsonResult\u003cVoid\u003e showCarts(Integer cid){ cartService.deleteCartByCid(cid); return new JsonResult\u003c\u003e(OK); } 4.前端页面 //给每个删除按钮绑定点击事件 function delCartItem(cid){ if (confirm(\"确定要删除这条商品吗?\")){ $.ajax({ url: \"/cart/delete_cart\", type: \"post\", data: {\"cid\":cid}, dataType: \"json\", success:function (res) { alert(\"删除成功\") location.reload(); }, error:function (error) { alert(\"删除失败\") } }) } } ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:6:3","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.6 购物车数目增减 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:7:0","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.6.1 数目增减-持久层 1.编写sql //根据cid更新用户商品数量 UPDATE t_cart SET num = #{num},modified_user = #{modifiedUser}, #modified_time = #{modifiedTime} WHERE cid = #{cid} //根据cid查询用户cart信息 SELECT * FROM t_cart WHERE cid = #{cid}; 2.编写Mapper接口抽象方法 /** * 按照cid查询Cart * @param cid * @return */ Cart findCartByCid(Integer cid); /** * 按照cid增减购物车商品的数量 * @param num 数量 * @param cid 购物车id * @param modifiedUser 操作人 * @param modifiedTime 操作时间 * @return */ Integer updateCartNumByCid(Integer num,Integer cid,String modifiedUser,Date modifiedTime); 3.编写Mapper接口映射文件 \u003c!-- Cart findCartByCid(Integer cid);--\u003e \u003cselect id=\"findCartByCid\" resultType=\"com.zxl.store.entity.Cart\"\u003e SELECT * FROM t_cart WHERE cid = #{cid} \u003c/select\u003e \u003c!-- Integer updateCartNumByCid(Integer num,Integer cid,String username,Date time);--\u003e \u003cupdate id=\"updateCartNumByCid\"\u003e UPDATE t_cart SET num = #{num},modified_user = #{modifiedUser},modified_time = #{modifiedTime} WHERE cid = #{cid} \u003c/update\u003e 4.单元测试 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:7:1","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.6.2 数目增减-业务层 1.处理异常 2.编写业务层抽象方法 /** * 更新购物车商品数量 * @param cid * @param num * @param modifiedUser */ void updateCartNumByCid(Integer cid,Integer num,String modifiedUser); 3.编写实现逻辑 @Override public void updateCartNumByCid(Integer cid, Integer num, String modifiedUser) { //查询购物车数据 Cart res = cartMapper.findCartByCid(cid); if(res==null){ throw new CartInfoNotExistsException(\"购物车数据不存在\"); } //添加 Integer row = cartMapper.updateCartNumByCid(num, cid, modifiedUser, new Date()); //判断 if(row!=1){ throw new UpdateException(\"更新购物车商品数目出现未知异常\"); } } 4.单元测试 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:7:2","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.6.3 数目增减-控制层 1.异常处理 2. 设计请求 请求地址:/cart/update_num 请求参数:Integer cid,Integer num,HttpSession session 请求类型:post 响应类型:JsonResult\u003c void\u003e 3.处理请求,编写控制类 @RequestMapping(value = \"/update_num\",method = RequestMethod.POST) public JsonResult\u003cVoid\u003e updateNum(Integer cid,Integer num,HttpSession session){ String modifiedUser = getUsernameFromSession(session); cartService.updateCartNumByCid(cid,num,modifiedUser); return new JsonResult\u003c\u003e(OK); } 4. 前端页面 /*按加号数量增*/ function addNum(num) { var n = parseInt($(\"#goodsCount\"+num).val()); $(\"#goodsCount\"+num).val(n + 1); calcRow(num); } /*按减号数量减少*/ function reduceNum(num) { var n = parseInt($(\"#goodsCount\"+num).val()); if (n == 0) return; $(\"#goodsCount\"+num).val(n - 1); calcRow(num); } //计算单行小计价格的方法 function calcRow(num) { //取单价 parseFloat() 函数可解析一个字符串,并返回一个浮点数。 var vprice = parseFloat($(\"#goodsPrice\"+num).html()); //取数量 var vnum = parseFloat($(\"#goodsCount\"+num).val()); //小计金额 var vtotal = vprice * vnum; //赋值 $(\"#goodsCast\"+num).html(\"¥\" + vtotal); } //向服务器发送ajax请求减少用户购物车的商品数量 function ajaxProductCountRec(num){ reduceNum(num); let cid = $(\"#cid\"+num).val(); let updateNum = $(\"#goodsCount\"+num).val() $.ajax({ url : \"/cart/update_num\", type: \"post\", dataType: \"json\", data:{cid:cid,num:updateNum}, error: function () { alert(\"增加失败,请等待攻城狮修复!!\") } }) } //向服务器发送ajax请求增加用户购物车的商品数量 function ajaxProductCountAdd(num){ addNum(num) let cid = $(\"#cid\"+num).val(); let updateNum = $(\"#goodsCount\"+num).val() $.ajax({ url : \"/cart/update_num\", type: \"post\", dataType: \"json\", data:{cid:cid,num:updateNum}, error: function () { alert(\"增加失败,请等待攻城狮修复!!\") } }) } ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:7:3","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.7 显示勾选的购物车数据 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:8:0","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.7.1 后端-持久层 1.编写sql 用户在购物车列表随机勾选相关商品,在点击结算按钮之后,跳转到结算页面,在这个页面中需要展示上个页面所勾选的购物车对应数据 SELECT c.cid, c.uid, c.pid, c.price, c.num, p.title, p.image, p.price AS realPrice FROM t_cart c LEFT JOIN t_product p ON c.pid=p.id WHERE c.cid IN (?,?,?) ORDER BY c.created_time DESC ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:8:1","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"2.编写Mapper接口抽象方法 /** * 按照cids查询购物车数据 * @param cids * @return */ List\u003cCartVo\u003e findVoByCid(Integer[] cids); 3.编写映射文件 \u003c!-- List\u003cCartVo\u003e findVoByCid(Integer[] cids);--\u003e \u003cselect id=\"findVoByCid\" resultType=\"com.zxl.store.vo.CartVo\"\u003e SELECT c.cid, c.uid, c.pid, c.price, c.num, p.title, p.image, p.price AS realPrice FROM t_cart c LEFT JOIN t_product p ON c.pid=p.id WHERE c.cid IN ( \u003cforeach collection=\"array\" item=\"cid\" separator=\",\"\u003e #{cid} \u003c/foreach\u003e ) ORDER BY c.created_time DESC \u003c/select\u003e 4 单元测试 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:8:2","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.7.2 后端-业务层 1.编写异常 2.编写业务层抽象方法 /** * 按照cids查询购物车数据 * @param cids * @return */ List\u003cCartVo\u003e getVoByCid(Integer[] cids); 3.编写实现逻辑 @Override public List\u003cCartVo\u003e getVoByCid(Integer[] cids) { //查询数据 List\u003cCartVo\u003e data = cartMapper.findVoByCid(cids); //传输数据 return data; } 4.单元测试 ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:8:3","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"6.7.3 后端-控制层 1.处理异常 2.设计请求 请求地址:/cart/list 请求参数:Integer[] cids 请求类型:get 响应类型:JsonResult\u003cList\u003e 3.处理请求,编写处理方法 @RequestMapping(value = \"/list\",method = RequestMethod.GET) public JsonResult\u003cList\u003cCartVo\u003e\u003e getCartCids(Integer[] cids){ List\u003cCartVo\u003e data = cartService.getVoByCid(cids); return new JsonResult\u003c\u003e(OK,data); } 4.前端页面 1.将cart.html页面中结算按钮属性变更成type=“submit” 2.orderConfirm.html页面中添加自动加载从上个页面中传递过来的cids数据,再去请求ajax中进行填充当前页面区域中 \u003cscript type=\"text/javascript\"\u003e //展示从购物车界面选中的商品信息 function showOrderItem(){ data = location.search.substr(1);//截取地址栏url?后的第二个元素,即购物车商品的cid //记录商品总数和总价格 let totalNum = 0; let countPrice = 0; //自动发送ajax请求查询url地址上的cid信息 $.ajax({ url : \"/cart/list\", type: \"get\", data: data, dataType: \"json\", success:function(res){ if (res.state === 200){ //填充信息 $(\"#cart-list\").empty() for (let i = 0; i \u003c res.data.length; i++) { let str = \"\"; let cartVo = res.data[i] let totalPrice = cartVo.price * cartVo.num str = \"\u003ctr\u003e\" + \"\u003ctd id=cid\" + i + \" hidden='hidden'\u003e\" + cartVo.cid + \"\u003c/td\u003e\" + \"\u003ctd\u003e\u003cimg src=..\" + cartVo.image + \"collect.png\" + \" class='img-responsive' /\u003e\u003c/td\u003e\" + \"\u003ctd\u003e\" + cartVo.title + \"\u003c/td\u003e\" + \"\u003ctd\u003e¥\u003cspan\u003e\" + cartVo.price + \"\u003c/span\u003e\u003c/td\u003e\" + \"\u003ctd id=num\" + i + \" \u003e\" + cartVo.num + \"\u003c/td\u003e\" + \"\u003ctd\u003e\u003cspan\u003e\" + totalPrice + \"\u003c/span\u003e\u003c/td\u003e\" + \"\u003c/tr\u003e\" $(\"#cart-list\").append(str) //计算商品的数量和总金额 totalNum = totalNum + cartVo.num countPrice = countPrice + totalPrice; } $(\"#all-count\").empty().html(totalNum) $(\"#all-price\").empty().html(countPrice) }else{ location.href = \"500.html\" } }, error: function () { location.href = \"500.html\" } }); } $(function () { //页面加载完成执行查找 showOrderItem(); }); ","date":"2022-10-16","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/:8:4","tags":["computerStore"],"title":"SpringBoot项目-购物车管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E8%B4%AD%E7%89%A9%E8%BD%A6%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5 商品管理 ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:0:0","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.1 创建数据表 DROP TABLE IF EXISTS t_product; CREATE TABLE t_product ( id int(20) NOT NULL COMMENT '商品id', category_id int(20) DEFAULT NULL COMMENT '分类id', item_type varchar(100) DEFAULT NULL COMMENT '商品系列', title varchar(100) DEFAULT NULL COMMENT '商品标题', sell_point varchar(150) DEFAULT NULL COMMENT '商品卖点', price bigint(20) DEFAULT NULL COMMENT '商品单价', num int(10) DEFAULT NULL COMMENT '库存数量', image varchar(500) DEFAULT NULL COMMENT '图片路径', status int(1) DEFAULT '1' COMMENT '商品状态 1:上架 2:下架 3:删除', priority int(10) DEFAULT NULL COMMENT '显示优先级', created_time datetime DEFAULT NULL COMMENT '创建时间', modified_time datetime DEFAULT NULL COMMENT '最后修改时间', created_user varchar(50) DEFAULT NULL COMMENT '创建人', modified_user varchar(50) DEFAULT NULL COMMENT '最后修改人', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; //具体录入信息参照githu ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:1:0","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.2 定义实体类 /** * @author zxl * @description 商品的实体类 * @date 2022/11/4 */ @Data @AllArgsConstructor @NoArgsConstructor public class Product extends BaseEntity{ private Integer id; private Integer categoryId ; private String itemType; private String title; private String sellPoint; private Long price; private Integer num; private String image; private Integer status; private String priority; } ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:2:0","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.3 热销商品 ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:3:0","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.3.1 商品排行-持久层 1.编写sql语句 //查询热销商品的sql SELECT * FROM t_product WHERE status = 1 ORDER BY priorty DESC LIMIT 0,4 2.编写Mapper接口和抽象方法 /** * @author zxl * @description 处理商品数据的Mapper接口 * @date 2022/11/4 */ public interface ProductMapper { /** * 按照priority查找热销前五的商品数据 * @return 返回商品数据集合 */ List\u003cProduct\u003e findHotList(); } 3.编写Mapper接口的映射 \u003cmapper namespace=\"com.zxl.store.mappers.ProductMapper\"\u003e \u003c!-- List\u003cProduct\u003e findHotList();--\u003e \u003cselect id=\"findHotList\" resultType=\"com.zxl.store.entity.Product\"\u003e SELECT * FROM t_product WHERE status = 1 ORDER BY priority DESC LIMIT 0,4 \u003c/select\u003e \u003c/mapper\u003e 4.单元测试 ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:3:1","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.3.2 商品排行-业务层 1.异常规划 没有异常 2.定义业务层接口和抽象方法 /** * @author zxl * @description 处理商品的业务层接口 * @date 2022/11/4 */ public interface IProductService { /** * 按照priority查找热销前五的商品数据 * @return 返回商品数据集合 */ List\u003cProduct\u003e findHotList(); } 3.编写业务层方法逻辑 /** * @author zxl * @description 处理商品的业务层实现类 * @date 2022/11/4 */ public class IProductServiceImpl implements IProductService { @Autowired(required = false) private ProductMapper productMapper; @Override public List\u003cProduct\u003e findHotList() { //查找数据 List\u003cProduct\u003e res = productMapper.findHotList(); return res; } } 4.单元测试 ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:3:2","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.3.3 商品排行-控制层 1.异常处理 没有异常 2.设计请求 请求路径:/product/hot_list 请求参数:无 请求类型:get 响应类型:JsonResult\u003cList\u003c Product» 3.处理请求,编写控制层方法 /** * @author zxl * @description 处理商品相关请求的控制类 * @date 2022/11/4 */ @RestController @RequestMapping(\"/product\") public class ProductController extends BaseController { @Autowired(required = false) private IProductService productService; /** * 返回热销商品 * @return */ @RequestMapping(value = \"/host_list\",method = RequestMethod.GET) public JsonResult\u003cList\u003cProduct\u003e\u003e getHotList(){ List\u003cProduct\u003e data = productService.findHotList(); return new JsonResult\u003c\u003e(OK,data); } } 4.前端页面 function showHotList() { $(\"#hot-list\").empty(); $.ajax({ url:\"/product/hot_list\", type:\"GET\", dataType: \"json\", success: function (res) { for (let i = 0; i \u003c res.data.length; i++) { let str = \"\"; let product = res.data[i] let image = \"..\" + product.image + \"collect.png\" str = \"\u003cdiv class='col-md-12'\u003e\" + \"\u003cdiv class=\\\"col-md-7 text-row-2\\\"\u003e\" + \"\u003ca href='javascript:void(0);'onclick='jumpWithId(#{id})'\u003e\" + product.title + \"\u003c/a\u003e\" + \"\u003c/div\u003e\" + \"\u003cdiv class=\\\"col-md-2\\\"\u003e¥\" + product.price + \"\u003c/div\u003e\" + \"\u003cdiv class=\\\"col-md-3\\\"\u003e\" + \"\u003cimg src=\" + image + \" class='img-responsive' /\u003e\" + \"\u003c/div\u003e\" + \"\u003c/div\u003e\" str = str.replaceAll(\"#{id}\",product.id) $(\"#hot-list\").append(str) } }, error: function () { alert(\"查询错误,请等待攻城狮修复!!\") } }); } ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:3:3","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.4 新到商品 ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:4:0","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.4.1 新到商品-持久层 1.编写sql语句 //按照上架物品的修改时间查询新商品 SELET * FROM t_product WHERE status = 1 ORDER BY modified_time DESC LIMIT 0,4 2.定义抽象方法 /** * 按照创建时间查找新到的商品 * @return 返回新商品列表 */ List\u003cProduct\u003e findNewProductList(); 3.定义mapper接口映射文件 \u003c!-- List\u003cProduct\u003e findNewProductList();--\u003e \u003cselect id=\"findNewProductList\" resultType=\"com.zxl.store.entity.Product\"\u003e SELECT * FROM t_product WHERE status = 1 ORDER BY modified_time DESC LIMIT 0,4 \u003c/select\u003e 4.单元测试 ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:4:1","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.4.2 新到商品-业务层 1.定义异常 无 2.定义业务层抽象方法 /** * 按照商品状态和创建时间选取新商品集合 * @return */ List\u003cProduct\u003e findNewProductList(); 3. 编写业务层逻辑 @Override public List\u003cProduct\u003e findNewProductList() { //查找数据 List\u003cProduct\u003e res = productMapper.findNewProductList(); return res; } 4. 单元测试 ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:4:2","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.4.3 新到商品-控制层 1.处理异常 无 2.定义请求 请求路径:/product/new_list 请求参数:无 请求类型:get 响应类型:JsonResult\u003cList\u003c Product» 3.处理请求,编写控制方法 /** * 返回新商品 * @return */ @RequestMapping(value = \"/new_list\",method = RequestMethod.GET) public JsonResult\u003cList\u003cProduct\u003e\u003e getNewList(){ List\u003cProduct\u003e data = productService.findNewProductList(); return new JsonResult\u003c\u003e(OK,data); } 4.前端页面 function showNewList() { $(\"#new-list\").empty(); $.ajax({ url:\"/product/new_list\", type:\"GET\", dataType: \"json\", success: function (res) { for (let i = 0; i \u003c res.data.length; i++) { let str = \"\"; let product = res.data[i] let image = \"..\" + product.image + \"collect.png\" str = \"\u003cdiv class='col-md-12'\u003e\" + \"\u003cdiv class=\\\"col-md-7 text-row-2\\\"\u003e\" + \"\u003ca href='javascript:void(0);'onclick='jumpWithId(#{id})'\u003e\" + product.title + \"\u003c/a\u003e\" + \"\u003c/div\u003e\" + \"\u003cdiv class=\\\"col-md-2\\\"\u003e¥\" + product.price + \"\u003c/div\u003e\" + \"\u003cdiv class=\\\"col-md-3\\\"\u003e\" + \"\u003cimg src=\" + image + \" class='img-responsive' /\u003e\" + \"\u003c/div\u003e\" + \"\u003c/div\u003e\" str = str.replaceAll(\"#{id}\",product.id) $(\"#new-list\").append(str) } }, error: function () { alert(\"查询错误,请等待攻城狮修复!!\") } }); } ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:4:3","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.5 显示商品 ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:5:0","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.5.1 显示商品-持久层 1.编写sql //查找按照商品id查找商品 SELECT id,title,price,image FROM t_product WHERE id = #{id} 2.编写Mapper接口的抽象方法 /** * 按照商品的id查找商品 * @param pid * @return */ Product findProductById(Integer id); 3.编写Mapper接口的映射文件 \u003c!-- Product findProductById(Integer id);--\u003e \u003cselect id=\"findProductById\" resultType=\"com.zxl.store.entity.Product\"\u003e SELECT * FROM t_product WHERE status = 1 AND id = #{id} \u003c/select\u003e 4.单元测试 ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:5:1","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.5.2 显示商品-业务层 1.异常处理 商品为找到异常 商品状态异常(为未上架或者已经删除的商品) /** * @author zxl * @description 商品未找到异常 * @date 2022/11/4 */ public class ProductNotFoundException extends ServiceException {} /** * @author zxl * @description 商品状态异常 * @date 2022/11/4 */ public class ProductStatusException extends ServiceException {} 2.编写业务层接口的抽象方法 /** * 根据商品id查找商品 * @param id 商品id * @return 返回商品信息 */ Product findProductById(Integer id); 3.编写业务层具体逻辑 @Override public Product findProductById(Integer id) { //根据id查询商品信息 Product res = productMapper.findProductById(id); //判断商品存在或者商品状态是否为上架 if(res==null){ throw new ProductNotFoundException(\"查询商品不存在\"); } if(res.getStatus()!=1){ throw new ProductStatusException(\"查询商品尚未上架\"); } //传输商品信息 return res; } 4.单元测试 ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:5:2","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"5.5.3 显示商品-控制层 1.异常处理 将上述两个异常加入BaseConrtroller 2.设计请求 请求路径:/product/{id} 请求参数:Integet id 请求类型:get 响应类型:JsonResult\u003c Product\u003e 3.处理请求,设计控制层方法 @RequestMapping(value = \"/{id}\") public JsonResult\u003cProduct\u003e findProductById(@PathVariable(\"id\")Integer id){ //按照获得的id查询商品 Product data = productService.findProductById(id); return new JsonResult\u003c\u003e(OK,data); } 4.前端页面 //index.html function jumpWithId(id) { let jumpUrl = \"product.html?id=\"+id; location.href=jumpUrl; } //function //返回一个参数 function getOne(){ var result; //返回字符串从url的?处开始 var url = decodeURI(window.location.search); //如果等于-1,代表没有找到,即网页连接没有携带任何参数 if (url.indexOf(\"?\") != -1){ //返回一个新的字符串,从url连接=符号处索引+1的位置开始返回 result = url.substr(url.indexOf(\"=\")+1); } return result; } //product.html //接收上一个页面传来的连接 var pid = getOne(); function showInThisProductHtml(){ //判断是否携带参数 if (location.search.substring(1).indexOf(\"id\") === -1){ location.href = \"500.html\" return false; } //在页面加载完成时自动发送此ajax请求并填充表单 $.ajax({ url: \"/product/\" + pid, type: \"get\", dataType: \"json\", success:function (res) { let product = res.data; //将普通的数据填充至页面 $(\"#product-title\").text(product.title) $(\"#product-sell-point\").append(product.sellPoint) $(\"#product-price\").text(product.price) $(\"#stock\").text(product.num) //将数据库查询的图片进行替换 let image = \"..\" + product.image //遍历填充图片数据 for (let i = 1; i \u003c= 5; i++) { $(\"#product-image-\" + i + \"-big\").attr(\"src\",image + i + \"_big.png\") $(\"#product-image-\" + i).attr(\"src\",image + i + \".jpg\") } } }) } ","date":"2022-10-15","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/:5:3","tags":["computerStore"],"title":"SpringBoot项目-商品管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%95%86%E5%93%81%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.地址管理 收货地址的功能模块:列表展示,修改,删除,设置默认,新增收货地址 开发的顺序:新增收货地址-\u003e列表展示-\u003e设置默认收货地址-\u003e修改删除 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:1:0","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.1 创建数据表 创建数据表 CREATE TABLE t_address ( aid INT AUTO_INCREMENT COMMENT '收货地址id', uid INT COMMENT '归属的用户id', NAME VARCHAR(20) COMMENT '收货人姓名', province_name VARCHAR(15) COMMENT '省-名称', province_code CHAR(6) COMMENT '省-行政代号', city_name VARCHAR(15) COMMENT '市-名称', city_code CHAR(6) COMMENT '市-行政代号', area_name VARCHAR(15) COMMENT '区-名称', area_code CHAR(6) COMMENT '区-行政代号', zip CHAR(6) COMMENT '邮政编码', address VARCHAR(50) COMMENT '详细地址', phone VARCHAR(20) COMMENT '手机', tel VARCHAR(20) COMMENT '固话', tag VARCHAR(6) COMMENT '标签', is_default INT COMMENT '是否默认:0-不默认,1-默认', created_user VARCHAR(20) COMMENT '创建人', created_time DATETIME COMMENT '创建时间', modified_user VARCHAR(20) COMMENT '修改人', modified_time DATETIME COMMENT '修改时间', PRIMARY KEY (aid) ) ENGINE=INNODB DEFAULT CHARSET=utf8; ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:1:1","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.2 创建实体类 地址类 @Data @AllArgsConstructor @NoArgsConstructor public class Address extends BaseEntity{ /* * aid INT AUTO_INCREMENT COMMENT '收货地址id', uid INT COMMENT '归属的用户id', name VARCHAR(20) COMMENT '收货人姓名', province_name VARCHAR(15) COMMENT '省-名称', province_code CHAR(6) COMMENT '省-行政代号', city_name VARCHAR(15) COMMENT '市-名称', city_code CHAR(6) COMMENT '市-行政代号', area_name VARCHAR(15) COMMENT '区-名称', area_code CHAR(6) COMMENT '区-行政代号', zip CHAR(6) COMMENT '邮政编码', address VARCHAR(50) COMMENT '详细地址', phone VARCHAR(20) COMMENT '手机', tel VARCHAR(20) COMMENT '固话', tag VARCHAR(6) COMMENT '标签', is_default INT COMMENT '是否默认:0-不默认,1-默认', created_user VARCHAR(20) COMMENT '创建人', created_time DATETIME COMMENT '创建时间', modified_user VARCHAR(20) COMMENT '修改人', modified_time DATETIME COMMENT '修改时间', */ private Integer aid; private Integer uid; private String name; private String provinceName; private String provinceCode; private String cityName; private String cityCode; private String areaName; private String areaCode; private String zip; private String address; private String phone; private String tel; private String tag; private Integer isDefault; } ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:1:2","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.3 新增地址 4.3.1 后端-持久层 1.编写sql语句 //插入语句 insert into t_address(除了aid外字段)value(字段值) //查看收货地址数量(一个用户最大只能20条) select count(*) from t_address where uid = ? 2.定义Mapper接口和抽象方法 /** * @author zxl * @version 1.0.0 * @date 2022/11/02 * @desciption Address所对应的Mapper接口 */ public interface AddressMapper { /** * 插入用户的收货地址数据 * @param address 收货地址 * @return {@link Integer} 受影响的行数 */ Integer addAddress(Address address); /** * 根据用户id统计用户收货地址数量 * @param uid 用户uid * @return {@link Integer} 返回当前用户的收货地址总数 */ Integer userAddressCount(Integer uid); } 3.实现Mapper接口的映射 \u003cmapper namespace=\"com.zxl.store.mappers.AddressMapper\"\u003e \u003c!--addAddress 插入用户的地址--\u003e \u003cinsert id=\"addAddress\" useGeneratedKeys=\"true\" keyProperty=\"aid\"\u003e INSERT INTO t_address ( uid,name, province_name, province_code, city_name, city_code, area_name, area_code, zip, address, phone, tel, tag, is_default, created_user, created_time, modified_user, modified_time ) VALUES ( #{uid}, #{name}, #{provinceName}, #{provinceCode}, #{cityName}, #{cityCode}, #{areaName}, #{areaCode}, #{zip}, #{address}, #{phone}, #{tel}, #{tag}, #{isDefault}, #{createdUser}, #{createdTime}, #{modifiedUser}, #{modifiedTime} ) \u003c/insert\u003e \u003c!--userAddressCount 根据用户uid查询用户地址数量--\u003e \u003cselect id=\"userAddressCount\"\u003e SELECT COUNT(*) FROM t_address WHERE uid = #{uid} \u003c/select\u003e \u003c/mapper\u003e 4.单元测试Mapper接口 4.3.2 后端-业务层 如果用户插入的地址是第一条时,将其设为默认的收货地址 1. 规划异常 用户地址超出数量异常 /** * @author zxl * @description 用户地址数目超出异常 * @date 2022/11/2 */ public class AddressCountLimitException extends ServiceException {} 2.接口和抽象方法 /** * @author zxl * @description 收货地址业务层接口 * @date 2022/11/2 */ public interface IAddressService { /** * 添加用户地址 * @param uid 用户uid * @param username 用户名 * @param address 地址 */ void addAddress(Integer uid, String username, Address address); } 3.实现抽象方法 /** * @author zxl * @description 收货地址业务层的实现类 * @date 2022/11/2 */ @Service public class IAddressServiceImpl implements IAddressService { @Autowired private AddressMapper addressMapper; @Value(\"${user.address.max-count}\") private Integer maxCount; @Override public void addAddress(Integer uid, String username, Address address) { //先判断用户地址的条数 Integer count = addressMapper.userAddressCount(uid); //判断是否超过20条记录 if(count\u003e=maxCount){ throw new AddressCountLimitException(\"用户地址已经到达上限,请删除部分地址\"); } //判断当前地址是否为0 if(count==0){ address.setIsDefault(1); } //补全四项日志 Date currentTime = new Date(); address.setCreatedUser(username); address.setCreatedTime(currentTime); address.setModifiedUser(username); address.setModifiedTime(currentTime); //插入 Integer row = addressMapper.addAddress(address); //判断插入结果 if(row!=1){ throw new InsertException(\"插入时产生未知异常\"); } } } 4.单元测试 4.3.3 后端控制层 1.处理异常 将异常添加到全局异常处理 2.设计请求 请求路径:/address 请求参数:Address address,HttpSession session 请求类型:post 响应类型:JsonResult\u003c Void\u003e 3.处理请求,创建相应controller /** * @author zxl * @description 地址操作的控制层 * @date 2022/11/2 */ @RestController @RequestMapping(\"/address\") public class AddressController extends BaseController{ @Autowired private IAddressService addressService; /** * 处理用户添加地址的操作 * @param address 添加的地址 * @param session 项目自动生成的Session * @return {@link JsonResult}\u003c{@link Void}\u003e */ @PostMapping public JsonResult\u003cVoid\u003e addAddress(Address address, HttpSession session){ //从Session中获取Uid和Username Integer uid = getUserIdFromSession(session); String username = getUsernameFromSession(session); //添加 addressService.addAddress(uid,username,address); return new JsonResult\u003c\u003e(OK); } } 4.前端页面 \u003cscript type=\"text/javascript\"\u003e $(document).ready(function () { $(\"#btn-add-new-address\").click(function() { //判断手机号和收货人是否为空 let name = $(\"#name\").val(); let phone = $(\"#phone\").val(); let zip = $(\"#zip\").val(); let tag = $(\"#tag\").val(); if (phone == \"\" || name == \"\"){ $(\"#error-msg\").html(\"请先填写需要添加的信息!\"); return false; } //验证手机号是否符合要求 let checkPhone = /(^1\\d{10}$)|(^[0-9]\\d{7}$)/; if (!checkPhone.test(phone)){ $(\"#error-msg\").html(\"手机号不符合要求!\"); return false; } //验证邮箱是否为空或者超出最大长度6 if(zip.length\u003e=6){ $(\"#error-msg\").html(\"邮箱的最大长度6\"); return false; } //验证地址类型不可以超过6 if(tag.length\u003e=6){ $(\"","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:1:3","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.4 获取省市区列表 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:2:0","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.4.1 创建数据表 //parent属性代表的是父区域的代码号,省的父代码号是+86 //code代表的是本身的代码号 //name就是code所代表的本身的名称 CREATE TABLE t_dict_district ( id int(11) NOT NULL AUTO_INCREMENT, parent varchar(6) DEFAULT NULL, code varchar(6) DEFAULT NULL, name varchar(16) DEFAULT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; //部分省市区数据太大 //请参考代码 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:2:1","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.4.2 创建实体类 创建District实体类 ** * @author zxl * @description 省市区的实体类 * @date 2022/11/3 */ @Data @AllArgsConstructor @NoArgsConstructor public class District extends BaseEntity { private Integer id; private String parent; private String code; private String name; } ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:2:2","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.4.3 后端-持久层 1.编写sql语句 //1.查询语句,根据父代号查询 (ASC 升序) SELECT * FROM t_dict_district WHERE parent = ? ORDER BY code ASC //2.根据code查询对应省市区名字 SELECT name FROM t_dict_district WHERE code = ? 2.定义Mapper接口和抽象方法 public interface DistrictMapper { /** * 根据用户的父代号查询区域信息 * @param parent 父代号 * @return 某个父区域下的所有区域列表 */ List\u003cDistrict\u003e findDistrictByParent(String parent); /** * 根据code查询区域名称 * @param code 区域代号 * @return 返回区域名称 */ String findNameByCode(String code); } 3.编写映射文件 \u003cmapper namespace=\"com.zxl.store.mappers.DistrictMapper\"\u003e \u003c!-- List\u003cDistrict\u003e findDistrictByParent(String parent);--\u003e \u003cselect id=\"findDistrictByParent\" resultType=\"com.zxl.store.entity.District\"\u003e SELECT * FROM t_dict_district WHERE parent = #{parent} ORDER BY code ASC \u003c/select\u003e \u003c!-- String findNameByCode(String code);--\u003e \u003cselect id=\"findNameByParent\" resultType=\"java.lang.String\"\u003e SELECT name FROM t_dict_district WHERE code = #{code} \u003c/select\u003e \u003c/mapper\u003e 4.单元测试 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:2:3","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.4.4 后端-业务层 1.规划异常 暂无异常可以规划 2.定义Service接口和抽象方法 /** * @author zxl * @description District业务层的接口类 * @date 2022/11/3 */ public interface IDistrictService { /** * 根据父代号查询区域信息(省市区) * @param parent 父代号 * @return 返回多个查询结果 */ List\u003cDistrict\u003e getDistrictByParent(String parent); /** * 按照code查询当前省市区名称 * @param code code * @return 返回省市区名称 */ String getNameByCode(String code); } 3.编写具体的实现方法和处理逻辑 /** * @author zxl * @description 处理省市区业务层接口的实现类 * @date 2022/11/3 */ @Service public class IDistrictServiceImpl implements IDistrictService { @Autowired(required = false) private DistrictMapper districtMapper; //根据父代号查询省市区的信息 @Override public List\u003cDistrict\u003e getDistrictByParent(String parent) { List\u003cDistrict\u003e districtByParent = districtMapper.findDistrictByParent(parent); //无效数据设为null for (District district : districtByParent) { district.setId(null); district.setParent(null); } return districtByParent; } //根据code查询省市区的名称 @Override public String getNameByCode(String code) { return districtMapper.findNameByCode(code); } } 4.单元测试 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:2:4","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.4.5 后端-控制层 1.异常处理 没有异常,不需要处理 2.设计请求 请求路径:/district/parent 请求参数:String parent 请求类型:get 响应类型:JsonResult\u003cList\u003c District» 3.处理请求,创建一个控制类 /** * @author zxl * @description 处理省市区相关业务的控制层 * @date 2022/11/3 */ @RestController @RequestMapping(\"/district\") public class DistrictController extends BaseController{ @Autowired(required = false) private IDistrictService districtService; @RequestMapping(value = \"/parent\",method = RequestMethod.GET) public JsonResult\u003cList\u003cDistrict\u003e\u003e getDistrictByParent(String parent){ //查询数据 List\u003cDistrict\u003e Data = districtService.getDistrictByParent(parent); //返回数据 return new JsonResult\u003c\u003e(OK,Data); } } 4. 前端页面 \u003cscript type=\"text/javascript\"\u003e $(document).ready(function () { //页面加载完成时,先把三个省市区的提示设置好 let provinceFirst = '\u003coption value=\"0\"\u003e--- 请选择省 ---\u003c/option\u003e'; let cityFirst = '\u003coption value=\"0\"\u003e--- 请选择市 ---\u003c/option\u003e'; let areaFirst = '\u003coption value=\"0\"\u003e--- 请选择区 ---\u003c/option\u003e'; //插入提示框 $(\"#province-list\").empty(); $(\"#city-list\").empty(); $(\"#area-list\").empty(); $(\"#province-list\").append(provinceFirst); $(\"#city-list\").append(cityFirst); $(\"#area-list\").append(areaFirst); //记录省的点击查询次数 let provinceClick = 0; $(\"#province-list\").click(function () { provinceClick++; console.log(provinceClick); //如果provinceClick为1代表首次点击 if(provinceClick==1){ let str = \"\"; $.ajax({ url:\"/district/parent\", type:\"get\", data:\"parent=86\", dataType:\"json\", success:function (json) { //如果返回消息成功,将数据填充回列表 if(json.state==200){ for(i = 0;i\u003cjson.data.length;i++){ //每个district对象 let district = json.data[i]; str = '\u003coption value=\"' + district.code + '\"\u003e' + district.name + '\u003c/option\u003e'; $(\"#province-list\").append(str); } } }, error:function () { alert(\"查询省市区列表错误,请联系管理员修复!\"); } }); } }); //监听省份的选择 为城市的选择做出变化 $(\"#province-list\").change(function () { //清空select下的所有option元素 $(\"#city-list\").empty(); $(\"#area-list\").empty(); //追加默认值 $(\"#city-list\").append(cityFirst); $(\"#area-list\").append(areaFirst); //获取省份选择的是什么 let provinceChoice = $(\"#province-list\").val(); //等于0 则不做请求 if(provinceChoice==\"0\")return false; let str = \"\"; $.ajax({ url:\"/district/parent\", type:\"get\", data:\"parent=\"+provinceChoice, dataType:\"json\", success:function (json) { if(json.state==200){ for( i = 0;i\u003cjson.data.length;i++){ //每个district对象 let district = json.data[i]; str = '\u003coption value=\"' + district.code + '\"\u003e' + district.name + '\u003c/option\u003e'; $(\"#city-list\").append(str); } } }, error:function () { alert(\"查询省市区列表错误,请联系管理员修复!\"); } }); }); //监听城市选择 为区县的选择做出变化 $(\"#city-list\").change(function () { //获取当前选择的城市是什么 let cityChoice = $(\"#city-list\").val(); //清空select下的option元素 $(\"#area-list\").empty(); //重新设置默认值 $(\"#area-list\").append(areaFirst); //判断默认值是什么 //如果是0则不发送ajax请求 if(cityChoice==\"0\")return false; //发送请求 $.ajax({ url:\"/district/parent\", type:\"get\", data:\"parent=\"+cityChoice, dataType:\"json\", success:function (json) { if(json.state==200){ for( i = 0;i\u003cjson.data.length;i++){ //每个district对象 let district = json.data[i]; str = '\u003coption value=\"' + district.code + '\"\u003e' + district.name + '\u003c/option\u003e'; $(\"#area-list\").append(str); } } }, error:function () { alert(\"查询省市区列表错误,请联系管理员修复!\"); } }); }); //添加地址 $(\"#btn-add-new-address\").click(function() { //判断手机号和收货人是否为空 let name = $(\"#name\").val(); let phone = $(\"#phone\").val(); let zip = $(\"#zip\").val(); let tag = $(\"#tag\").val(); //由于不知道的原因导致省市区的名称无法提交,所以进行这一步 let provinceName = $(\"#province-list\").find(\"option:selected\").text(); $(\"#provinceName\").val(provinceName); let cityName = $(\"#city-list\").find(\"option:selected\").text(); $(\"#cityName\").val(cityName); let areaName = $(\"#area-list\").find(\"option:selected\").text(); $(\"#areaName\").val(areaName); if (phone == \"\" || name == \"\"){ $(\"#error-msg\").html(\"请先填写需要添加的信息!\"); return false; } //验证手机号是否符合要求 let checkPhone = /(^1\\d{10}$)|(^[0-9]\\d{7}$)/; if (!checkPhone.test(phone)){ $(\"#error-msg\").html(\"手机号不符合要求!\"); return false; } //验证邮箱是否为空或者超出最大长度6 if(zip.length\u003e=6){ $(\"#error-msg\").html(\"邮箱的最大长度6\"); return false; } //验证地址类型不可以超过6 if(tag.length\u003e=6){ $(\"#error-msg\").html(\"地址类型的最大长度6\"); return false; } $.aja","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:2:5","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.5 获取用户地址 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:3:0","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.5.1 后端-持久层 1.编写sql语句 //获取用户收货地址 SELECT * FROM t_address WHERE uid = ? ORDER BY is_default DESC , created_time DESC 2.定义抽象方法 只需要在AddressMapper接口中定义新的抽象方法即可 /** * 根据用户的uid查询用户的收货地址集合 * @param uid 用户uid * @return 返回收货地址数据 */ List\u003cAddress\u003e findByUid(Integer uid); 3.编写映射文件 \u003c!-- List\u003cAddress\u003e findByUid(Integer uid);--\u003e \u003cselect id=\"findByUid\" resultType=\"com.zxl.store.entity.Address\"\u003e SELECT * FROM t_address WHERE uid = #{uid} ORDER BY is_default DESC ,created_time DESC \u003c/select\u003e 4.创建Mapper的单元测试 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:3:1","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.5.2 后端-业务层 1.异常控制 2.定义业务层抽象方法 /** * 根据用户的uid获取用户的收货地址信息集 * @param uid 用户的uid * @return 返回用户的地址集合 */ List\u003cAddress\u003e getAddressByUid(Integer uid); 3.编写具体的业务处理逻辑 /** * 按照用户uid查询用户的收货地址集合 * @param uid 用户的uid * @return 返回用户的收货地址的集合 */ @Override public List\u003cAddress\u003e getAddressByUid(Integer uid) { //获取结果 List\u003cAddress\u003e res = addressMapper.findByUid(uid); //由于网页端只需要地址类型,收件人姓名,详细地址,联系电话,所以对信息进行部分过滤 for (Address address : res) { address.setUid(null); address.setProvinceCode(null); address.setCityCode(null); address.setAreaCode(null); address.setTel(null); address.setCreatedTime(null); address.setCreatedUser(null); address.setModifiedTime(null); address.setModifiedUser(null); address.setIsDefault(null); } return res; } 4.编写业务层单元测试 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:3:2","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.5.3 后端-控制层 1.异常处理 2.设计请求 请求地址:/address 请求参数:HttpSession session 请求类型:GET 相应类型:JsonResult\u003cList\u003e 3.处理请求,编写控制层 /** * 处理网页端自动显示用户收货地址的请求 * @param session 项目自动生成的session * @return 返回JsonResult\u003cList\u003cAddress\u003e\u003e */ @GetMapping public JsonResult\u003cList\u003cAddress\u003e\u003e queryAllAddress(HttpSession session){ List\u003cAddress\u003e res = addressService.getAddressByUid(getUserIdFromSession(session)); return new JsonResult\u003c\u003e(OK,res); } 4.前端页面 //页面初始化加载用户收货地址信息 $(document).ready(function () { $(\"#address-list\").empty(); $.ajax({ url:\"/address\", type:\"GET\", dataType:\"json\", success:function (json) { if(json.data.length!=0){ let list = json.data; for (let i = 0; i \u003c list.length; i++) { let address = list[i]; let str = \" \"; str = \"\u003ctr\u003e\" +\"\u003ctd\u003e\"+address.tag+\"\u003c/td\u003e\" +\"\u003ctd\u003e\"+address.name+\"\u003c/td\u003e\" +\"\u003ctd\u003e\" +address.provinceName + address.cityName + address.areaName + address.address +\"\u003c/td\u003e\" +\"\u003ctd\u003e\"+address.phone+\"\u003c/td\u003e\" + \"\u003ca href='javascript:void(0);' onclick='updateAddress(#{editAid})' class='btn btn-xs btn-info'\u003e\" + \"\u003cspan class='fa fa-edit'\u003e\u003c/span\u003e修改\" + \"\u003c/a\u003e\" + \"\u003c/td\u003e\" + \"\u003ctd\u003e\" + \"\u003ca href='javascript:void(0);' onclick='deleteAddress(#{deleteAid},#{isDefault})' class='btn btn-xs add-del btn-info'\u003e\" + \"\u003cspan class='fa fa-trash-o'\u003e\u003c/span\u003e删除\" + \"\u003c/a\u003e\" + \"\u003c/td\u003e\" + \"\u003ctd\u003e\" + \"\u003ca href='javascript:void(0);' onclick='setDefault(#{defaultAid})' class='btn btn-xs add-def btn-default'\u003e设为默认\u003c/a\u003e\" + \"\u003c/td\u003e\" + \"\u003c/tr\u003e\" //使用正则表达式替换获取该地址的aid值,#{aid}只是一个占位符的含义,没其他含义 str = str.replace(\"#{editAid}\",address.aid) str = str.replace(\"#{deleteAid}\",address.aid) str = str.replace(\"#{defaultAid}\",address.aid) str = str.replace(\"#{isDefault}\",address.isDefault) $(\"#address-list\").append(str) } $(\".add-def:eq(0)\").hide(); }else{ let text = \"\u003ctr\u003e\u003ctd colspan='12' style='font-weight: bold;color: red;padding: 20px;font-size: medium'\u003e\" + \"暂无收货地址,请先添加收货地址\" + \"\u003c/td\u003e\u003c/tr\u003e\" $(\"#address-list\").append(text) } }, error: function (error) { alert(error.message) } }); ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:3:3","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.6 设置默认地址 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:4:0","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.6.1 后端-持久层 1.编写sql语句 //检测当前用户想设置的地址是否存在 SELECT * FROM t_address WHERE aid = ? //将所有的地址都设为非默认地址 UPDATE t_address SET is_default = 0 WHERE uid = ? //更新选中地址的默认值 UPDATE t_address SET is_default = 1,modified_user = ?,modified_time=? WHERE aid = ? 2.定义Mapper接口抽象方法 /** * 根据用户收货地址aid查询收货地址 * @param aid 收货地址id * @return 返回地址 */ Address findByAid(Integer aid); /** * 根据用户uid将所有地址设为非默认地址 * @param uid 用户uid * @return 返回影响行数 */ Integer updateNoneDefault(Integer uid); /** * 按照aid将该条收货地址设为默认地址 * @param aid 用户收货地址aid * @param modifiedUser 执行操作的操作人 * @param modifiedTime 执行操作的操作时间 * @return 返回影响行数 */ Integer updateDefault(Integer aid, String modifiedUser, Date modifiedTime); 3.编写Mapper接口的映射文件 \u003c!-- Address findByAid(Integer aid);--\u003e \u003cselect id=\"findByAid\" resultType=\"com.zxl.store.entity.Address\"\u003e SELECT * FROM t_user WHERE aid = #{aid} \u003c/select\u003e \u003c!-- Integer updateNoneDefault(Integer uid);--\u003e \u003cupdate id=\"updateNoneDefault\"\u003e UPDATE t_address SET is_default = 0 WHERE uid = #{uid} \u003c/update\u003e \u003c!-- Integer updateDefault(Integer aid, String modifiedUser, Date modifiedTime);--\u003e \u003cupdate id=\"updateDefault\"\u003e UPDATE t_address SET is_default = 1,modified_user = #{modifiedUser},modified_time=#{modifiedTime} WHERE aid = #{aid} \u003c/update\u003e 4.编写Mapper接口的单元测试 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:4:1","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.6.2 后端-业务层 1.异常处理 更新时异常(已经存在,无需创建) 数据不存在异常 /** * @author zxl * @description 用户地址未找到异常 * @date 2022/11/3 */ public class AddressNotFoundException extends ServiceException {} 2.定义Service层抽象方法 /** * 修改用户选中地址为默认收货地址 * @param uid 用户uid * @param aid 收货地址id * @param username 操作人 * @return void */ void setDefault(Integer uid,Integer aid,String username); 3.编写方法的具体逻辑 @Override public void setDefault(Integer uid, Integer aid, String username) { Address res = addressMapper.findByAid(aid); if(res==null){ throw new AddressNotFoundException(\"用户收货地址不存在\"); } //先将所有的地址设为非默认地址 Integer row = addressMapper.updateNoneDefault(uid); if(row\u003c1){ throw new UpdateException(\"将所有地址设置为非默认地址时出现异常\"); } //按照aid将收货地址设置为默认地址 Integer updateRow = addressMapper.updateDefault(aid, username, new Date()); if(updateRow!=0){ throw new UpdateException(\"设置默认地址时出现异常\"); } } 4.单元测试 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:4:2","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.6.3 后端-控制层 1.异常处理 将异常加入到全局处理 2.请求设计 请求路径:/address/set_default/{aid} 请求参数:@PathVariable(“aid”)Integer aid,HttpSession session 请求类型:post 响应类型:JsonResult\u003c void\u003e 3.处理请求,编写控制层 /** * 处理设置默认地址的请求 * @param aid 被设置为默认的收货地址id * @param session 项目启动时生成的session * @return 返回void */ @RequestMapping(value = \"/set_default/{aid}\",method = RequestMethod.POST) public JsonResult\u003cVoid\u003e setDefault(@PathVariable(\"aid\")Integer aid,HttpSession session){ //获取uid,username Integer uid = getUserIdFromSession(session); String username = getUsernameFromSession(session); //设置默认 addressService.setDefault(uid,aid,username); return new JsonResult\u003c\u003e(OK); } 4. 前端页面 //为设置默认按钮绑定事件 function setDefault(aid) { if(confirm(\"确定要这条收货地址设为默认地址吗?\")){ $.ajax({ url: \"/address/set_default/\"+aid, type: \"POST\", dataType: \"JSON\", success: function(json) { if (json.state == 200) { alert(\"设置成功\"); location.reload(); } else { alert(\"设置默认收货地址失败!\" + json.message); } }, error: function(json) { alert(\"您的登录信息已经过期,请重新登录!HTTP响应码:\" + json.status); location.href = \"login.html\"; } }); } } ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:4:3","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.7 删除地址 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:5:0","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.7.1 后端-持久层 1.编写sql语句 //根据aid删除用户收货地址的sql DELETE FROM t_address WHERE aid = #{aid} 2.定义Mapper接口抽象方法 /** * 按照aid删除用户的收货地址 * @param aid 用户的收货地址id * @return 返回影响行数 */ Integer DeleteAddressByAid(Integer aid); 3.编写Mapper接口的映射文件 \u003c!-- Integer DeleteAddressByAid(Integer aid);--\u003e \u003cdelete id=\"DeleteAddressByAid\"\u003e DELETE * FROM t_address WHERE aid = #{aid} \u003c/delete\u003e 4.单元测试 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:5:1","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.7.2 后端-业务层 1.异常控制 删除时异常 /** * @author zxl * @description 数据库删除时异常 * @date 2022/11/4 */ public class DeleteException extends ServiceException {} 2.定义抽象方法 /** * 按照收货地址id删除收货地址 * @param aid 收货地址id */ void deleteAddressByAid(Integer aid, Integer uid, String username); 3.编写实现逻辑 @Override public void deleteAddressByAid(Integer aid,Integer uid,String username) { //需要先判断用户有多少条地址 Integer count = addressMapper.userAddressCount(uid); //判断用户收货地址是否存在 Address byAid = addressMapper.findByAid(aid); if(byAid==null)throw new AddressNotFoundException(\"用户收货地址不存在\"); //如果地址多条 而且当前要删除的地址为默认地址 if(count\u003e1\u0026\u0026byAid.getIsDefault()==1){ //先删除当前地址 //按照用户aid删除 Integer row = addressMapper.DeleteAddressByAid(aid); //判断是否出现异常 if(row!=1)throw new DeleteException(\"删除用户收货地址时出现未知异常\"); //查询所有的地址,由于查询是按照创造时间排序的 List\u003cAddress\u003e byUid = addressMapper.findByUid(uid); //设置默认地址 addressMapper.updateDefault(aid,username,new Date()); return; } //如果只有一条,就不用判断是否需要再次设置默认地址 //按照用户aid删除 Integer row = addressMapper.DeleteAddressByAid(aid); //判断是否出现异常 if(row!=1)throw new DeleteException(\"删除用户收货地址时出现未知异常\"); } 4.单元测试 ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:5:2","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"4.7.3 后端-控制层 1.异常处理 将异常加入全局处理 2.设计请求 请求路径:/address/delete_address/{aid} 请求参数:@PathVariable(“aid”)Integer aid,HttpSession session 请求类型:post 响应类型:JsonResult\u003c void\u003e 3.处理请求,编写控制方法 /** * 处理删除地址的请求 * @param aid 需要删除的收货地址的id * @return 返回OK */ @RequestMapping(value = \"/delete_address/{aid}\",method = RequestMethod.POST) public JsonResult\u003cVoid\u003e deleteAddressByAid(@PathVariable(\"aid\")Integer aid,HttpSession session){ //获取uid Integer uid = getUserIdFromSession(session); //获取username String username = getUsernameFromSession(session); //执行删除 addressService.deleteAddressByAid(aid,uid,username); //执行成功返回数据 return new JsonResult\u003c\u003e(OK); } 4.前端页面 //为删除按钮定义事件 function deleteAddress(aid) { if(confirm(\"确定要删除这条收货地址吗?\")){ $.ajax({ url: \"/address/delete_address/\"+aid, type: \"POST\", dataType: \"JSON\", success: function(json) { if (json.state == 200) { location.reload(); } else { alert(\"删除收货地址失败!\" + json.message); } }, error: function(xhr) { alert(\"您的登录信息已经过期,请重新登录!HTTP响应码:\" + xhr.status); location.href = \"login.html\"; } }); } } ","date":"2022-10-14","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/:5:3","tags":["computerStore"],"title":"SpringBoot项目-地址管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E5%9C%B0%E5%9D%80%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"2环境搭建 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:0:0","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"2.1基本环境: 1.JDK1.8 2.Maven 3.6.1 3.Mysql 8.0.28 4.IDEA 2019.2.4 3.用户管理 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:1:0","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.1 创建数据库 CREATE DATABASE IF NOT EXISTS `computer_store` CHARACTER SET 'utf8'; ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:2:0","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.2 创建数据表 CREATE TABLE t_user ( uid INT AUTO_INCREMENT COMMENT '用户id', username VARCHAR(20) NOT NULL UNIQUE COMMENT '用户名', password CHAR(32) NOT NULL COMMENT '密码', salt CHAR(36) COMMENT '盐值', phone VARCHAR(20) COMMENT '电话号码', email VARCHAR(30) COMMENT '电子邮箱', gender INT COMMENT '性别:0-女,1-男', avatar VARCHAR(50) COMMENT '头像', is_delete INT COMMENT '是否删除:0-未删除,1-已删除', created_user VARCHAR(20) COMMENT '日志-创建人', created_time DATETIME COMMENT '日志-创建时间', modified_user VARCHAR(20) COMMENT '日志-最后修改执行人', modified_time DATETIME COMMENT '日志-最后修改时间', PRIMARY KEY (uid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 考虑到每个表中都有固定的四个字段,可以使用一个java基类表示 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:3:0","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.3 创建实体类 //用于与用户四个字段所形成映射关系的基类 public class BaseEntity implements Serializable { private String createdUser;//日志创建人 private Date createdTime;//日志创建时间 private String modifiedUser;//日志最后修改人 private Date modifiedTime;//日志最后修改时间 } //对应数据表的User实体类 public class User extends BaseEntity implements Serializable { private Integer uid;//用户id private String username;//用户名 private String password;//密码 private String salt;//盐值 private String phone;//电话号码 private String email;//电子邮箱 private Integer gender;//性别:0-女,1-男 private String avatar;//头像 private Integer isDelete;//是否删除:0-未删除,1-已删除 } ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:4:0","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.4 注册 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:5:0","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.4.1 后端持久层(使用mybatis) 1 编写sql语句 #增加用户的sql语句 INSERT INTO t_user(username,password,)VALUES (?,?) #查询用户是否存在(username在数据库由UNIQUE修饰) SELECT * FROM t_user WHERE username = ? 2 定义mapper接口和抽象方法 由于项目可能有多个mapper接口,所以在项目目录下创建一个mappers包,用于管理所有mapper接口 并在SpringBoot启动类上添加MapperScan注解扫描mapper包或直接在接口说使用@Mapper注解 /*用户模块持久层接口*/ //@Mapper public interface UserMapper { /** * 插入用户数据 * @param user 用户数据 * @return 返回影响行数 */ Integer insert(User user); /** * 根据用户名查询用户数据(数据库中用户名唯一) * @param username 用户名 * @return 返回用户或者null */ User findByUsername(String username); } 3 编写映射文件 项目可能有多个Mapper映射文件,所以需要在项目的resource下创建一个mappers包便于管理。 \u003c?xml version=\"1.0\" encoding=\"UTF-8\" ?\u003e \u003c!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\"\u003e \u003c!--namespace属性:指定当前映射文件和哪个接口映射--\u003e \u003cmapper namespace=\"com.zxl.store.mappers.UserMapper\"\u003e \u003c!--Integer insert(User user);--\u003e \u003c!-- useGeneratedKeys 开启某(主键)个字段的值递增 keyProperty 表示将表中的xxx字段作为主键 --\u003e \u003cinsert id=\"insert\" useGeneratedKeys=\"true\" keyProperty=\"uid\"\u003e INSERT INTO t_user( username,password, salt,phone, email,gender, avatar,is_delete, created_user, created_time, modified_user, modified_time )VALUES ( #{username},#{password}, #{salt},#{phone}, #{email},#{gender}, #{avatar},#{isDelete}, #{createdUser}, #{createdTime}, #{modifiedUser}, #{modifiedTime} ) \u003c/insert\u003e \u003c!--User findByUsername(String username);--\u003e \u003c!-- ResultType 表示查询的结果类型 ResultMap 当表的字段和类的对象属性字段名称不一致时,来自定义结果的映射规则 --\u003e \u003cselect id=\"findByUsername\" resultType=\"com.zxl.store.pojo.User\"\u003e SELECT * FROM t_user WHERE username = #{username} \u003c/select\u003e \u003c/mapper\u003e #针对t_user与User类的名称不对应问题, #可以在application.properties下针对mybatis开启 mybatis.configuration.map-underscore-to-camel-case=true 4 将mapper映射文件的位置在yml配置文件中进行对应的设置 将mapper的映射文件路径添加在application.properties配置文件中进行配置 mybatis.mapper-locations=classpath:mappers/*.xml 在application.properties中配置数据库连接,否则无法连接数据库 (此处以使用mysql为例子设置,具体数据库请参考自己使用的数据库) pring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.username=root spring.datasource.password=123 spring.datasource.url=jdbc:mysql://localhost:3306/store 5 单元测试 进行单元测试,建议尽可能完成所有测试 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:5:1","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.4.2 后端-业务层 业务层的包下主要有ex、impl、interface,其中ex放置各种异常处理类,impl中放置interface的实现类 1.规划异常处理机制 考虑到在后端处理业务的过程中,会出现各种异常情况,如执行过程中数据库宕机、用户名重复等。 虽然java在异常处理机制已经很完善,以上的情况都是抛出RuntimeException异常,对定位异常不够明确。 因此在业务层的制定中,需要考虑对异常的定义处理。 在业务层制定一个继承RuntimeException异常的异常类ServiceException,再让具体的异常继承这个异常。 /*业务层异常的基类*/ public class ServiceException extends RuntimeException { public ServiceException() { super(); } public ServiceException(String message) { super(message); } public ServiceException(String message, Throwable cause) { super(message, cause); } public ServiceException(Throwable cause) { super(cause); } protected ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } 根据业务层不同的 来详细定义具体异常的类型,统一的继承ServiceException异常基类 //用户未找到异常 public class UserNotFoundException extends ServiceException {} //用户名重复异常 public class UsernameDuplicateException extends ServiceException {} //插入时未知异常 public class InsertException extends ServiceException{} //验证码不匹配异常 public class ValidCodeNotMatchException extends ServiceException {} 2 定义业务层接口和抽象方法 public interface IUserService { /** * 处理用户注册 * @param user 用户信息 */ void userRegister(User user); } 3 定义业务层接口的实现类 补全五个字段: is_Delete:便于逻辑删除 create_user:注册记录创建人 create_time:注册日期 modified_user:增删改操作人 modified_time:增删改时间 便于后期数据库管理 @Service public class IUserServiceImpl implements IUserService { @Autowired private UserMapper userMapper; //处理用户注册 @Override public void userRegister(User user) { //首先判断用户名是否在数据库中重复使用 User res = userMapper.findUserByUsername(user.getUsername()); //重复的情况下抛出异常 if(res!=null){ throw new UsernameDuplicateException(\"用户名已被注册\"); } //加密处理:md5算法 //串+password+串---\u003emd5,连续加载3次 //盐值+password+盐值 ---\u003e盐值随机字符串 //记录旧密码 String oldPass = user.getPassword(); //使用UUID获取salt String salt = UUID.randomUUID().toString().toUpperCase(); //进行加密操作 String newPass = getMD5Password(oldPass,salt); //对User进行补全 user.setSalt(salt); user.setPassword(newPass); //修改逻辑删除判定 user.setIsDelete(0); //补全四个操作字段 Date currentTime = new Date(); user.setCreatedTime(currentTime); user.setCreatedUser(user.getUsername()); user.setModifiedTime(currentTime); user.setModifiedUser(user.getUsername()); //调用插入方法 插入用户数据 Integer row = userMapper.addUser(user); //判断插入结果 if(row!=1){ throw new InsertException(\"处理用户注册过程出现未知异常\"); } } /*md5加密*/ private String getMD5Password(String password,String salt){ //加密算法 //加密之后的密匙 for (int i = 0; i \u003c 3 ; i++) { password = DigestUtils.md5DigestAsHex((salt + password + salt).getBytes()).toUpperCase(); } return password; } } 4 业务层进行单元测试 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:5:2","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.4.3 后端-控制层 1 设置返回响应信息给前端的基类 /** * @author zxl * @description 相应数据给前端 * @date 2022/10/30 */ @Data public class JsonResult\u003cE\u003e { //响应状态码 200-成功 4000-用户名重复 5000-数据库或服务器异常 private int status; //响应信息 private String message; //响应数据 private E data; public JsonResult() { } public JsonResult(int status) { this.status = status; } public JsonResult(Throwable e) { this.message = e.getMessage(); } public JsonResult(int status, E data) { this.status = status; this.data = data; } } 2 设计请求 请求路径:/user/reg 请求参数:User user,HttpSession session,String code 请求类型:post 响应结果:JsonResult\u003c Void\u003e 3 设计控制层 设计一个BaseController处理全局所有自定义的异常 /*控制层的基类*/ public class BaseController { /*操作成功状态码*/ public static final int OK = 200; /** * 1.当出现了value内的异常之一,就会将下方的方法作为新的控制器方法进行执行 * 因此该方法的返回值也同时是返回给前端的页面 * 2.此外还自动将异常对象传递到此方法的参数列表中,这里使用Throwable e来接收 **/ @ExceptionHandler(ServiceException.class) //统一处理抛出的异常 public JsonResult\u003cVoid\u003e handleException(Throwable e){ JsonResult\u003cVoid\u003e result = new JsonResult\u003c\u003e(e); if (e instanceof UsernameDuplicateException){ result.setStatus(4000); //表示用户名重复 result.setMessage(e.getMessage()); }else if (e instanceof UserNotFoundException){ result.setStatus(4001); //表示用户数据不存在 result.setMessage(e.getMessage()); }else if (e instanceof InsertException){ result.setStatus(5000); //数据库或服务器有问题 result.setMessage(e.getMessage()); } //返回异常处理结果 return result; } } 创建一个UserController处理注册请求 UserController继承了BaseController也就间接拥有了BaseController的属性和方法 @RestController @RequestMapping(\"users\") public class UserController extends BaseController{ @Autowired private IUserService iUserService; //注册用户 @RequestMapping(value = \"reg\",method = RequestMethod.POST) //@ResponseBody//表示此方法的响应结果以json格式进行数据的响应给到前端 public JsonResult\u003cVoid\u003e reg(User user){ iUserService.reg(user); return new JsonResult\u003c\u003e(OK,\"注册成功\"); } } 前端页面 前端页面只需要将表单通过ajax异步向后端服务器发送即可(目标文件:register.html) 只需要通过JavaScript给用户注册按钮绑定事件即可 当前功能: 1.注册信息空缺检测 2.用户名是否合规检测 3.添加验证码 4.验证输入密码是否一致 5.表单提交 \u003cscript type=\"text/javascript\"\u003e $(document).ready(function () { //验证信息和发送ajax注册用户请求 //给用户注册绑定点击事件 $(\"#btn-reg\").click(function () { let name = $(\"#username\").val(); let pwd = $(\"#password\").val(); let rePwd = $(\"#rePwd\").val(); let codeStr = $(\"#code\").val(); //去掉验证码前后空格 codeStr = $.trim(codeStr); if (name == \"\" || pwd == \"\" || rePwd == \"\" || codeStr == \"\") { $(\"#error-msg\").text(\"请先填写需要注册的信息!\"); return false; } //验证用户名是否符合规则 let nameCheck = /^\\w{5,12}$/; let username = $(\"#username\").val(); if (!(nameCheck.test(username))) { $(\"#error-msg\").text(\"用户名必须是5-12位的字母和数字\"); return false; } else { $(\"#error-msg\").empty() } //验证密码是否符合规则 let passCheck = /^\\w{5,12}$/; let password = $(\"#password\").val(); if (!passCheck.test(password)) { $(\"#error-msg\").text(\"密码必须是5-12位的字母和数字\"); return false; } else { $(\"#error-msg\").empty() } //验证确认密码和密码是否相同 let rePass = $(\"#rePwd\").val(); if (rePass !== password) { $(\"#error-msg\").text(\"密码不一致\"); return false; } else { $(\"#error-msg\").empty() } $.ajax({ url: \"/user\", type: \"post\", data: $(\"#form-reg\").serialize(), //获取表单的所有内容 dataType: \"json\", success: function (res) { if (res.status === 200) { alert(\"注册成功!\") location.href = \"login.html\" } else { $(\"#error-msg\").html(res.message) } }, error: function (error) { alert(error.status + \"错误,服务器出现故障,请等待攻城狮修复!!\") } }); }); //显示或隐藏密码的方法 function showPasswordOrNot(eleId,imgId){ let pwd = document.getElementById(eleId) let img = document.getElementById(imgId) if (pwd.type == \"password\"){ pwd.type = \"text\"; img.src = \"../images/img/close.jpeg\" }else { pwd.type = \"password\"; img.src = \"../images/img/open.jpeg\" } } //给图片验证码绑定点击事件,刷新验证码 function reFlashImg(imgId) { let kaptcha = document.getElementById(imgId) kaptcha.src = \"/kaptcha/kaptcha-image?time=\"+ new Date(); } }); \u003c/script\u003e ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:5:3","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.5 用户登录 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:6:0","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.5.1 后端-持久层 持久层可以利用上面写好的sql语句判断用户是否存在 1.sql语句可以使用上面的 2.Mapper接口用上面的 3.\bMapper接口的映射文件可以使用上面的 4.单元测试 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:6:1","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.5.2 后端-业务层 1.规划异常 创建两个异常类继承ServiceException基类 // 表示用户名不存在的异常 public class UserNotFoundException extends ServiceException {} //表示密码错误的异常 public class PasswordNotMatchException extends ServiceException {} 2.编写接口抽象方法 /** * 用户登陆操作 * @param user 用户信息 * @return 返回用户 */ User userLogin(User user); 3.实现类内具体的业务处理流程 //处理用户登陆 @Override public User userLogin(User user) { //用户名 String username = user.getUsername(); //密码 String password = user.getPassword(); //查询用户是否在数据库中 User res = userMapper.findUserByUsername(username); //判断结果为空或者逻辑删除 if(res==null||(res.getIsDelete()==1)){ throw new UserNotFoundException(\"用户数据不存在\"); } //密码校验 String salt = user.getSalt(); String databasePass = res.getPassword(); //获取加密密码 String md5Password = getMD5Password(password, salt); // if(!(md5Password.equals(databasePass))){ throw new PasswordNotMatchException(\"密码错误\"); } //密码正确返回查询结果 //将查询结果中的uid、username、avatar封装到新的user对象中 User ret = new User(); user.setUid(res.getUid()); user.setUsername(res.getUsername()); user.setAvatar(res.getAvatar()); return ret; } 进行单元测试 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:6:2","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.5.3 后端-控制层 1.在全局的BaseController中添加异常处理 2.设计请求 请求路径:/user/login 请求参数:User user, HttpSession session,String code 请求类型:get 响应结果:JsonResult\u003c User\u003e 3. 处理请求 //用户登陆 @GetMapping public JsonResult\u003cUser\u003e userLogin(User user,HttpSession session,String code){ //从session取出验证码 String validCode = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY); //判断验证码是否正确 if (!validCode.equals(code)) { throw new ValidCodeNotMatchException(\"验证码错误,请重试!\"); } //执行登陆操作 User LoginUser = userService.userLogin(user); //将用户名和uid保存到session中 session.setAttribute(\"uid\",LoginUser.getUid()); session.setAttribute(\"username\", LoginUser.getUsername()); //返回数据 return new JsonResult\u003c\u003e(OK,LoginUser); } 4.单元测试 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:6:3","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.5.4 前端页面 登录成功以及登录成功后需要做的事情: 需要在登录成功后跳转至首页 window.location.href = “xxx.html” 这个直接跳转到指定页面 保存用户信息到session域中 3.5.4.1 保存在前端的会话窗口中,供前端页面使用 $.ajax({ url : \"/user\", type: \"get\", data: $(\"#form-login\").serialize(), //获取表单的所有内容 dataType: \"json\", success: function (res) { if (res.status === 200){ alert(\"登录成功!\"); //前往首页 window.location.href=\"index.html\"; //将用户信息存入session域中 sessionStorage.setItem(\"user\",JSON.stringify(res.data)); }else { $(\"#error-msg\").html(res.message) } }, error: function (error) { alert(error.status + \"错误,服务器出现故障,请等待攻城狮修复!!\") } }); 3.5.4.2 保存在工程项目的session中,供整个工程使用 session对象主要存在服务器端,可以用于保存服务器的临时数据的对象,也可用于拦截器的拦截请求 public class BaseController { /** * Description : 从session中获取用户uid * @param session springboot启动时生成的session对象 **/ public final Integer getUserIdFromSession(HttpSession session){ String uidStr = session.getAttribute(\"uid\").toString(); return Integer.valueOf(uidStr); } //从session中获取用户username public final String getUsernameFromSession(HttpSession session){ return session.getAttribute(\"username\").toString(); } } 3.5.4.3 拦截器,对每个访问的页面进行拦截判断,没有登录则重定向至登录页面 在interceptor包下自定义拦截器类,实现HandleInterceptor接口,实现此接口的方法 public class LgoinInterceptor implements HandlerInterceptor { //在调用所有处理请求的方法之前被自动调用执行的方法 /** * 检测全局Session对象中是否有Uid数据,如果有放行,如果没有重定向到登陆页面 * * @param request 请求对象 * @param response 响应对象 * @param handler 处理器 * @return 如果返回值为true--\u003e 放行 如果false--\u003e 拦截 * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //获取项目工程的session HttpSession session = request.getSession(); if (session.getAttribute(\"uid\") != null) { //说明此时已登录 return true; } else {//说明未登录 //重定向至登录页面 response.sendRedirect(\"/web/login.html\"); return false; } } //ModelAndView对象返回之后被调用的方法 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } //在整个请求所有关联的资源被执行完毕最后所执行的方法 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } } 3.5.4.4 在config包下创建自定义配置类,实现WebMvcConfigurer接口,将拦截器添加到容器中 指定拦截规则【如果是拦截所有,静态资源也会被拦截,所以要指定白名单和黑名单】 拦截器也要放行接口的请求,不然就报错 @Configuration//加载当前的拦截器并进行注册 //处理器拦截器的注册 public class LoginInterceptorConfigurer implements WebMvcConfigurer { //将自定义的拦截器进行注册 @Override public void addInterceptors(InterceptorRegistry registry) { //自定义一个拦截器对象 HandlerInterceptor interceptor = new LgoinInterceptor(); //配置白名单 List\u003cString\u003e patterns = new ArrayList\u003c\u003e(); patterns.add(\"/bootstrap3/**\"); patterns.add(\"/css/**\"); patterns.add(\"/images/**\"); patterns.add(\"/js/**\"); patterns.add(\"/web/register.html\"); patterns.add(\"/web/login.html\"); patterns.add(\"/web/index.html\"); patterns.add(\"/web/product.html\"); patterns.add(\"/users/**\"); patterns.add(\"/kaptcha/**\"); patterns.add(\"/address/**\"); patterns.add(\"/cart/**\"); patterns.add(\"/district/**\"); patterns.add(\"/product/**\"); //向注册器对象添加拦截器 registry.addInterceptor(interceptor) .addPathPatterns(\"/**\")//要拦截的Url是什么 .excludePathPatterns(patterns); } } ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:6:4","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.6 修改密码 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:7:0","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.6.1 后端-持久层 1.编写sql语句 SELECT * FROM t_user WHERE uid = #{uid} UPDATE t_user SET password=#{password}, modified_time=#{modifiedTime}, modified_user=#{modifiedUser} WHERE uid = #{uid} 2.定义mapper接口的抽象方法 /** * 根据用户的id查询用户数据 * @param uid 用户Uid * @return 返回用户数据或者null */ User findByUid(Integer uid); /** * 根据用户Uid修改密码 * @param uid 用户Uid * @param password 用户输入的新密码 * @param modifiedUser 表示修改的执行者 * @param modifiedTime 表示修改的时间 * @return */ Integer updatePasswordByUid(Integer uid, String password, String modifiedUser, Date modifiedTime); 3.编写Mapper接口映射 \u003c!--User findByUid(Integer uid);--\u003e \u003cselect id=\"findByUid\" resultType=\"com.zxl.store.entity.User\"\u003e SELECT * FROM t_user WHERE uid = #{uid} \u003c/select\u003e \u003c!-- Integer updatePasswordByUid(Integer uid, String password, String modifiedUser, Date modifiedTime);--\u003e \u003cupdate id=\"updatePasswordByUid\" \u003e UPDATE t_user SET password=#{password}, modified_time=#{modifiedTime}, modified_user=#{modifiedUser} WHERE uid = #{uid} \u003c/update\u003e 4.测试 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:7:1","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.6.2 后端-业务层 1.异常处理,创建一个表示密码不匹配的异常,比如原密码不对 2.定义Userservice的抽象方法 3.编写实现类实现接口方法的业务处理逻辑 1.密码不匹配错误 public class PasswordNotMatchException extends ServiceException {} 2.IUserService的抽象方法 /** * 更改用户密码 * @param uid 用户id * @param username 用户名 * @param oldPassword 用户的旧密码 * @param newPasswrod 用户的新密码 */ void changePasswrod(Integer uid,String username,String oldPassword,String newPasswrod); 3.IUserServiceImpl 实体类的方法 @Override public void changePasswrod(Integer uid, String username, String oldPassword, String newPasswrod) { //先查询用户数据是否为空或者逻辑删除 User res = userMapper.findByUid(uid); if(res==null||res.getIsDelete()==1){ throw new UserNotFoundException(\"用户数据不存在\"); } //数据库密码和旧密码对比 String dataPassword = res.getPassword(); String md5Password = getMD5Password(oldPassword, res.getSalt()); if(!dataPassword.equals(md5Password)){ throw new PasswordNotMatchException(\"密码不匹配\"); } //插入新密码插入数据库,更新操作人和操作时间 String newPass = getMD5Password(newPasswrod, res.getSalt()); Integer row = userMapper.updatePasswordByUid(uid, newPass, username, new Date()); if(row!=1){ throw new InsertException(\"更新数据时产生未知异常\"); } } ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:7:2","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.6.3 后端-控制层 1.在全局异常处理机制中添加对业务层异常的处理 2.设计请求 请求路径 /user/change_password 请求参数 @RequestParam(“oldPassword”) String oldPassword, @RequestParam(“newPassword”) String newPassword, HttpSession session 请求类型 post 响应类型 JsonResult\u003c Void\u003e 3.处理请求 //用户更改密码 @RequestMapping(value = \"/change_password\",method = RequestMethod.POST) public JsonResult\u003cVoid\u003e changePassword(@RequestParam(\"oldPassword\") String oldPassword, @RequestParam(\"newPassword\") String newPassword, HttpSession session){ //获取Uid Integer uid = getUserIdFromSession(session); String username = getUsernameFromSession(session); userService.changePasswrod(uid,username,oldPassword,newPassword); //在用户修改密码之后清除session中保存的密码 session.setAttribute(\"uid\",null); return new JsonResult\u003c\u003e(OK,\"修改密码成功\"); } ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:7:3","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.6.4 前端页面 1.在用户修改完密码之后,重新定向为index.html 2.\b在后端控制层清楚掉uid值,使用户重新登陆 \u003cscript type=\"text/javascript\"\u003e $(document).ready(function () { $(\"#btn-change-password\").click(function () { if ($(\"#oldPwd\").val() == \"\" || $(\"#newPwd\").val() == \"\" || $(\"#rePwd\").val() == \"\"){ $(\"#error-msg\").text(\"请填写完信息后再提交!\"); return false; } //验证密码是否符合规则 let passCheck = /^\\w{5,12}$/; let password = $(\"#newPwd\").val(); if (!passCheck.test(password)){ $(\"#error-msg\").text(\"新密码必须是5-12位的字母和数字\"); return false; }else { $(\"#error-msg\").empty() } //验证确认密码和密码是否相同 let rePass = $(\"#rePwd\").val(); if (rePass !== password){ $(\"#error-msg\").text(\"密码不一致\"); return false; }else { $(\"#error-msg\").empty() } $(\"#btn-change-password\").click(function() { $.ajax({ url: \"/user/change_password\", type: \"POST\", data: $(\"#form-change-password\").serialize(), dataType: \"json\", success: function(json) { if (json.state === 200){ alert(\"密码已更改成功,请重新登录\") //跳转至首页,让用户重新登录 location.href = \"login.html\" }else { $(\"#error-msg\").html(json.message) } }, error: function (xhr) { alert(\"您的登录信息已经过期,请重新登录!HTTP响应码:\" + xhr.status); location.href = \"login.html\"; } }); }); }); }); \u003c/script\u003e ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:7:4","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.7 个人资料 3.7.1 后端持久层 1.编写Sql 需要更新phone、email、gender、modified_user ,modified_time这五个字段 //更新的sql语句 update t_user set phone = #{phone}, email = #{email}, gender = #{gender}, modified_user = #{modifiedUser}, modified_time = #{modifiedTime} where uid = #{uid} 2.定义Mapper接口抽象方法 /** * 根据用户的id查询用户数据 * * @param uid 用户Uid * @return 返回用户数据或者null */ User findByUid(Integer uid); /** * 更新用户信息 * @param user 用户数据 * @return 返回影响的行数 */ Integer updateInfoByUid(User user); 3.编写Mapper接口的映射文件 \u003c!--User findByUid(Integer uid);--\u003e \u003cselect id=\"findByUid\" resultType=\"com.zxl.store.entity.User\"\u003e SELECT * FROM t_user WHERE uid = #{uid} \u003c/select\u003e \u003cupdate id=\"updateInfoByUid\"\u003e UPDATE t_user SET \u003cif test=\"phone!=null\"\u003ephone = #{phone},\u003c/if\u003e \u003cif test=\"email!=null\"\u003eemail = #{email},\u003c/if\u003e \u003cif test=\"gender!=null\"\u003egender = #{gender},\u003c/if\u003e modified_time = #{modifiedTime}, modified_user = #{modifiedUser} WHERE uid = #{uid} \u003c/update\u003e 4.单元测试 3.7.2 后端-业务层 1.定义异常(无) 2.定义IUserService接口的抽象方法 3.实现抽象方法,编辑业务逻辑 1.定义异常 (无) 2.定义IUserService接口的抽象方法 /** * 根据用户的id查询用户的数据 * @param uid 用户id * @return 返回查询到的用户 或者 null */ User getByUid(Integer uid); /** * 更新用户的数据操作 * @param uid 用户的id * @param username 用户名 * @param user 用户对象数据 */ void changeInfo(Integer uid,String username,User user); 3.实现抽象方法,编写业务逻辑 @Override public User getByUid(Integer uid) { User res = userMapper.findByUid(uid); if(res==null||res.getIsDelete()==1){ throw new UserNotFoundException(\"用户数据不存在\"); } User usr = new User(); //防止重要内容泄漏 usr.setUsername(res.getUsername()); usr.setPhone(res.getPhone()); usr.setEmail(res.getEmail()); usr.setGender(res.getGender()); return usr; } /*User对象中的phone/email/gender 手动将uid/username/封装*/ @Override public void changeInfo(Integer uid, String username, User user) { User res = userMapper.findByUid(uid); if(res==null||res.getIsDelete()==1){ throw new UserNotFoundException(\"用户数据不存在\"); } user.setUid(uid); user.setUsername(username); user.setModifiedUser(username); user.setModifiedTime(new Date()); Integer row = userMapper.updateInfoByUid(user); if(row!=1){ throw new InsertException(\"更新时数据产生未知异常\"); } } 4.单元测试 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:8:0","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.7.3 后端-控制层 1.无异常,不需要处理 2.设计请求 请求路径 /user/change_info 请求参数 User user,HttpSession session 请求类型 post 响应类型 JsonResult 3.处理请求 //修改用户信息 @RequestMapping(value = \"/change_info\",method = RequestMethod.POST) public JsonResult\u003cVoid\u003e changeInfo(User user,HttpSession session){ //User数据只有四部分 用户电话邮箱性别 System.out.println(user.getUsername()+user.getEmail()+ user.getPhone()+user.getGender()); //Service内部已经重新写入 userService.changeInfo(getUserIdFromSession(session),getUsernameFromSession(session),user); return new JsonResult\u003c\u003e(OK,\"修改信息成功\"); } //获取用户信息 @RequestMapping(value = \"/get_by_uid\",method = RequestMethod.GET) public JsonResult\u003cUser\u003e getByUid(HttpSession session){ Integer uid = getUserIdFromSession(session); User user = userService.getByUid(uid); //将用户名、id、电话、邮箱、性别进行回传 User newUser = new User(); newUser.setUsername(user.getUsername()); newUser.setUid(user.getUid()); newUser.setGender(user.getGender()); newUser.setPhone(user.getPhone()); newUser.setEmail(user.getEmail()); newUser.setAvatar(user.getAvatar()); return new JsonResult\u003c\u003e(OK,newUser); } ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:8:1","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.7.4 前端页面 1.第一个ajax请求在用户信息页面加载完成后自动发送,并根据返回值通过js的id选择器 找到对应的元素并修改其属性值 2.第二个ajax请求在用户点击修改按钮之后先提示是否修改,再根据其选择进行处理, 同理根据结果利用js的id选择器找到对应的元素并修改其属性值 \u003cscript type=\"text/javascript\"\u003e $(document).ready(function() { $.ajax({ url: \"/user/get_by_uid\", type: \"GET\", dataType: \"json\", success: function(json) { if (json.state == 200) { console.log(\"username=\" + json.data.username); console.log(\"phone=\" + json.data.phone); console.log(\"email=\" + json.data.email); console.log(\"gender=\" + json.data.gender); $(\"#username\").val(json.data.username); $(\"#phone\").val(json.data.phone); $(\"#email\").val(json.data.email); let radio = json.data.gender == 0 ? $(\"#gender-female\") : $(\"#gender-male\"); radio.prop(\"checked\", \"checked\"); } else { alert(\"获取用户信息失败!\" + json.message); } } }); //给用户更改信息绑定点击事件 $(\"#btn-change-info\").click(function () { //根据用户选择状态决定是否发生ajax请求 if (confirm(\"确定要修改吗?\")){ let phone = $(\"#phone\").val(); let email = $(\"#email\").val(); if (phone == \"\" || email == \"\"){ $(\"#error-msg\").html(\"请先填写需要修改的信息!\"); return false; } let checkPhone = /^[1][3,4,5,7,8][0-9]{9}$/; if (!checkPhone.test(phone)){ $(\"#error-msg\").html(\"手机号不符合要求!\"); return false; } //验证邮箱是否符合规则 let checkEmail = /^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$/; if (!checkEmail.test(email)){ $(\"#error-msg\").html(\"邮箱不符合要求!\"); return false; } $.ajax({ url : \"/user/change_info\", type: \"post\", data: $(\"#form-change-info\").serialize(),//获取表单的所有内容 dataType: \"json\", success: function (res) { if (res.status === 200){ alert(\"修改成功!\") //网页刷新 location.reload(); }else { $(\"#error-msg\").html(res.message); } }, error: function (error) { alert(\"服务器出现故障,请等待攻城狮修复!!\") } }); } }) }); \u003c/script\u003e ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:8:2","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.8 头像上传 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:9:0","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.8.1 后端-持久层 1.编写sql 对应的avatar字段保存头像地址 UPDATE t_user SET avatar=#{avatar}, modified_user=#{modifiedUser}, modified_time=#{modifiedTime} WHERE uid = #{uid} 2.定义Mapper接口的抽象方法 /** * 根据用户的Uid修改头像 * @param uid 用户Uid * @param avatar 头像数据 * @param modifiedUser 表示修改的执行者 * @param modifiedTime 表示修改的时间 * @return */ Integer updateAvatarByUid(@Param(\"uid\") Integer uid, @Param(\"avatar\") String avatar, @Param(\"modifiedUser\") String modifiedUser, @Param(\"modifiedTime\") Date modifiedTime); 3.编写Mapper接口的映射文件 \u003cupdate id=\"updateAvatarByUid\"\u003e UPDATE t_user SET avatar=#{avatar}, modified_user=#{modifiedUser}, modified_time=#{modifiedTime} WHERE uid = #{uid} \u003c/update\u003e 4.单元测试 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:9:1","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.8.2 后端-业务层 1.异常规划 例如用户数据不存在,服务器宕机等 2.定义service层接口抽象方法 3.实现类重写抽象方法,编写业务处理逻辑 2.定义Service层接口抽象方法 /** * 更新用户的头像操作 * @param uid 用户id * @param avatar 用户头像路径 * @param username 修改的执行者 */ void changeAvatar(Integer uid,String avatar,String username); 3.实现类重写抽象方法,编写业务层逻辑 /**/ @Override public void changeAvatar(Integer uid, String avatar, String username) { //查询当前的用户数据是否存在 User res = userMapper.findByUid(uid); if(res==null||res.getIsDelete()==1){ throw new UserNotFoundException(\"用户数据不存在\"); } Integer row = userMapper.updateAvatarByUid(uid, avatar, username, new Date()); if(row!=1){ throw new InsertException(\"更新时数据产生未知异常\"); } } 4 .单元测试 ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:9:2","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.8.3 后端-控制层 1.处理控制层和业务层异常,将控制层异常加入全局异常处理@ExceptionHandler的值中 考虑到控制层接口前端数据也有可能出现异常,因此控制层也要进行异常控制 创建一个FileUploadException继承RunTimeException,其余异常继承此异常 ①文件为空异常 ②文件大小超出限制异常 ③文件状态异常 ④文件类型不符异常 ⑤文件读取IO异常 2.设计请求 请求路径:/change_avatar 请求参数:MultipartFile file,Httpsession session 请求类型:post 响应类型:JsonResult\u003c Void\u003e 3.处理请求 ①创建一个Controller专门处理文件的上传和下载 ②将文件的下载地址保存到数据库 /** * @author zxl * @description * @date 2022/10/30 */ @RestController @RequestMapping(\"/file\") public class FileController extends BaseController{ @Autowired private IUserService userService; /*设置上传文件的最大值*/ public static final int AVATAR_MAX_SIZE=10 * 1024 * 1024; /*限制上传文件的类型*/ public static final List\u003cString\u003e AVATAR_TYPES = new ArrayList\u003c\u003e(); static { AVATAR_TYPES.add(\"image/jpeg\"); AVATAR_TYPES.add(\"image/jpg\"); AVATAR_TYPES.add(\"image/png\"); AVATAR_TYPES.add(\"image/bmp\"); AVATAR_TYPES.add(\"image/gif\"); } /** * MultipartFile接口时SpringMVC提供的一个接口,这个接口为我们包装了 * 获取文件数据(任何类型的文件File都可以),Springboot整合了SpringMVC * 只需要在处理请求的方法参数列表上声明一个参数为MultipartFile即可 * @param session * @param file * @return */ @PostMapping public JsonResult\u003cString\u003e changeAvatar(HttpSession session, @RequestParam(\"file\") MultipartFile file){ //判断文件是否为null if(file.isEmpty()){ throw new FileEmptyException(\"文件为空\"); } //判断文件大小 if(file.getSize()\u003eAVATAR_MAX_SIZE){ throw new FileSizeException(\"文件大小超出限制\"); } // 判断上传的文件类型是否超出限制 String contentType = file.getContentType(); // boolean contains(Object o):当前列表若包含某元素,返回结果为true;若不包含该元素,返回结果为false if (!AVATAR_TYPES.contains(contentType)) { // 是:抛出异常 throw new FileTypeException(\"不支持使用该类型的文件作为头像,允许的文件类型:\" + AVATAR_TYPES); } //获取当前文件的绝对路径 //String parent = session.getServletContext().getRealPath(\"upload\"); String parent = \"/Users/zhaoxinlei/workspace/StoreRebuild/store/src/main/resources/static/avatar\"; System.out.println(parent); //保存头像文件的文件夹 File dir = new File(parent); if(!dir.exists()){ dir.mkdirs(); } //保存头像文件的文件名 String suffix =\"\"; String originalFilename = file.getOriginalFilename(); int beginIndex = originalFilename.lastIndexOf(\".\"); if(beginIndex\u003e0){ suffix = originalFilename.substring(beginIndex); } String filename = UUID.randomUUID().toString() + suffix; // 创建文件对象,表示保存的头像文件 File dest = new File(dir,filename); //执行保存文件操作 try{ file.transferTo(dest); }catch (IllegalStateException e){ //抛出异常 throw new FileStateException(\"文件状态异常,可能文件已被移动或者删除\"); }catch (IOException e){ //抛出异常 throw new FileUploadIOException(\"上传文件时读写错误,请稍后重新尝试\"); } //从Session中获取Uid和username Integer uid = getUserIdFromSession(session); String username = getUsernameFromSession(session); //将头像写入数据库 userService.changeAvatar(uid,filename,username); //返回成功头像路径 String avatar = \"../avatar/\"+filename; System.out.println(filename); System.out.println(avatar); return new JsonResult\u003c\u003e(OK,avatar); } } ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:9:3","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"},{"categories":["SpringBoot","Mybatis"],"content":"3.8.4. 前端页面 \u003cscript type=\"text/javascript\"\u003e $(document).ready(function () { //网页加载完成之前自动发送ajax请求 $.ajax({ url: \"/user/get_by_uid\", type: \"get\", dataType: \"json\", success:function (res) { //判断用户是首次注册还是老用户 if (res.data.avatar !== null \u0026\u0026res.data.avatar !== \"\" ){ //设置用户头像 $(\"#img-avatar\").attr(\"src\",\"../avatar/\"+res.data.avatar) }else{ //设置为默认头像 $(\"#img-avatar\").attr(\"src\",\"../images/index/user.jpg\") } }, error:function (err) { alert(err.message()) } }) $(\"#btn-change-avatar\").click(function() { $.ajax({ url: \"/file\", type: \"POST\", data: new FormData($(\"#form-change-avatar\")[0]), dataType: \"JSON\", processData: false, // processData处理数据 contentType: false, // contentType发送数据的格式 success: function(json) { if (json.state == 200) { $(\"#img-avatar\").prop(\"src\", json.message); console.log($(\"#img-avatar\").prop(\"src\")); } else { alert(\"修改失败!\" + json.message); } }, error: function(xhr) { alert(\"您的登录信息已经过期,请重新登录!HTTP响应码:\" + xhr.status); location.href = \"login.html\"; } }); }); }); \u003c/script\u003e ","date":"2022-10-13","objectID":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/:9:4","tags":["computerStore"],"title":"SpringBoot项目-用户管理","uri":"/springboot%E9%A1%B9%E7%9B%AE-%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/"}]