滑块验证码参数 验证码的 ID：SLIDER_ff970847732c44859fb56935fab7f7c9 鼠标 move 的轨迹，一共记录 36 次 滑块从开始到结束的时间 ImageCaptchaTrack(bgImageWidth=300, bgImageHeight=180, templateImageWidth=null, templateImageHeight=null, startTime=Tue Sep 09 17:30:21 CST 2025, stopTime=Tue Sep 09 17:30:24 CST 2025, left=null, top=null, trackList=[ImageCaptchaTrack.Track(x=0.0, y=0.0, t=1244.0, type=down), ImageCaptchaTrack.Track(x=1.0, y=0.0, t=1268.0, type=move), ImageCaptchaTrack.Track(x=3.0, y=0.0, t=1284.0, type=move), ImageCaptchaTrack.Track(x=10.0, y=0.0, t=1301.0, type=move), ImageCaptchaTrack.Track(x=19.0, y=0.0, t=1318.0, type=move), ImageCaptchaTrack.Track(x=34.0, y=0.0, t=1334.0, type=move), ImageCaptchaTrack.Track(x=46.0, y=0.0, t=1351.0, type=move), ImageCaptchaTrack.Track(x=55.0, y=0.0, t=1368.0, type=move), ImageCaptchaTrack.Track(x=63.0, y=-1.0, t=1384.0, type=move), ImageCaptchaTrack.Track(x=75.0, y=-1.0, t=1401.0, type=move), ImageCaptchaTrack.Track(x=91.0, y=-1.0, t=1418.0, type=move), ImageCaptchaTrack.Track(x=113.0, y=-1.0, t=1435.0, type=move), ImageCaptchaTrack.Track(x=122.0, y=-1.0, t=1451.0, type=move), ImageCaptchaTrack.Track(x=132.0, y=-1.0, t=1468.0, type=move), ImageCaptchaTrack.Track(x=143.0, y=-1.0, t=1485.0, type=move), ImageCaptchaTrack.Track(x=156.0, y=-2.0, t=1501.0, type=move), ImageCaptchaTrack.Track(x=166.0, y=-2.0, t=1518.0, type=move), ImageCaptchaTrack.Track(x=172.0, y=-3.0, t=1535.0, type=move), ImageCaptchaTrack.Track(x=173.0, y=-3.0, t=1551.0, type=move), ImageCaptchaTrack.Track(x=174.0, y=-3.0, t=1568.0, type=move), ImageCaptchaTrack.Track(x=176.0, y=-3.0, t=1585.0, type=move), ImageCaptchaTrack.Track(x=182.0, y=-3.0, t=1602.0, type=move), ImageCaptchaTrack.Track(x=193.0, y=-3.0, t=1618.0, type=move), ImageCaptchaTrack.Track(x=206.0, y=-3.0, t=1635.0, type=move), ImageCaptchaTrack.Track(x=217.0, y=-3.0, t=1652.0, type=move), ImageCaptchaTrack.Track(x=218.0, y=-3.0, t=1668.0, type=move), ImageCaptchaTrack.Track(x=219.0, y=-3.0, t=1685.0, type=move), ImageCaptchaTrack.Track(x=220.0, y=-3.0, t=1701.0, type=move), ImageCaptchaTrack.Track(x=220.0, y=-4.0, t=1718.0, type=move), ImageCaptchaTrack.Track(x=221.0, y=-4.0, t=1735.0, type=move), ImageCaptchaTrack.Track(x=222.0, y=-4.0, t=1751.0, type=move), ImageCaptchaTrack.Track(x=221.0, y=-4.0, t=1835.0, type=move), ImageCaptchaTrack.Track(x=220.0, y=-4.0, t=2002.0, type=move), ImageCaptchaTrack.Track(x=218.0, y=-4.0, t=2018.0, type=move), ImageCaptchaTrack.Track(x=217.0, y=-3.0, t=2035.0, type=move), ImageCaptchaTrack.Track(x=218.0, y=-3.0, t=2369.0, type=move), ImageCaptchaTrack.Track(x=218.0, y=-3.0, t=2659.0, type=up)], data=null) 点选验证码参数 验证码的 ID：SLIDER_ff970847732c44859fb56935fab7f7c9 鼠标 move 的轨迹，一共记录 225 次 滑块从开始到结束的时间 ImageCaptchaTrack(bgImageWidth=300, bgImageHeight=180, templateImageWidth=null, templateImageHeight=null, startTime=Tue Sep 09 17:39:50 CST 2025, stopTime=Tue Sep 09 17:40:04 CST 2025, left=null, top=null, trackList=[ImageCaptchaTrack.Track(x=23.0, y=157.0, t=7061.0, type=click), ImageCaptchaTrack.Track(x=8.0, y=87.0, t=7133.0, type=move), ImageCaptchaTrack.Track(x=14.0, y=83.0, t=7142.0, type=move), ImageCaptchaTrack.Track(x=30.0, y=75.0, t=7159.0, type=move), ImageCaptchaTrack.Track(x=47.0, y=65.0, t=7176.0, type=move), ImageCaptchaTrack.Track(x=63.0, y=55.0, t=7193.0, type=move), ImageCaptchaTrack.Track(x=73.0, y=47.0, t=7209.0, type=move), ImageCaptchaTrack.Track(x=81.0, y=40.0, t=7226.0, type=move), ImageCaptchaTrack.Track(x=87.0, y=35.0, t=7243.0, type=move), ImageCaptchaTrack.Track(x=91.0, y=31.0, t=7259.0, type=move), ImageCaptchaTrack.Track(x=92.0, y=30.0, t=7276.0, type=move), ImageCaptchaTrack.Track(x=93.0, y=30.0, t=7293.0, type=move), ImageCaptchaTrack.Track(x=92.0, y=29.0, t=7454.0, type=move), ImageCaptchaTrack.Track(x=90.0, y=27.0, t=7461.0, type=move), ImageCaptchaTrack.Track(x=88.0, y=25.0, t=7476.0, type=move), ImageCaptchaTrack.Track(x=83.0, y=21.0, t=7493.0, type=move), ImageCaptchaTrack.Track(x=79.0, y=13.0, t=7509.0, type=move), ImageCaptchaTrack.Track(x=76.0, y=8.0, t=7526.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=4.0, t=7543.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=1.0, t=7560.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=-1.0, t=7576.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=-2.0, t=7593.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=-4.0, t=7609.0, type=move), ImageCaptchaTrack.Track(x=78.0, y=-6.0, t=7626.0, type=move), ImageCaptchaTrack.Track(x=84.0, y=-10.0, t=7643.0, type=move), ImageCaptchaTrack.Track(x=91.0, y=-12.0, t=7660.0, type=move), ImageCaptchaTrack.Track(x=97.0, y=-12.0, t=7676.0, type=move), ImageCaptchaTrack.Track(x=106.0, y=-12.0, t=7693.0, type=move), ImageCaptchaTrack.Track(x=122.0, y=-8.0, t=7710.0, type=move), ImageCaptchaTrack.Track(x=129.0, y=-7.0, t=7726.0, type=move), ImageCaptchaTrack.Track(x=134.0, y=-7.0, t=7743.0, type=move), ImageCaptchaTrack.Track(x=137.0, y=-7.0, t=7760.0, type=move), ImageCaptchaTrack.Track(x=139.0, y=-7.0, t=7777.0, type=move), ImageCaptchaTrack.Track(x=141.0, y=-7.0, t=7793.0, type=move), ImageCaptchaTrack.Track(x=143.0, y=-8.0, t=7810.0, type=move), ImageCaptchaTrack.Track(x=144.0, y=-9.0, t=7826.0, type=move), ImageCaptchaTrack.Track(x=145.0, y=-10.0, t=7845.0, type=move), ImageCaptchaTrack.Track(x=145.0, y=-11.0, t=7894.0, type=move), ImageCaptchaTrack.Track(x=145.0, y=-12.0, t=7917.0, type=move), ImageCaptchaTrack.Track(x=146.0, y=-12.0, t=7934.0, type=move), ImageCaptchaTrack.Track(x=147.0, y=-12.0, t=7943.0, type=move), ImageCaptchaTrack.Track(x=154.0, y=-12.0, t=7960.0, type=move), ImageCaptchaTrack.Track(x=161.0, y=-13.0, t=7977.0, type=move), ImageCaptchaTrack.Track(x=166.0, y=-13.0, t=7993.0, type=move), ImageCaptchaTrack.Track(x=167.0, y=-13.0, t=8010.0, type=move), ImageCaptchaTrack.Track(x=168.0, y=-13.0, t=8027.0, type=move), ImageCaptchaTrack.Track(x=169.0, y=-13.0, t=8318.0, type=move), ImageCaptchaTrack.Track(x=170.0, y=-13.0, t=8327.0, type=move), ImageCaptchaTrack.Track(x=171.0, y=-13.0, t=8344.0, type=move), ImageCaptchaTrack.Track(x=172.0, y=-13.0, t=8365.0, type=move), ImageCaptchaTrack.Track(x=178.0, y=-11.0, t=8430.0, type=move), ImageCaptchaTrack.Track(x=182.0, y=-10.0, t=8444.0, type=move), ImageCaptchaTrack.Track(x=195.0, y=-7.0, t=8460.0, type=move), ImageCaptchaTrack.Track(x=210.0, y=-6.0, t=8477.0, type=move), ImageCaptchaTrack.Track(x=214.0, y=-5.0, t=8494.0, type=move), ImageCaptchaTrack.Track(x=214.0, y=-4.0, t=8549.0, type=move), ImageCaptchaTrack.Track(x=212.0, y=-4.0, t=8573.0, type=move), ImageCaptchaTrack.Track(x=208.0, y=-3.0, t=8581.0, type=move), ImageCaptchaTrack.Track(x=205.0, y=-3.0, t=8594.0, type=move), ImageCaptchaTrack.Track(x=198.0, y=-2.0, t=8611.0, type=move), ImageCaptchaTrack.Track(x=193.0, y=-2.0, t=8627.0, type=move), ImageCaptchaTrack.Track(x=190.0, y=-2.0, t=8644.0, type=move), ImageCaptchaTrack.Track(x=188.0, y=-2.0, t=8661.0, type=move), ImageCaptchaTrack.Track(x=186.0, y=-2.0, t=8677.0, type=move), ImageCaptchaTrack.Track(x=185.0, y=-2.0, t=8694.0, type=move), ImageCaptchaTrack.Track(x=192.0, y=-2.0, t=8744.0, type=move), ImageCaptchaTrack.Track(x=206.0, y=-2.0, t=8761.0, type=move), ImageCaptchaTrack.Track(x=221.0, y=-2.0, t=8778.0, type=move), ImageCaptchaTrack.Track(x=231.0, y=-2.0, t=8794.0, type=move), ImageCaptchaTrack.Track(x=236.0, y=-3.0, t=8811.0, type=move), ImageCaptchaTrack.Track(x=237.0, y=-3.0, t=8862.0, type=move), ImageCaptchaTrack.Track(x=238.0, y=-3.0, t=8885.0, type=move), ImageCaptchaTrack.Track(x=239.0, y=-4.0, t=8894.0, type=move), ImageCaptchaTrack.Track(x=240.0, y=-5.0, t=8911.0, type=move), ImageCaptchaTrack.Track(x=242.0, y=-6.0, t=8927.0, type=move), ImageCaptchaTrack.Track(x=243.0, y=-7.0, t=8944.0, type=move), ImageCaptchaTrack.Track(x=245.0, y=-9.0, t=8961.0, type=move), ImageCaptchaTrack.Track(x=246.0, y=-9.0, t=8978.0, type=move), ImageCaptchaTrack.Track(x=248.0, y=-10.0, t=8994.0, type=move), ImageCaptchaTrack.Track(x=251.0, y=-10.0, t=9011.0, type=move), ImageCaptchaTrack.Track(x=256.0, y=-10.0, t=9028.0, type=move), ImageCaptchaTrack.Track(x=260.0, y=-10.0, t=9044.0, type=move), ImageCaptchaTrack.Track(x=264.0, y=-10.0, t=9061.0, type=move), ImageCaptchaTrack.Track(x=265.0, y=-10.0, t=9078.0, type=move), ImageCaptchaTrack.Track(x=264.0, y=-9.0, t=9237.0, type=move), ImageCaptchaTrack.Track(x=279.0, y=60.0, t=9325.0, type=click), ImageCaptchaTrack.Track(x=263.0, y=-8.0, t=9382.0, type=move), ImageCaptchaTrack.Track(x=255.0, y=-6.0, t=9395.0, type=move), ImageCaptchaTrack.Track(x=240.0, y=-5.0, t=9412.0, type=move), ImageCaptchaTrack.Track(x=224.0, y=-5.0, t=9428.0, type=move), ImageCaptchaTrack.Track(x=210.0, y=-5.0, t=9445.0, type=move), ImageCaptchaTrack.Track(x=193.0, y=-5.0, t=9461.0, type=move), ImageCaptchaTrack.Track(x=187.0, y=-6.0, t=9478.0, type=move), ImageCaptchaTrack.Track(x=183.0, y=-7.0, t=9495.0, type=move), ImageCaptchaTrack.Track(x=180.0, y=-8.0, t=9511.0, type=move), ImageCaptchaTrack.Track(x=178.0, y=-8.0, t=9528.0, type=move), ImageCaptchaTrack.Track(x=177.0, y=-8.0, t=9545.0, type=move), ImageCaptchaTrack.Track(x=174.0, y=-9.0, t=9562.0, type=move), ImageCaptchaTrack.Track(x=172.0, y=-9.0, t=9578.0, type=move), ImageCaptchaTrack.Track(x=168.0, y=-10.0, t=9595.0, type=move), ImageCaptchaTrack.Track(x=166.0, y=-10.0, t=9612.0, type=move), ImageCaptchaTrack.Track(x=163.0, y=-12.0, t=9628.0, type=move), ImageCaptchaTrack.Track(x=162.0, y=-12.0, t=9646.0, type=move), ImageCaptchaTrack.Track(x=161.0, y=-12.0, t=9662.0, type=move), ImageCaptchaTrack.Track(x=160.0, y=-12.0, t=9694.0, type=move), ImageCaptchaTrack.Track(x=159.0, y=-12.0, t=9710.0, type=move), ImageCaptchaTrack.Track(x=158.0, y=-12.0, t=9745.0, type=move), ImageCaptchaTrack.Track(x=157.0, y=-12.0, t=9814.0, type=move), ImageCaptchaTrack.Track(x=172.0, y=57.0, t=9893.0, type=click), ImageCaptchaTrack.Track(x=155.0, y=-12.0, t=9974.0, type=move), ImageCaptchaTrack.Track(x=146.0, y=-12.0, t=9981.0, type=move), ImageCaptchaTrack.Track(x=135.0, y=-12.0, t=9995.0, type=move), ImageCaptchaTrack.Track(x=104.0, y=-12.0, t=10012.0, type=move), ImageCaptchaTrack.Track(x=81.0, y=-12.0, t=10029.0, type=move), ImageCaptchaTrack.Track(x=66.0, y=-12.0, t=10045.0, type=move), ImageCaptchaTrack.Track(x=69.0, y=-7.0, t=10126.0, type=move), ImageCaptchaTrack.Track(x=72.0, y=-2.0, t=10133.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=3.0, t=10145.0, type=move), ImageCaptchaTrack.Track(x=82.0, y=15.0, t=10162.0, type=move), ImageCaptchaTrack.Track(x=90.0, y=26.0, t=10179.0, type=move), ImageCaptchaTrack.Track(x=98.0, y=35.0, t=10196.0, type=move), ImageCaptchaTrack.Track(x=107.0, y=44.0, t=10212.0, type=move), ImageCaptchaTrack.Track(x=112.0, y=49.0, t=10229.0, type=move), ImageCaptchaTrack.Track(x=122.0, y=56.0, t=10246.0, type=move), ImageCaptchaTrack.Track(x=129.0, y=61.0, t=10262.0, type=move), ImageCaptchaTrack.Track(x=134.0, y=66.0, t=10279.0, type=move), ImageCaptchaTrack.Track(x=137.0, y=70.0, t=10296.0, type=move), ImageCaptchaTrack.Track(x=138.0, y=71.0, t=10312.0, type=move), ImageCaptchaTrack.Track(x=138.0, y=72.0, t=10329.0, type=move), ImageCaptchaTrack.Track(x=137.0, y=72.0, t=10389.0, type=move), ImageCaptchaTrack.Track(x=135.0, y=72.0, t=10405.0, type=move), ImageCaptchaTrack.Track(x=133.0, y=72.0, t=10422.0, type=move), ImageCaptchaTrack.Track(x=132.0, y=71.0, t=10438.0, type=move), ImageCaptchaTrack.Track(x=131.0, y=70.0, t=10446.0, type=move), ImageCaptchaTrack.Track(x=130.0, y=69.0, t=10462.0, type=move), ImageCaptchaTrack.Track(x=129.0, y=68.0, t=10479.0, type=move), ImageCaptchaTrack.Track(x=130.0, y=68.0, t=10589.0, type=move), ImageCaptchaTrack.Track(x=130.0, y=69.0, t=10629.0, type=move), ImageCaptchaTrack.Track(x=130.0, y=70.0, t=10654.0, type=move), ImageCaptchaTrack.Track(x=130.0, y=71.0, t=10663.0, type=move), ImageCaptchaTrack.Track(x=130.0, y=72.0, t=10765.0, type=move), ImageCaptchaTrack.Track(x=128.0, y=74.0, t=10813.0, type=move), ImageCaptchaTrack.Track(x=122.0, y=74.0, t=10829.0, type=move), ImageCaptchaTrack.Track(x=114.0, y=72.0, t=10846.0, type=move), ImageCaptchaTrack.Track(x=107.0, y=69.0, t=10863.0, type=move), ImageCaptchaTrack.Track(x=101.0, y=64.0, t=10880.0, type=move), ImageCaptchaTrack.Track(x=97.0, y=62.0, t=10896.0, type=move), ImageCaptchaTrack.Track(x=92.0, y=58.0, t=10913.0, type=move), ImageCaptchaTrack.Track(x=89.0, y=55.0, t=10930.0, type=move), ImageCaptchaTrack.Track(x=86.0, y=52.0, t=10946.0, type=move), ImageCaptchaTrack.Track(x=83.0, y=51.0, t=10963.0, type=move), ImageCaptchaTrack.Track(x=82.0, y=50.0, t=10980.0, type=move), ImageCaptchaTrack.Track(x=82.0, y=49.0, t=10998.0, type=move), ImageCaptchaTrack.Track(x=81.0, y=49.0, t=11046.0, type=move), ImageCaptchaTrack.Track(x=79.0, y=48.0, t=11063.0, type=move), ImageCaptchaTrack.Track(x=76.0, y=46.0, t=11080.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=45.0, t=11096.0, type=move), ImageCaptchaTrack.Track(x=71.0, y=43.0, t=11113.0, type=move), ImageCaptchaTrack.Track(x=71.0, y=44.0, t=11190.0, type=move), ImageCaptchaTrack.Track(x=73.0, y=47.0, t=11197.0, type=move), ImageCaptchaTrack.Track(x=77.0, y=52.0, t=11213.0, type=move), ImageCaptchaTrack.Track(x=80.0, y=57.0, t=11230.0, type=move), ImageCaptchaTrack.Track(x=82.0, y=60.0, t=11247.0, type=move), ImageCaptchaTrack.Track(x=82.0, y=62.0, t=11264.0, type=move), ImageCaptchaTrack.Track(x=83.0, y=62.0, t=11293.0, type=move), ImageCaptchaTrack.Track(x=81.0, y=62.0, t=11382.0, type=move), ImageCaptchaTrack.Track(x=78.0, y=61.0, t=11398.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=58.0, t=11413.0, type=move), ImageCaptchaTrack.Track(x=70.0, y=56.0, t=11430.0, type=move), ImageCaptchaTrack.Track(x=69.0, y=55.0, t=11447.0, type=move), ImageCaptchaTrack.Track(x=67.0, y=53.0, t=11464.0, type=move), ImageCaptchaTrack.Track(x=66.0, y=52.0, t=11480.0, type=move), ImageCaptchaTrack.Track(x=64.0, y=52.0, t=11497.0, type=move), ImageCaptchaTrack.Track(x=64.0, y=51.0, t=11514.0, type=move), ImageCaptchaTrack.Track(x=63.0, y=50.0, t=11530.0, type=move), ImageCaptchaTrack.Track(x=62.0, y=48.0, t=11547.0, type=move), ImageCaptchaTrack.Track(x=61.0, y=46.0, t=11564.0, type=move), ImageCaptchaTrack.Track(x=60.0, y=44.0, t=11581.0, type=move), ImageCaptchaTrack.Track(x=58.0, y=43.0, t=11597.0, type=move), ImageCaptchaTrack.Track(x=59.0, y=43.0, t=11846.0, type=move), ImageCaptchaTrack.Track(x=60.0, y=42.0, t=11853.0, type=move), ImageCaptchaTrack.Track(x=61.0, y=42.0, t=11864.0, type=move), ImageCaptchaTrack.Track(x=62.0, y=42.0, t=11881.0, type=move), ImageCaptchaTrack.Track(x=62.0, y=41.0, t=11898.0, type=move), ImageCaptchaTrack.Track(x=63.0, y=41.0, t=11914.0, type=move), ImageCaptchaTrack.Track(x=62.0, y=40.0, t=12485.0, type=move), ImageCaptchaTrack.Track(x=61.0, y=39.0, t=12533.0, type=move), ImageCaptchaTrack.Track(x=67.0, y=41.0, t=13014.0, type=move), ImageCaptchaTrack.Track(x=80.0, y=43.0, t=13022.0, type=move), ImageCaptchaTrack.Track(x=95.0, y=46.0, t=13032.0, type=move), ImageCaptchaTrack.Track(x=133.0, y=52.0, t=13048.0, type=move), ImageCaptchaTrack.Track(x=167.0, y=57.0, t=13065.0, type=move), ImageCaptchaTrack.Track(x=208.0, y=61.0, t=13082.0, type=move), ImageCaptchaTrack.Track(x=240.0, y=63.0, t=13099.0, type=move), ImageCaptchaTrack.Track(x=265.0, y=63.0, t=13115.0, type=move), ImageCaptchaTrack.Track(x=279.0, y=63.0, t=13132.0, type=move), ImageCaptchaTrack.Track(x=283.0, y=62.0, t=13149.0, type=move), ImageCaptchaTrack.Track(x=283.0, y=61.0, t=13166.0, type=move), ImageCaptchaTrack.Track(x=283.0, y=60.0, t=13190.0, type=move), ImageCaptchaTrack.Track(x=283.0, y=59.0, t=13198.0, type=move), ImageCaptchaTrack.Track(x=283.0, y=57.0, t=13215.0, type=move), ImageCaptchaTrack.Track(x=283.0, y=55.0, t=13232.0, type=move), ImageCaptchaTrack.Track(x=281.0, y=50.0, t=13249.0, type=move), ImageCaptchaTrack.Track(x=277.0, y=46.0, t=13266.0, type=move), ImageCaptchaTrack.Track(x=274.0, y=41.0, t=13282.0, type=move), ImageCaptchaTrack.Track(x=267.0, y=37.0, t=13299.0, type=move), ImageCaptchaTrack.Track(x=263.0, y=35.0, t=13315.0, type=move), ImageCaptchaTrack.Track(x=260.0, y=34.0, t=13332.0, type=move), ImageCaptchaTrack.Track(x=259.0, y=34.0, t=13349.0, type=move), ImageCaptchaTrack.Track(x=256.0, y=34.0, t=13366.0, type=move), ImageCaptchaTrack.Track(x=253.0, y=34.0, t=13382.0, type=move), ImageCaptchaTrack.Track(x=249.0, y=35.0, t=13399.0, type=move), ImageCaptchaTrack.Track(x=243.0, y=36.0, t=13416.0, type=move), ImageCaptchaTrack.Track(x=240.0, y=37.0, t=13432.0, type=move), ImageCaptchaTrack.Track(x=237.0, y=37.0, t=13449.0, type=move), ImageCaptchaTrack.Track(x=235.0, y=37.0, t=13465.0, type=move), ImageCaptchaTrack.Track(x=231.0, y=37.0, t=13482.0, type=move), ImageCaptchaTrack.Track(x=227.0, y=37.0, t=13499.0, type=move), ImageCaptchaTrack.Track(x=224.0, y=37.0, t=13516.0, type=move), ImageCaptchaTrack.Track(x=220.0, y=37.0, t=13532.0, type=move), ImageCaptchaTrack.Track(x=217.0, y=37.0, t=13549.0, type=move), ImageCaptchaTrack.Track(x=212.0, y=37.0, t=13566.0, type=move), ImageCaptchaTrack.Track(x=209.0, y=37.0, t=13582.0, type=move), ImageCaptchaTrack.Track(x=207.0, y=37.0, t=13599.0, type=move), ImageCaptchaTrack.Track(x=222.0, y=106.0, t=13758.0, type=click)], data=null) 简单校验 概述： 提供点击式验证码和滑动式验证码的验证机制 通过位置百分比计算和容忍度判断，验证用户操作的准确性 支持存储验证过程中的关键数据（目标值、实际值、偏差等） 点击验证码校验： 提取预设目标点信息（格式为 "x%，y%；x%,y%") 从轨迹中筛选出点击类型（CLICK）的轨迹点 两种验证模式： 有序验证：按顺序匹配点击轨迹与目标点 无序验证：不限制顺序，只要每个目标点都有匹配的点击 通过百分比计算（相对背景图尺寸）和容忍度判断位置是否匹配 滑块验证码校验： 提取预设的目标滑动百分比 计算实际滑动距离（终点 X 坐标 - 起点 X 坐标） 将实际滑动距离转换为相对于背景图宽度的百分比 校验实际百分比与目标百分比是否在容忍范围内 public boolean doValidClickCaptcha(ImageCaptchaTrack imageCaptchaTrack, AnyMap imageCaptchaValidData, Float tolerant, String type) { String validStr = imageCaptchaValidData.getString(PERCENTAGE_KEY, null); Object checkOrder = imageCaptchaValidData.getOrDefault(CLICK_IMAGE_CHECK_ORDER_KEY, true); if (ObjectUtils.isEmpty(validStr)) { return false; } String[] splitArr = validStr.split(";"); List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList(); if (trackList.size() < splitArr.length) { return false; } // 取出点击事件的轨迹数据 List<ImageCaptchaTrack.Track> clickTrackList = trackList .stream() .filter(t -> TrackTypeConstant.CLICK.equalsIgnoreCase(t.getType())) .collect(Collectors.toList()); if (clickTrackList.size() != splitArr.length) { return false; } StringBuilder sb = new StringBuilder(); List<Double> percentages = new ArrayList<>(); for (int i = 0; i < splitArr.length; i++) { String posStr = splitArr[i]; String[] posArr = posStr.split(","); float xPercentage = Float.parseFloat(posArr[0]); float yPercentage = Float.parseFloat(posArr[1]); float calcXPercentage = 0f; float calcYPercentage = 0f; if (Boolean.TRUE.equals(checkOrder)) { ImageCaptchaTrack.Track track = clickTrackList.get(0); calcXPercentage = calcPercentage(track.getX(), imageCaptchaTrack.getBgImageWidth()); calcYPercentage = calcPercentage(track.getY(), imageCaptchaTrack.getBgImageHeight()); if (!checkPercentage(calcXPercentage, xPercentage, tolerant) || !checkPercentage(calcYPercentage, yPercentage, tolerant)) { return false; } clickTrackList.remove(0); } else { boolean flag = false; for (int a = 0; a < clickTrackList.size(); a++) { ImageCaptchaTrack.Track track = clickTrackList.get(a); calcXPercentage = calcPercentage(track.getX(), imageCaptchaTrack.getBgImageWidth()); calcYPercentage = calcPercentage(track.getY(), imageCaptchaTrack.getBgImageHeight()); if (checkPercentage(calcXPercentage, xPercentage, tolerant) && checkPercentage(calcYPercentage, yPercentage, tolerant)) { // 验证命中 clickTrackList.remove(a); flag = true; break; } } if (!flag) { return false; } } if (i > 0) { sb.append("|"); } sb.append(calcXPercentage).append(",").append(calcYPercentage); percentages.add((double) ((calcXPercentage - xPercentage) + (calcYPercentage - yPercentage))); } // 存储一下当前计算出来的值 return true; } public boolean doValidSliderCaptcha(ImageCaptchaTrack imageCaptchaTrack, AnyMap imageCaptchaValidData, Float tolerant, String type) { Float oriPercentage = imageCaptchaValidData.getFloat(PERCENTAGE_KEY); if (oriPercentage == null) { // 没读取到百分比 return false; } List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList(); ImageCaptchaTrack.Track firstTrack = trackList.get(0); // 取最后一个滑动轨迹 ImageCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1); // 计算百分比 float calcPercentage = calcPercentage(lastTrack.getX() - firstTrack.getX(), imageCaptchaTrack.getBgImageWidth()); // 校验百分比 boolean percentage = checkPercentage(calcPercentage, oriPercentage, tolerant); if (percentage) { // 校验成功 // 存储一下当前计算出来的值 imageCaptchaValidData.put(USER_CURRENT_PERCENTAGE, String.valueOf(calcPercentage)); imageCaptchaValidData.put(USER_CURRENT_PERCENTAGE_STD, String.valueOf(calcPercentage - oriPercentage)); } return percentage; } protected void addPercentage(ImageCaptchaInfo imageCaptchaInfo, AnyMap imageCaptchaValidData) { float percentage = calcPercentage(imageCaptchaInfo.getRandomX(), imageCaptchaInfo.getBackgroundImageWidth()); imageCaptchaValidData.put(PERCENTAGE_KEY, percentage); } } 复杂轨迹校验 // 遍历轨迹列表 Iterator var2 = imageCaptchaTrack.getTrackList().iterator(); do { if (!var2.hasNext()) { return; // 所有轨迹点都校验通过 } ImageCaptchaTrack.Track track = (ImageCaptchaTrack.Track)var2.next(); x = track.getX(); // X坐标 y = track.getY(); // Y坐标 t = track.getT(); // 时间戳 type = track.getType();// 轨迹类型 // 只要有一个轨迹点的任何属性为空，就抛出异常 } while(x != null && y != null && t != null && !ObjectUtils.isEmpty(type)); throw new IllegalArgumentException("track[x,y,t,type] must not be null"); 根据上文获取到的坐标行为轨迹，做的校验： 滑动时间验证：检查滑动开始到结束的时间是否过短（小于 300 毫秒） 轨迹点数量验证：轨迹点数量需满足 10 ≤ 数量 ≤ 背景图宽度×5，太少（<10）可能是简单脚本，太多（> 宽度 ×5）可能是异常数据。 初始位置验证：x 轴和 y 轴应该是从 0 开始的，要是一开始 x 轴和 y 轴乱跑，返回 false 轨迹连续性与合理性验证： 相邻轨迹点的 X 或 Y 方向位移不能超过 50 像素（防止瞬间跳变，真人滑动有速度限制）； 统计 Y 坐标不变的点（机器可能水平匀速滑动，真人难免有上下波动）； 统计 X 坐标超出背景图宽度的点（正常滑动不会越界，过多越界点为异常） 滑动速度变化验证： 验证逻辑：对比滑动过程中前 70% 轨迹与后 30% 轨迹的平均时间间隔。 判断标准：真人滑动通常越到后期速度越快（后 30% 平均时间间隔 > 前 70%），若不符合此特征（如匀速或减速），则判定为异常。 public ApiResponse<?> afterValid(Boolean basicValid, ImageCaptchaTrack imageCaptchaTrack, AnyMap captchaValidData, Float tolerant, String type) { // 如果基础验证未通过，直接返回成功（无需二次验证） if (!basicValid) { return ApiResponse.ofSuccess(); // 如果不是滑块验证码类型，直接返回成功（无需验证） } else if (!CaptchaTypeClassifier.isSliderCaptcha(type)) { return ApiResponse.ofSuccess(); } long startSlidingTime = imageCaptchaTrack.getStartTime().getTime(); // 滑动开始时间 long endSlidingTime = imageCaptchaTrack.getStopTime().getTime(); // 滑动结束时间 Integer bgImageWidth = imageCaptchaTrack.getBgImageWidth(); // 背景图宽度 List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList(); // 滑动轨迹点列表 // 如果滑动时间小于300毫秒（太快），判定为机器操作 if (startSlidingTime + 300L > endSlidingTime) { return ApiResponse.ofMessage(DEFINITION); } // 轨迹点数量需在 [10, 背景图宽度*5] 范围内 else if (trackList.size() >= 10 && trackList.size() <= bgImageWidth * 5) { // 轨迹点数量合理，进入下一步验证 } else { // 轨迹点太少（<10）或太多（>宽度*5），判定为异常 return ApiResponse.ofMessage(DEFINITION); } ImageCaptchaTrack.Track firstTrack = trackList.get(0); // 第一个轨迹点 // 检查初始位置是否在 [(-10,-10), (10,10)] 范围内（接近起点） if (!(firstTrack.getX() > 10.0F) && !(firstTrack.getX() < -10.0F) && !(firstTrack.getY() > 10.0F) && !(firstTrack.getY() < -10.0F)) { // 初始位置合理，进入下一步验证 } else { // 初始位置偏离起点过远，判定为异常 return ApiResponse.ofMessage(DEFINITION); } int check4 = 0; // 记录Y坐标与初始Y相同的轨迹点数量 int check7 = 0; // 记录X坐标超出背景图宽度的轨迹点数量 for(i = 1; i < trackList.size(); ++i) { track = trackList.get(i); x = track.getX(); y = track.getY(); // 统计Y坐标未变化的点（机器可能水平滑动，Y值不变） if (firstTrack.getY() == y) { ++check4; } // 统计X坐标超出背景图宽度的点（异常越界） if (x >= bgImageWidth) { ++check7; } // 检查相邻轨迹点的位移是否过大（单次移动>50像素） preTrack = trackList.get(i - 1); if (track.getX() - preTrack.getX() > 50.0F || track.getY() - preTrack.getY() > 50.0F) { return ApiResponse.ofMessage(DEFINITION); // 位移过大，判定为异常 } } // 条件1：Y坐标不变的点数量≠总轨迹点数量（排除完全水平滑动） // 条件2：越界点数量≤200（允许少量异常，但不能过多） if (check4 != trackList.size() && check7 <= 200) { // 取轨迹的前70%和后30%，计算两段的平均时间间隔 int i = (int)(trackList.size() * 0.7); ImageCaptchaTrack.Track track70 = trackList.get(i - 1); float avgTimeFirst70 = track70.getT() / i; // 前70%轨迹的平均时间 ImageCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1); double avgTimeLast30 = lastTrack.getT() / (trackList.size() - i); // 后30%轨迹的平均时间 // 真人滑动通常会越到后面越快（平均时间间隔增大） if (avgTimeLast30 > avgTimeFirst70) { return ApiResponse.ofSuccess(); // 符合真人特征，验证通过 } else { return ApiResponse.ofMessage(DEFINITION); // 速度变化异常，验证失败 } } else { return ApiResponse.ofMessage(DEFINITION); // 异常点比例过高，验证失败 } }
前言 由于国内法律法规的限制，免费的SSL证书由1年变成了3个月，对于在弄了一个带有自己域名博客的人来说，这是一件比较无语的事情，每3个月重复性的工作需要手动更换SSL证书。网上也有很多docker版本的自动续签的应用，如宝塔，acme.sh等等。但是安装这种软件的话，功能就多了许多其它的，个人又不需要。因此自己简单弄一个脚本去实现就十分必要。 操作过程 1、首先需要在工作台创建一个子账号，并取得账号分配的key与secret，给子用户分配AliyunDNSFULLAccess与AliyunYunDunCertAccess权限。 2、生成2048位的crs证书，一般以RSA算法为主，通过Linux系统中自带的openssl命令生成即可 #openssl req -new -nodes -sha256 -newkey rsa:2048 -keyout blog.key -out blog.csr -config openssl.cnf [req] default_bits = 2048 prompt = no default_md = sha256 distinguished_name = dn req_extensions = req_ext [dn] CN = aiwin.net.cn emailAddress = test@foxmail.com O = zirong OU = liu L = Guangzhou ST = Guangdong C = CN [req_ext] subjectAltName = @alt_names [alt_names] DNS.1 = aiwin.net.cn DNS.2 = www.aiwin.net.cn 3、通过crontab定时任务每3个月执行脚本，注意脚本中的crs需要带上\r\n或\n，最好通过一行直接写完，否则传输到阿里云接口会因为没有固定格式的分行而报错。 import os import time import logging from alibabacloud_cas20200407.client import Client as cas20200407Client from alibabacloud_credentials.client import Client as CredentialClient from alibabacloud_tea_openapi import models as open_api_models from alibabacloud_cas20200407 import models as cas_20200407_models from alibabacloud_tea_util import models as util_models from alibabacloud_alidns20150109.client import Client as Alidns20150109Client from alibabacloud_alidns20150109 import models as alidns_20150109_models logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') ''' 全局存储OrderId与RecordId ''' OrderIdList = [] RecordIdList = [] class SSL: def __init__(self, client, endpoint): self.client = self.create_client(client, endpoint) ''' 环境变量设置id与secret,根据不同的方法与endpoint创建不同的安全功能客户端 @ALIBABA_CLOUD_ACCESS_KEY_ID @ALIBABA_CLOUD_ACCESS_KEY_SECRET ''' @staticmethod def create_client(func, endpoint): credential = CredentialClient() config = open_api_models.Config( credential=credential, region_id='cn-hangzhou', endpoint=endpoint, connect_timeout=5000, read_timeout=5000 ) return func(config) ''' 请求创建SSL证书并获取订单ID ''' @staticmethod def main(self): create_certificate_for_package_request_request = cas_20200407_models.CreateCertificateForPackageRequestRequest( csr='''-----BEGIN CERTIFICATE REQUEST-----\r\n内容\r\n-----END CERTIFICATE REQUEST-----''', product_code='digicert-free-1-free', username='xxxx', phone='xxxx', email='test@foxmail.com', domain='aiwin.net.cn', validate_type='DNS' ) runtime = util_models.RuntimeOptions() try: response = self.client.create_certificate_for_package_request_with_options( create_certificate_for_package_request_request, runtime) OrderIdList.append(response.body.order_id) self.parseOrderId(self) except Exception as error: logging.error(error) ''' 通过订单Id,请求SSL证书详情,获取验证时需要设置的域名与TXT记录,并且每相隔60秒请求一次是否已签发接口,签发即删除DNS记录并获取pem与key ''' @staticmethod def parseOrderId(self): describe_certificate_state_request = cas_20200407_models.DescribeCertificateStateRequest( order_id=OrderIdList[0] ) runtime = util_models.RuntimeOptions() try: response = self.client.describe_certificate_state_with_options(describe_certificate_state_request, runtime) self.addDNS(self, response.body.record_domain, response.body.record_value) IssuedResult = self.ConfirmSSLIssued(self) while not IssuedResult: time.sleep(60) IssuedResult = self.ConfirmSSLIssued(self) self.delDNS(self) self.getPrivatePem(self) except Exception as error: logging.error(error) ''' 添加DNS记录 ''' @staticmethod def addDNS(self, domain, value): self.client = self.create_client(Alidns20150109Client, 'alidns.cn-hangzhou.aliyuncs.com') add_domain_record_request = alidns_20150109_models.AddDomainRecordRequest( lang='zh', type='TXT', value='wdadawdawd.txt', rr='_dnsauth.aiwin.net.cn', # value=value, # rr=domain, domain_name='aiwin.net.cn' ) runtime = util_models.RuntimeOptions() try: response = self.client.add_domain_record_with_options(add_domain_record_request, runtime) RecordIdList.append(response.body.record_id) except Exception as error: logging.error(error) ''' 根据RecordId删除DNS记录,并清空RecordId ''' @staticmethod def delDNS(self) -> None: self.client = self.create_client(Alidns20150109Client, 'alidns.cn-hangzhou.aliyuncs.com') delete_domain_record_request = alidns_20150109_models.DeleteDomainRecordRequest( record_id=RecordIdList[0], lang='zh' ) runtime = util_models.RuntimeOptions() try: response = self.client.delete_domain_record_with_options(delete_domain_record_request, runtime) if response.status_code == 200: RecordIdList.clear() print(RecordIdList) except Exception as error: logging.error(error) ''' 验证创建的证书是否已签发状态 ''' @staticmethod def ConfirmSSLIssued(self): self.client = self.create_client(cas20200407Client, 'cas.aliyuncs.com') list_user_certificate_order_request = cas_20200407_models.ListUserCertificateOrderRequest(status='ISSUED') runtime = util_models.RuntimeOptions() try: response = self.client.list_user_certificate_order_with_options(list_user_certificate_order_request, runtime) for ResponseBodyCertificateOrderList in response.body.certificate_order_list: if OrderIdList[0] == ResponseBodyCertificateOrderList.order_id: return True return False except Exception as error: logging.error(error) ''' 根据订单ID,查看证书的详情,并取得certificate与private_key,即nginx文件中的pem和key内容 ''' @staticmethod def getPrivatePem(self): self.client = self.create_client(cas20200407Client, 'cas.aliyuncs.com') describe_certificate_state_request = cas_20200407_models.DescribeCertificateStateRequest( order_id=OrderIdList[0] ) runtime = util_models.RuntimeOptions() try: response = self.client.describe_certificate_state_with_options(describe_certificate_state_request, runtime) self.CreateFile(self, "\n".join(response.body.certificate.splitlines()), "\n".join(response.body.private_key.splitlines())) except Exception as error: logging.error(error) ''' 移除原文件,将新内容写入到nginx.conf路径文件当中 ''' @staticmethod def CreateFile(self, pem_body, key_body): try: os.remove('nginx中pem的路径') os.remove('nginx中key的路径') KeyFile = open('替换为域名.key', 'a') PemFile = open('替换为域名.pem', 'a') KeyFile.write(key_body) PemFile.write(pem_body) logging.info('pem与key文件创建成功') except Exception as error: logging.error(error) if __name__ == '__main__': ssl = SSL(cas20200407Client, 'cas.aliyuncs.com') ssl.main(ssl)
前言 简单的入门一下Go、会对基础语法、网络编程、Gin开发进行简单的了解，关键是Gin开发。 关于Go Go语言（也称为Golang）是Google开发的一种开源编程语言。它被设计用于构建高效、可靠和可扩展的软件系统。下面是Go语言的一些主要用途： 服务器端开发：Go语言提供了强大的标准库和并发模型，使其成为构建高性能网络服务器的理想选择。许多大型互联网公司正在使用Go语言来开发后端服务，以处理高负载和并发请求。 网络编程：Go语言提供了丰富的网络编程库，可用于开发各种网络应用程序，包括Web服务、API服务器、网络代理等。 分布式系统：Go语言的并发模型和原生支持的并发原语（goroutine和channel）使其非常适合构建分布式系统，例如数据处理管道、消息队列等。 命令行工具：Go语言的编译速度快，生成的可执行文件体积小，使其成为开发命令行工具的良好选择。许多开发者使用Go语言来构建工具、脚本和自动化任务。 嵌入式系统：Go语言可以用于编写嵌入式系统的控制逻辑和驱动程序。它提供了对底层硬件的访问和控制能力，并具有较小的内存消耗。 需要注意的是，Go语言具有简洁而直观的语法，易于学习和使用。它的性能非常好，可以充分利用多核处理器和并发编程来提高应用程序的性能和吞吐量。 学习流程 基础语法->Web开发->常见中间件->云平台 基础语法 注释 增强语言的可读性 单行注释 多行注释 package main import "fmt" // 单行注释 /* 多行注释 多行注释 */ func main() { fmt.Println("hello world") } 变量 var 定义变量，var 变量名 变量类型。 简短变量声明，使用:=运算符可以在函数内部声明并初始化变量。 匿名变量，使用 _ 占位符可以声明一个匿名变量，忽略不需要的值，任何赋值給这个标识符的值都将被抛弃，并且不会导致变量的冲突 定义多个变量，可以使用()包裹，表示定义多个变量 注意点：变量名的首个字符不能为数字，全局变量可被局部变量再定义(就近原则 )，定义的变量一定要使用。 变量声明后的默认值： 整数型浮点数变量默认值是0和0.0 字符串变量默认值是空字符串 布尔型变量默认是false 切片，函数，指针变量默认是nil Printf输出时声明的格式 %v：默认格式化输出，会根据变量的类型自动选择合适的格式。 %s：输出字符串。 %d、%b、%o、%x：输出整数，分别表示十进制、二进制、八进制和十六进制。 %t：输出布尔值，结果为 true 或 false。 %f、%e、%g：输出浮点数，分别表示十进制表示法、科学计数法和通用格式。 %p：输出指针地址。 %c：输出字符。 %q：输出带引号的字符串。 %%：输出一个百分号。 fmt.Printf("内存地址:%p,变量类型:%T",name,&name) //打印内存地址，变量类型等 例子： package main import "fmt" var name = "Lau" //全局变量(隐式定义) func fun() (int, int) { return 100, 200 } func main() { var name string = "aiwin" //显示定义 var a, b int //同时定义a,b两个变量 age := 18 fmt.Printf("姓名:%s,内存地址为:%p,类型为:%T,年龄:%d,年龄十六进制数为:%x\n", name, &name, name, age, age) //就近原则 a, _ = fun() _, b = fun() fmt.Println("_可代替被舍弃的值,并且可被重复定义:", a, b) a, b = b, a fmt.Println("类似于Python,可直接进行值交换:", a, b) } 常量 使用const来定义常量，不可改变 iota 开始是0，默认会不断的自增进行计数，相当于是一个常量的计数器，直至新的一组常量计数器出现才会恢复，可理解过const语句块的索引 package main import "fmt" func main() { const ( a = iota b c d = "aiwin" e //未被定义所以用上一个的值,但是iota还是会一直计数 f = 100 g //未被定义所以用上一个的值 h = iota j ) const ( k = iota //新的const出现,新iota被重新从0开始计数 l ) fmt.Println(a, b, c, d, e, f, g, h, j, k, l) //0 1 2 aiwin aiwin 100 100 7 8 0 1 } 数据类型 布尔型bool，默认值是false 数字类型，分为int和float，并且支持复数，位运算采用补码 序号 类型和描述 1 uint8无符号8位整型(0~255) 2 uint16无符号16位整型(0~65535) 3 uint32无符号32位整型(0~4294967295) 4 uint64无符号64位整型(0~18446744073709551615) 5 int8有符号8位整型(-128~127)i 6 int16符号16位整型(-32768~32767) 7 int32有符号32位整型(-2147483648~2147483647) 8 int64有符号64位整型(-9223372036854775808~9223372036854775807) float浮点型，默认是64位，保留6位小数，保留小数会丢失精度，采取四舍五入的原则 序号 类型和描述 1 float32 IEEE-754 32位浮点型数 2 float64 IEEE-754 64位浮点型数 3 complex64 32 位实数和虚数 4 complex128 64 位实数和虚数 类型别名，Go语言会有一些类型的别名 序号 类型和描述 1 byte类似uint8 2 int和uint一样大小 3 rune类似int32 7 uintptr无符号整型，用于存放指针 字符类型，' 和" 和双引号包裹的字符是有差别的，' 默认是int32 类型，会自动转换成Unicode 编码的值，字符可以直接使用+ 连接 类型转换 ，Go语言不存在隐式类型转换，所有的类型转换都必须是显式的声明 package main import ( "fmt" ) func main() { var age byte = 18 //相当于uint8 //超过范围,报错 age = 9223372036854775808 var num1 float32 = -123.0000901 var num2 float64 = -123.0000901 fmt.Println("num1=", num1, "num2=", num2) //精度缺失 fmt.Println("转换后导致的精度丢失:num=", float32(num2)) var num3 float64 = 3.19 fmt.Printf("num3=%.1f\n", num3) //四舍五入,输出3.2 fmt.Printf("age的类型为%T,数值为%d", age, age) str := "Hello" str1 := '中' //使用Unicode编码表,会自动认为是int32类型 str2 := "World" fmt.Printf("%T,%s\n", str, str) fmt.Printf("%T,%d\n", str1, str1) //默认是int32类型,转换成数字 fmt.Println(str + "," + str2) var b uint16 = 256 fmt.Println("b=", uint8(b)) //变成了0 /* flag := 2 fmt.Println(bool(flag)) 整型不能转换成bool类型*/ } 运算符 运算符 描述 && 逻辑 AND 运算符。 如果两边的操作数都是 True，则条件 True，否则为 False。 双竖线 逻辑 OR 运算符。 如果两边的操作数有一个 True，则条件 True，否则为 False。 ! 逻辑 NOT 运算符。 如果条件为 True，则逻辑 NOT 条件 False，否则为 True。 运算符 描述 & 按位与运算符"&"是双目运算符。都是1结果为1，否则为0 竖线 按位或运算符"竖线"是双目运算符。 都是0结果为0，否则为1 ^ 按位异或运算符"^"是双目运算符。 其功能是参与运算的两数各对应的二进位相异或，当两对应的二进位相异时，结果为1。 << 左移运算符"<<"是双目运算符。左移n位就是乘以2的n次方。 其功能把"<<"左边的运算数的各二进位全部左移若干位，由"<<"右边的数指定移动的位数，高位丢弃，低位补0。 >> 右移运算符">>"是双目运算符。右移n位就是除以2的n次方。 其功能是把">>"左边的运算数的各二进位全部右移若干位，">>"右边的数指定移动的位数。 &^ 位清空，a&^b，对于b上的每个数值，如果为0，则取a对应位上的数，如果为1，则取0 运算符 描述 & 返回变量存储地址 * 指针变量。 package main import "fmt" func main() { var a uint = 13 //0011 1100 var b uint = 60 //0000 1101 fmt.Println(a & b) //0000 1100 fmt.Println(a | b) //0011 1101 fmt.Println(a ^ b) //0011 0001 fmt.Println(a &^ b) // 0000 0001 fmt.Println(b >> a) //60右移60位,结果为0 } fmt库 常用的一些函数 Print / Println / Printf：格式化并输出到标准输出。 Println：类似于 Print，但在输出后添加换行符。 Printf：使用格式化字符串进行输出（类似于 C 语言中的 printf 函数）。 Sprint / Sprintln / Sprintf：将格式化的结果以字符串形式返回，而不是输出到标准输出。 Fprint / Fprintln / Fprintf：将格式化的结果输出到指定的文件（io.Writer）。 Errorf：生成一个格式化的错误字符串。 Scan / Scanln / Scanf：从标准输入读取并格式化输入。 Sscan / Sscanln / Sscanf：从给定的字符串中读取并格式化输入。 Fscan / Fscanln / Fscanf：从指定的文件（io.Reader）中读取并格式化输入。 package main import ( "fmt" "os" ) func main() { name := "Alice" age := 30 height := 1.68 // 格式化并输出到标准输出 fmt.Print("Hello, ") fmt.Print(name) fmt.Println("!") // 使用格式化字符串进行输出 fmt.Printf("%s is %d years old.\n", name, age) // 输出到指定文件 file, _ := os.Create("user.gob") defer file.Close() fmt.Fprintln(file, name) fmt.Fprintf(file, "%d", age) //标准化读取文件输入 file, _ = os.Open("user.gob") defer file.Close() var ReadName string var ReadAge int fmt.Fscanln(file, &ReadName) //以行为单位来读取 fmt.Fscanln(file, &ReadAge) fmt.Printf("读取到的数据为,Name: %s,Age: %d\n", ReadName, ReadAge) // 将格式化的结果以字符串形式返回 info := fmt.Sprintf("Name: %s, Age: %d, Height: %.2f", name, age, height) fmt.Println(info) // 从标准输入读取并格式化输入 var input string fmt.Print("Enter your name: ") fmt.Scanln(&input) fmt.Printf("Hello, %s!\n", input) } 流程控制 if、switch、select 语句 描述 if语句 if 语句 由一个布尔表达式后紧跟一个或多个语句组成。 if else if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。 else if 你可以在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句。 switch switch 语句用于基于不同条件执行不同动作。 fallthrough 当使用swich语句时，可以使用fallthrough 进行case穿透，下面的条件一定会执行 select select 语句类似于 switch 语句，但是select会随机执行一个可运行的case。如果没有case可运行，它将阻塞，直到有case可运行。 package main import ( "fmt" "time" ) func main() { //if语句 var password string var username string fmt.Print("请输入账号:") fmt.Scan(&username) fmt.Print("请输入密码:") fmt.Scan(&password) if username == "admin" { if password == "Qyx@Sxf715" { fmt.Println("登录成功") } else { fmt.Println("密码错误") } } else { fmt.Println("用户名错误") //注意这里的else一定要接在if的}后面,不能进行换行 } //switch var score int = 88 switch { case score >= 90: fmt.Println("成绩为A级") case score >= 80 && score < 90: fmt.Println("成绩为B级") //fallthrough 一定会把下面的一个case也穿透掉 case score >= 70 && score < 80: fmt.Println("成绩为C级") case score >= 60 && score < 70: fmt.Println("成绩为D级") default: fmt.Println("成绩为不及格") } //select语句的使用 ch1 := make(chan string) //创建了两个通道 ch1 和 ch2 ch2 := make(chan string) //两个匿名的 goroutine 分别向这两个通道发送值 go func() { time.Sleep(2 * time.Second) ch1 <- "Hello" }() go func() { time.Sleep(3 * time.Second) ch2 <- "World" }() /*select 会同时监听多个通道的操作，当任何一个 case 中的操作就绪时，该 case 就会被执行。 如果同时有多个 case 就绪，select 随机选择一个可执行的 case 来执行。 如果没有任何 case 就绪，并且存在 default 分支，那么执行 default 分支。*/ select { case msg1 := <-ch1: fmt.Println("Received from ch1:", msg1) case msg2 := <-ch2: fmt.Println("Received from ch2:", msg2) case <-time.After(5 * time.Second): fmt.Println("Timed out") } } for、break、continue Go语言的for循环也需要三个参数，起始位，最终位，间距 ，但是三个参数都可以省略掉。 package main import "fmt" func main() { //9*9乘法表 for j := 1; j <= 9; j++ { for i := 1; i <= j; i++ { fmt.Printf("%dx%d=%d \t", i, j, i*j) } fmt.Println() } for j := 1; j <= 9; j++ { if j == 5 { //结束掉整个循环 break } fmt.Print(j) } fmt.Println() for j := 1; j <= 9; j++ { if j == 5 { //结束当次循环 continue } fmt.Print(j) } } 遍历String package main import "fmt" func main() { var str string = "Hello,Aiwin" fmt.Println(str) fmt.Printf("字符串的长度为%d\n", len(str)) fmt.Printf("第二个字符是%c\n", str[1]) //遍历字符串 for i := 0; i < len(str); i++ { fmt.Printf("%c", str[i]) } //for range fmt.Println("\n") for i, v := range str { fmt.Printf("%d%c\n", i, v) } } 函数 函数是一个基本代码块，用于执行一个任务 Go语言最少有一个main函数 函数声明告诉编译器函数的名称，返回类型，参数 函数本身也是一个变量，也可以进行赋值 function 函数名(参数，参数类型)(返回类型){} 形式参数：定义函数时，用于接收外部传入数据的参数 实际参数：调用函数时，传给形参的实际数据是实际参数 可变参数：参数类型确定，但是数量不确定，可以用...，可变参数前面可以继续定义参数，后面不能再定义参数，一个函数列表只能有一个可变参数 package main import ( "fmt" ) func main() { fmt.Println("1+2的结果为", add(1, 2)) x, y := swap("你好", "Go语言") fmt.Println(x, y) printMessage("一个参数的函数") printStatic() fmt.Println("求和函数:", getSum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) } // 有多个返回值函数 func swap(x, y string) (string, string) { return y, x } // 有两个参数的函数 func add(a, b int) int { result := a + b return result } // 一个参数函数 func printMessage(msg string) { fmt.Println(msg) } // 无参数函数 func printStatic() { fmt.Println("无参数函数") } func getSum(number ...int) int { sum := 0 for i := 0; i < len(number); i++ { sum += number[i] } return sum } 值传递和引用传递 值类型数据：操作的是数据本身，如int，string ，bool，array，改变值不会改变数据本身，地址是不一样的 引用类型数据，操作的是数据的地址，如slice，map ，chanel，改变的时候会一起改变，函数地址是一样的。 defer 可以在函数中添加多个defer语句，当函数执行到最后时，这些defer语句会逆序执行，可以用于在函数返回前关闭相应的资源等操作 package main import "fmt" func main() { //值传递 arr := [4]int{1, 2, 3, 4} updateArr(arr) fmt.Println(arr) //引用传递 sli := []int{1, 2, 3, 4, 5} updateSlice(sli) fmt.Println(sli) //defer函数 a := 10 defer MyPrint(a) //输出10,已经传递进去了,一切准备就绪 a++ fmt.Println(a) } func updateArr(arr [4]int) { arr[1] = 100 fmt.Println(arr) } func updateSlice(sli []int) { sli[1] = 100 fmt.Println(sli) } func MyPrint(number int) { fmt.Println(number) } init init()函数不能被其它函数调用，而是在main函数执行之前自动被调用 init 函数不能作为参数传入，不能有传入参数和返回值 当一个main有多个init函数，谁在前谁就先执行 匿名、回调、闭包函数 也叫闭包函数(closures),允许临时创建一个没有指定名称的函数 回调函数就是一个函数作为另一个函数的参数。 闭包结构，一个外层函数中有内层函数，该内层函数中可以操作外层函数的局部变量，并且外层函数的返回值是内层函数，这种结构是闭包结构。 正常的局部变量会随着函数的调用而创建，随着函数的结束而销毁，但是在闭包结构中的外层函数的局部变量并不会随着外层函数的结果而销毁，因为内层函数还在堆栈中使用 package main import "fmt" func main() { r1 := operator(1, 2, add) fmt.Println(r1) //匿名函数 r2 := operator(3, 2, func(a, b int) int { if b == 0 { return 0 } return a / b }) fmt.Println(r2) //闭包结构 f1 := increment() fmt.Println(f1()) fmt.Println(f1()) f2 := increment() fmt.Println(f2()) fmt.Println(f1())//输出3,f1没有被销毁 } // 回调函数,fun是告诫函数,operator是回调函数 func operator(a, b int, fun func(int, int) int) int { return fun(a, b) } func add(a, b int) int { return a + b } func increment() func() int { i := 0 fun := func() int { i++ return i } return fun } 数组和切片 数组的数量在创建的时候就定义的，不可再进行改变。 数组的定义语法： var 数组名 [数量]数据类型=[数量]数据类型{数据} 切片相对于数组来说更加灵活，切片的长度是可变的，相当于可切的数组 package main import "fmt" func main() { var nameArr [3]string = [3]string{"Hello", "Wor", "ld"} fmt.Println(nameArr) var nameList []string nameList = append(nameList, "Hello") fmt.Println(nameList) } Map Map就是Python中的字典(键值对)，Map创建的时候一定需要初始化 var 变量名 map[键类型]值类型{} package main import "fmt" func main() { var userMap map[int]string = map[int]string{ 1: "Aiwin", 2: "Lau", 3: "", } fmt.Println(userMap[1]) value, ok := userMap[3] //3存在,ok是true fmt.Println(value, ok) userMap[1] = "LauAiwin" delete(userMap, 3) fmt.Println(userMap) } 结构体 结构体定义，可以类比为对象 type 结构体名称 struct{ 名称 类型 } package main import ( "encoding/json" "fmt" ) type Parent struct { Name string Age int } type Children struct { Parent Name string Age int } func (children *Children) setChildernName(name string, age int) { //引用传递 children.Name = name children.Age = age } type User struct { Username string `json:"username"` //转换结果为username Password string `json:"-"` //不显示 Age int `json:"age,omitempty"` //抛弃空值 } func main() { parent := Parent{Name: "Lau", Age: 40} childern := Children{parent, "Aiwin", 20} fmt.Printf("%s的父亲是:%s,年龄是:%d\n", childern.Name, childern.Parent.Name, childern.Parent.Age) childern.setChildernName("LauAiwin", 19) fmt.Println(childern) user := User{"Aiwin", "123456", 0} byteData, _ := json.Marshal(user) fmt.Println(string(byteData)) } 自定义数据类型 自定义类型的本意就是为了代码更简化、易于理解、方便维护。 类型别名(就是将类型赋值给一个type) 不能绑定方法 打印类型还是原始类型 类型别名不用转换 package main import "fmt" type Code int const ( SuccessCode Code = 1 ErrorCode Code = 2 ) func (code Code) getMessage() (message string) { switch code { case SuccessCode: return "请求成功" case ErrorCode: return "请求失败" } return "" } func (code Code) result() (result Code, message string) { return code, code.getMessage() } func Request(name string) (code Code, message string) { if name == "admin" { return SuccessCode.result() } else { return ErrorCode.result() } } func main() { fmt.Println(Request("admin")) } 接口 接口是一组仅包含方法名、参数、返回值的为具体实现的方法的集合，同样接口也不能绑定方法 package main import "fmt" type Student struct { name string } type Name interface { getName() string } func (student Student) getName() string { return student.name } type Teacher struct { name string } func (teacher Teacher) getName() string { return teacher.name } // 接口,可以统一传入其它的类型 func getName(name Name) string { //name.(Teacher) switch types := name.(type) { //类型断言 case Teacher: fmt.Println(types) case Student: fmt.Println(types) } return name.getName() } func MyPrint(val interface{}) { //空接口 fmt.Println(val) } func main() { student := Student{"Aiwin"} teacher := Teacher{"Xd"} fmt.Println(getName(student)) fmt.Println(getName(teacher)) MyPrint(1) } 协程和channel 协程可以理解为轻量级线程，一个线程可以拥有多个协程，与线程相比，协程不受操作系统调度，协程调度器按照调度策略把协程调度到线程中执行，协程调度器由应用程序的runtime包提供，用户使用go关键字即可创建协程，这也就是GO在语言层面直接支持协程的特色 package main import ( "fmt" "sync" "time" ) func shopping(name string, group *sync.WaitGroup) { fmt.Printf("%s 开始攻击\n", name) time.Sleep(1 * time.Second) fmt.Printf("%s 停止攻击\n", name) group.Done() } func main() { var group sync.WaitGroup StartTime := time.Now() group.Add(3) go shopping("张三", &group) go shopping("李四", &group) go shopping("王五", &group) group.Wait() fmt.Println(time.Since(StartTime)) } 那么协程里面产生的数据，怎么传递给主线程，Go 官方使用channel 来传递 package main import ( "fmt" "sync" "time" ) var moneyChanel chan int = make(chan int) var nameChanel chan string = make(chan string) var DoneChanel chan struct{} = make(chan struct{}) func shopping(name string, money int, group *sync.WaitGroup) { fmt.Printf("%s 开始攻击\n", name) time.Sleep(1 * time.Second) fmt.Printf("%s 停止攻击\n", name) moneyChanel <- money nameChanel <- name group.Done() } func main() { var group sync.WaitGroup StartTime := time.Now() group.Add(3) go shopping("张三", 100, &group) go shopping("李四", 150, &group) go shopping("王五", 160, &group) var moneyList []int var nameList []string go func() { //解决moneyChanel一直死循环的问题 defer close(moneyChanel) defer close(nameChanel) defer close(DoneChanel) group.Wait() }() //go func(){ // for money := range moneyChanel { // moneyList = append(moneyList, money) //解决moneyChannel被一直输数据问题 // } //}() //for name := range nameChanel { // nameList = append(nameList, name) //} event := func() { for { select { case names := <-nameChanel: nameList = append(nameList, names) case money := <-moneyChanel: moneyList = append(moneyList, money) case <-DoneChanel://解决当协程全部完事,退出循环的问题 return } } } event() fmt.Println(moneyList) fmt.Println(nameList) fmt.Println(time.Since(StartTime)) } 超时 package main import ( "fmt" "time" ) var doneChanel = make(chan struct{}) func timeOut() { fmt.Println("开始") time.Sleep(3 * time.Second) fmt.Println("结束") close(doneChanel) } func main() { go timeOut() select { case <-doneChanel: fmt.Printf("执行完成") case <-time.After(4 * time.Second): fmt.Println("超时") return } } 线程锁 package main import ( "fmt" "sync" ) var sum int var wait sync.WaitGroup var lock sync.Mutex func add() { lock.Lock() //线程锁,不然会线程紊乱 for i := 0; i < 10000; i++ { sum++ } lock.Unlock() wait.Done() } func sub() { lock.Lock() for i := 0; i < 10000; i++ { sum-- } lock.Unlock() wait.Done() } func main() { wait.Add(2) go add() go sub() wait.Wait() fmt.Println(sum) var maps = sync.Map{}//Map的协程紊乱,要使用这种创建方式 go func() { for { maps.Store(1, "Aiwin") } }() go func() { for { fmt.Println(maps.Load(1)) } }() select {} } 异常处理 Go语言没有捕获异常的机制，每次都要接error ，这是Go语言的一个诟病，异常处理可分为三种，分别是中断、恢复、从上一级返回处理。 例子： package main import ( "errors" "fmt" ) // 中断仅适用于init开始时 // // func init() { // _, err := os.ReadFile("aaa") // if err != nil { // panic("中断报错了") // } // } func div(a, b int) (res int, err error) { if b == 0 { err = errors.New("除数不能为0") return 0, err } res = a / b return res, nil } func zhixing() (res int, err error) { res, err = div(2, 0) if err != nil { return 0, err } res += 2 return res, nil } func recovery() { defer func() { recover() }() var lists []int = []int{1, 2, 3} fmt.Println(lists[4]) } func main() { /*错误向上处理 res, err := zhixing() if err != nil { fmt.Println(err) return } fmt.Println(res)*/ recovery() fmt.Println("恢复正常逻辑") } 泛型 Go语言的泛型是指在定义函数、数据结构或接口时，可以不指定具体数据类型，而是以一种通用的方式编写代码，以便在不同的数据类型上有效地进行操作。泛型使得代码更具有通用性和可复用性，因为它可以适用于多种不同类型的数据而无需重复编写相似的代码。 比如说结构的泛型： package main import ( "encoding/json" "fmt" ) type Response[A any] struct { Code int `json:"code"` Msg string `json:"msg"` Data A `json:"data"` } type User struct { Name string `json:"name"` } type User1 struct { Name string `json:"name"` Age int `json:"age"` } func main() { //UserInfo := Response[User]{ // Code: 1, // Msg: "反序列化", // Data: User{ // Name: "Aiwin", // }, //} //marshal, _ := json.Marshal(UserInfo) //fmt.Println(string(marshal)) var UnMarRes Response[User] json.Unmarshal([]byte(`{"code":1,"msg":"反序列化","data":{"name":"Aiwin"}}`), &UnMarRes) //通过泛型可以识别是属于哪一个User fmt.Println(UnMarRes.Data.Name) } 文件读取 os.ReadFile()一次性读取整个文件的内容。 os.Open()分片读取。 bufio依赖来读取，指定分割符，换行符等 package main import ( "bufio" "fmt" "os" ) func main() { file, err := os.Open("text.txt") if err != nil { panic("文件读取错误") } //buf := bufio.NewReader(file) //for { // line, _, err := buf.ReadLine() // if err == io.EOF { // break // } // fmt.Println(string(line)) //} //指定分割符 scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) for scanner.Scan() { fmt.Println(scanner.Text()) } } 文件写入 os.openFile()中flag的类型： const ( O_RDONLY int = syscall.O_RDONLY // open the file read-only. O_WRONLY int = syscall.O_WRONLY // open the file write-only. O_RDWR int = syscall.O_RDWR // open the file read-write. O_APPEND int = syscall.O_APPEND // append data to the file when writing. O_CREATE int = syscall.O_CREAT // create a new file if none exists. O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist. O_SYNC int = syscall.O_SYNC // open for synchronous I/O. O_TRUNC int = syscall.O_TRUNC // truncate regular writable file when opened. ) 例子: package main import ( "fmt" "io" "os" ) func main() { //文件写入 file, err := os.OpenFile("text.txt", os.O_CREATE|os.O_RDWR, 0777) //模式和权限,权限仅针对linux系统 if err != nil { panic("文件写入失败") } defer file.Close() file.Write([]byte("hello")) //文件写入 err1 := os.WriteFile("text.txt", []byte("password"), 0777) //全部写入,会全覆盖 fmt.Println(err1) //文件复制 rFile, err2 := os.Open("text.txt") if err2 != nil { panic("文件不可取") } wFile, err3 := os.OpenFile("copy.txt", os.O_CREATE|os.O_WRONLY, 0777) if err3 != nil { panic("文件错误") } defer wFile.Close() io.Copy(wFile, rFile) //文件复制 dir, err := os.ReadDir("基础语法") if err != nil { panic("error") } for _, entry := range dir { info, _ := entry.Info() fmt.Println(entry.IsDir(), entry.Name(), info.Size()) } } 反射 reflect.typeof 获取类型 reflect.Valueof 获取值 setInt、setString 重新设置值 demo package main import ( "fmt" "reflect" ) func getType(obj any) { v := reflect.TypeOf(obj) switch v.Kind() { case reflect.Int: fmt.Println("获取到Int类型") case reflect.String: fmt.Println("获取到String类型") } } //注意要更改值，需要使用指针的形式更改 func setValue(obj any, value any) { v1 := reflect.ValueOf(obj) v2 := reflect.ValueOf(value) if v1.Elem().Kind() != v2.Kind() { //获取一个值的指针所指向的元素值 return } switch v1.Elem().Kind() { case reflect.Int: v1.Elem().SetInt(v2.Int()) case reflect.String: v1.Elem().SetString(value.(string)) } } func main() { var name = "张三" var age = 24 getType(name) getType(age) setValue(&name, "李四") setValue(&age, 25) fmt.Println(name, age) } package main import ( "fmt" "reflect" ) type User struct { UserName string `json:"userName"` Password string `json:"password"` } func demo(obj any) { t := reflect.TypeOf(obj) v := reflect.ValueOf(obj) for i := 0; i < t.NumField(); i++ { value := v.Field(i) fmt.Println(value) } for i := 0; i < t.NumMethod(); i++ { m := t.Method(i) if m.Name != "Call" { //方法名必须为大写 continue } method := v.Method(i) method.Call([]reflect.Value{ reflect.ValueOf("Aiwin"), }) } } func (User) Call(name string) { fmt.Println("我的名字是", name) } func main() { user := User{UserName: "Aiwin", Password: "123456"} demo(user) } 反射转ord小案例： package main import ( "errors" "fmt" "reflect" "strings" ) type Users struct { Name string `orm:"name"` Id int `orm:"id"` } func Find(obj any, query ...any) (sql string, err error) { t := reflect.TypeOf(obj) if t.Kind() != reflect.Struct { err = errors.New("非结构体") return } //获取到where if len(query) > 0 { // 有第二个参数，校验第二个参数中的？个数，是不是和后面的个数一样 q := query[0] //取第一个参数 if strings.Count(q.(string), "?")+1 != len(query) { err = errors.New("参数个数不对") return } var where string for _, a := range query[1:] { at := reflect.TypeOf(a) switch at.Kind() { case reflect.Int: q = strings.Replace(q.(string), "?", fmt.Sprintf("%d", a.(int)), 1)//将?替换成数值 case reflect.String: q = strings.Replace(q.(string), "?", fmt.Sprintf("'%s'", a.(string)), 1) } } where += "where " + q.(string) //获取到字段 var columns []string for i := 0; i < t.NumField(); i++ { field := t.Field(i) f := field.Tag.Get("orm") columns = append(columns, f) } //获取表名称 table := strings.ToLower(t.Name()) sql = fmt.Sprintf("select %s from %s %s", strings.Join(columns, ","), table, where) } return } func main() { sql, err := Find(Users{}, "name= ? and id = ?", "Aiwin", 1) //select name,id from Users where name='Aiwin' and id=1 fmt.Println(sql, err) sql, err = Find(Users{}, "id = ?", 1) //select name,id from Users where id=1 fmt.Println(sql, err) } TCP网络编程 主要通过net依赖包来完成，比如以下方法： net.Dial(network, address string) (Conn, error)：通过指定的网络协议和地址连接到远程主机，返回一个Conn接口类型的实例和可能的错误。 net.Listen(network, address string) (Listener, error)：在指定的网络协议和地址上监听连接，返回一个Listener接口类型的实例和可能的错误。 net.DialTimeout(network, address string, timeout time.Duration) (Conn, error)：在指定的超时时间内，通过指定的网络协议和地址连接到远程主机，返回一个Conn接口类型的实例和可能的错误。 net.ListenPacket(network, address string) (PacketConn, error)：在指定的网络协议和地址上监听数据包的到达，返回一个PacketConn接口类型的实例和可能的错误。 net.ResolveTCPAddr(network, address string) (*TCPAddr, error)：将字符串形式的TCP地址解析为TCPAddr类型的实例，包括IP地址和端口号等信息。 net.ResolveUDPAddr(network, address string) (*UDPAddr, error)：将字符串形式的UDP地址解析为UDPAddr类型的实例，包括IP地址和端口号等信息。 net.LookupHost(host string) ([]string, error)：通过主机名查询对应的IP地址列表，并返回一个字符串切片和可能的错误。 net.LookupPort(network, service string) (port int, err error)：通过网络协议和服务名查询对应的端口号，并返回端口号和可能的错误。 demo： package main import ( "fmt" "io" "net" ) func main() { tcp, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:81") //将一个字符串形式的 TCP 地址解析为一个 TCPAddr 类型的实例 listen, _ := net.ListenTCP("tcp", tcp) //进行监听 for { //接收连接 fmt.Println("开始监听....") con, err := listen.Accept() if err != nil { break } fmt.Println(con.RemoteAddr().String() + "进来了") for { var buf []byte = make([]byte, 1024) n, err := con.Read(buf) //客户端退出 if err == io.EOF { fmt.Println(con.RemoteAddr().String() + "退出了") break } fmt.Println(string(buf[0:n])) } } } package main import ( "fmt" "net" ) func main() { conn, _ := net.Dial("tcp", "127.0.0.1:81") var s string for { fmt.Scanln(&s) if s == "quit" { break } conn.Write([]byte(s)) } conn.Close() } Http http.HandleFunc(pattern string, handler func(ResponseWriter, *Request))：注册一个处理函数，用于指定URL模式的请求处理。 http.Handle(pattern string, handler http.Handler)：注册一个处理器对象，该对象实现了http.Handler接口，用于指定URL模式的请求处理。 http.ListenAndServe(addr string, handler http.Handler)：启动一个HTTP服务器，监听指定地址，并使用指定的处理器对象处理接收到的请求。 http.Get(url string) (resp *http.Response, err error)：向指定URL发起GET请求，并返回响应结果。 http.Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error)：向指定URL发起POST请求，携带指定类型的请求体，并返回响应结果。 http.NewRequest(method, url string, body io.Reader) (*Request, error)：创建一个自定义的HTTP请求对象，可以指定请求方法、URL和请求体。 http.DefaultServeMux：默认的ServeMux多路复用器，可以通过它来注册处理函数，用于处理HTTP请求。 http.HandlerFunc：将一个函数转换为http.Handler接口的实现，用于处理HTTP请求。 http.Error(w ResponseWriter, error string, code int)：向客户端发送指定状态码的错误响应。 http.Redirect(w ResponseWriter, r *Request, url string, code int)：向客户端发送重定向指令，使其跳转到指定的URL。 demo package main import ( "crypto/md5" "fmt" "net/http" ) func HashUsingMD5(input string) string { hasher := md5.New() hasher.Write([]byte(input)) return fmt.Sprintf("%x", hasher.Sum(nil)) } func LoginHandler(res http.ResponseWriter, req *http.Request) { if req.Method == "POST" { username := req.FormValue("username") password := req.FormValue("password") if username == "admin" && password == "123456" { Cookie := HashUsingMD5(username + password) cookie := http.Cookie{ Name: "Value", Value: Cookie, } http.SetCookie(res, &cookie) http.Redirect(res, req, "/success", http.StatusFound) } else { http.Redirect(res, req, "/", http.StatusFound) } } else { res.WriteHeader(405) res.Write([]byte("Method Not Allow")) } } func SuccessHandler(res http.ResponseWriter, req *http.Request) { cookie, err := req.Cookie("Value") if err != nil { http.Redirect(res, req, "/", http.StatusFound) return } cookie_value := HashUsingMD5("admin123456") if cookie.Value != cookie_value { http.Redirect(res, req, "/", http.StatusFound) } else { res.Write([]byte("登录成功")) } } func main() { //创建一个文件服务器来处理静态文件 fs := http.FileServer(http.Dir("C:\\Users\\25018\\GolandProjects\\Project\\网络编程\\http")) http.Handle("/", http.StripPrefix("/", fs)) http.HandleFunc("/login", LoginHandler) http.HandleFunc("/success", SuccessHandler) fmt.Println("HTTP server running at http://127.0.0.1:7000") err := http.ListenAndServe("127.0.0.1:7000", nil) if err != nil { fmt.Println(err) } } package main import ( "fmt" "net/http" "net/url" ) func main() { client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } res, err := client.PostForm("http://127.0.0.1:7000/login", url.Values{"username": {"admin"}, "password": {"123456"}}) if err != nil { fmt.Println("请求失败") } if res.StatusCode == http.StatusFound { fmt.Println(res.Header) fmt.Println("重定向成功") } } websocket go get github.com/gorilla/websocket websocket是socket连接和http协议的结合体，可以实现网页和服务端的长连接 demo package main import ( "fmt" "github.com/gorilla/websocket" "net/http" ) var UP = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } func handler(res http.ResponseWriter, req *http.Request) { conn, err := UP.Upgrade(res, req, nil) if err != nil { fmt.Println(err) return } for { //消息类型,消息,错误 types, message, err := conn.ReadMessage() if err != nil { break } conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("你说的是:%s吗？", string(message)))) fmt.Println(types, string(message)) } defer conn.Close() fmt.Println("服务关闭") } func main() { http.HandleFunc("/", handler) http.ListenAndServe("127.0.0.1:7000", nil) } package main import ( "bufio" "fmt" "github.com/gorilla/websocket" "os" ) func send(conn *websocket.Conn) { reader := bufio.NewReader(os.Stdin) l, _, _ := reader.ReadLine() conn.WriteMessage(websocket.TextMessage, l) } func main() { dl := websocket.Dialer{} conn, _, err := dl.Dial("ws://127.0.0.1:7000", nil) if err != nil { fmt.Println(err) return } for { go send(conn) t, p, err := conn.ReadMessage() if err != nil { break } fmt.Println(t, string(p)) //1代表TextMessage } } 爬虫 正则表达式 package main import ( "fmt" _ "github.com/go-sql-driver/mysql" "io" "net/http" "os" "regexp" "strconv" "strings" "time" "xorm.io/xorm" ) type Page struct { Id int64 `xorm:"pk autoincr"` //Id自增 Title string Content string `xorm:"text"` CreateTime time.Time `xorm:"created"` UpdatedTime time.Time `xorm:"updated"` } var engine *xorm.Engine func init() { dbType := "mysql" dbHost := "localhost" dbPort := "3306" dbUser := "root" dbPassword := "root" dbName := "test_xorm" // 创建引擎 var err error engine, err = xorm.NewEngine(dbType, dbUser+":"+dbPassword+"@tcp("+dbHost+":"+dbPort+")/"+dbName) //连接mysql数据库 //engine, err = xorm.NewEngine("mysql", "root:root@/test_xorm?charset=utf-8") //连接mysql数据库 if err != nil { fmt.Println(err) panic("初始化不成功") } else { err2 := engine.Ping() if err2 != nil { panic("连接不成功") } else { fmt.Println("连接成功!") } } } func fetch(url string) string { client := &http.Client{} req, _ := http.NewRequest("GET", url, nil) req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36") req.Header.Add("Cookie", "_gid=GA1.2.1321863478.1703508772; _ga_YXBYDX14GJ=GS1.1.1703508771.1.1.1703510246.58.0.0; _ga=GA1.2.1396264026.1703508772") resp, err := client.Do(req) if err != nil { fmt.Println("Http error", err) return "" } if resp.StatusCode != 200 { fmt.Println("Http Status Code", resp.StatusCode) return "" } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { fmt.Println("Read error", err) return "" } return string(body) } func parse(html string) { html = strings.Replace(html, "\n", "", -1) re_sidebar := regexp.MustCompile(` (.*?) `) sidebar := re_sidebar.FindString(html) re_link := regexp.MustCompile(`href="(.*?)"`) links := re_link.FindAllString(sidebar, -1) url := "https://gorm.io/zh_CN/docs/" for _, value := range links { fmt.Println(value) href := value[6 : len(value)-1] url := url + href fmt.Println("url\n", url) body := fetch(url) go parse2(body) } } func parse2(body string) { body = strings.Replace(body, "\n", "", -1) re_title := regexp.MustCompile(` (.*?) `) title := re_title.FindString(body) title = title[42 : len(title)-5] fmt.Println("title", title) //save(title, body) saveToDB(title, body) } func save(title string, content string) { err := os.WriteFile("网络编程/Go爬虫/pages/"+title+".html", []byte(content), 0644) if err != nil { panic("保存出现错误") } } func saveToDB(title string, content string) { err := engine.Sync(new(Page)) if err != nil { fmt.Println("Failed to sync database: %v", err) } page := Page{ Title: title, Content: content, } affected, err := engine.Insert(&page) if err != nil { fmt.Println("插入出现错误", err) } fmt.Println("save:" + strconv.FormatInt(affected, 10)) } func main() { url := "https://gorm.io/zh_CN/docs/" html := fetch(url) parse(html) } goquery 通过goquery可以快速的对HTML或XML进行解析，提供简单的API提取一个HTML或XML页面中的节点。 常用元素： Find: 通过CSS选择器查找元素，例如 doc.Find("div.content") 将返回所有class为content的div元素。 Each: 遍历匹配的元素集合，并对每个元素执行指定的函数。 Text: 获取元素的文本内容。 Attr: 获取元素的属性值。 Html: 获取匹配元素的HTML内容。 Parent, Children, Next, Prev: 获取父元素、子元素、相邻的后一个元素、相邻的前一个元素等。 Filter, Not, HasClass, Is: 根据特定条件对元素集合进行过滤和判断。 AddClass, RemoveClass, ToggleClass: 添加、移除、切换元素的类名。 Serialize: 将匹配的表单元素序列化为URL编码的字符串。 Each: 遍历匹配的元素集合，并对每个元素执行指定的函数。 package main import ( "fmt" "github.com/PuerkitoBio/goquery" "net/http" ) func main() { url := "https://gorm.io/zh_CN/docs/" //goquery.NewDocument(url)已过时弃用 res, err := http.Get(url) if err != nil { panic("http请求出现错误") } defer res.Body.Close() doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { panic("goquery解析失败") } doc.Find(".sidebar-link").Each(func(i int, s *goquery.Selection) { href, _ := s.Attr("href") base_url := "https://gorm.io/zh_CN/docs/" detail_url := base_url + href resp, _ := http.Get(detail_url) detail_doc, _ := goquery.NewDocumentFromReader(resp.Body) title := detail_doc.Find(".article-title").Text() content, _ := detail_doc.Find(".article").Html() fmt.Println("title:\n", title) fmt.Println("content\n", content) }) } colly Colly 是一个用于爬取 Web 数据的 Golang 框架，具有以下特点： 简单易用：Colly 提供了一个简洁、直观的 API，易于使用和理解。 高度灵活：Colly 允许你自定义请求头、回调函数、处理方法等，以满足各种爬取需求。 并发支持：Colly 提供了并发请求的支持，可以同时处理多个请求，提高爬取效率。 支持动态网页：Colly 集成了 PhantomJS，可以处理 JavaScript 渲染的动态网页。 内置的选择器引擎：Colly 使用自己的选择器引擎，类似于 CSS 选择器，可以轻松地从 HTML 中提取所需的数据。 自动处理重试和错误：Colly 可以自动处理请求的重试和错误，提供了一种简化错误处理的方法。 支持代理：Colly 允许你使用代理服务器进行请求，以帮助隐藏真实 IP 地址和绕过访问限制。 自定义数据存储：Colly 提供了灵活的机制，允许你自定义数据的存储方式，比如输出到文件、存储到数据库等。 事件驱动：Colly 采用事件驱动的方式，通过注册回调函数处理请求和提取数据。 广泛的社区支持：Colly 作为一个流行的爬虫框架，有着活跃的社区，你可以轻松找到相关的文档、教程和示例代码。 package main import ( "fmt" "github.com/gocolly/colly" ) func main() { c := colly.NewCollector() c.OnHTML(".sidebar-link", func(element *colly.HTMLElement) { href := element.Attr("href") if href != "index.html" { c.Visit(element.Request.AbsoluteURL(href)) } }) c.OnHTML(".article-title", func(element *colly.HTMLElement) { title := element.Text fmt.Println("title:", title) }) c.OnHTML(".article", func(element *colly.HTMLElement) { content, _ := element.DOM.Html() fmt.Println("content:", content) }) c.OnRequest(func(request *colly.Request) { fmt.Println(request.URL.String()) }) url := "https://gorm.io/zh_CN/docs/" c.Visit(url) } 豆瓣250 package main import ( "fmt" "github.com/PuerkitoBio/goquery" _ "github.com/go-sql-driver/mysql" "net/http" "regexp" "strconv" "xorm.io/xorm" ) type MovieData struct { Id int64 `xorm:"pk autoincr"` Title string Year string Score string Director string Actor string `xorm:"text"` Quote string `xorm:"text"` Picture string } var engine *xorm.Engine func init() { dbType := "mysql" dbHost := "localhost" dbPort := "3306" dbUser := "root" dbPassword := "root" dbName := "test_xorm" var err error engine, err = xorm.NewEngine(dbType, dbUser+":"+dbPassword+"@tcp("+dbHost+":"+dbPort+")/"+dbName) if err != nil { fmt.Println(err) panic("初始化失败") } else { err2 := engine.Ping() if err2 != nil { panic("连接不成功") } else { fmt.Println("连接成功!") } } } func main() { for i := 0; i < 10; i++ { fmt.Printf("正常爬取第 %d 页信息\n", i) Spider(strconv.Itoa(i * 25)) } } func Spider(page string) { url := "https://movie.douban.com/top250" + "?start=" + page client := http.Client{} req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") req.Header.Set("User-Agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36") req.Header.Set("Accept-Encoding", "zh-CN,zh;q=0.9") req.Header.Set("Cache-Control", "max-age=0") resp, _ := client.Do(req) doc, _ := goquery.NewDocumentFromReader(resp.Body) //#content > div > div.article > ol > li:nth-child(1) //#content > div > div.article > ol > li:nth-child(2) doc.Find("#content > div > div.article > ol > li").Each(func(i int, selection *goquery.Selection) { title := selection.Find("div > div.info > div.hd > a > span:nth-child(1)").Text() img := selection.Find("div > div.pic > a > img") imgUrl, ok := img.Attr("src") quote := selection.Find("div > div.info > div.bd > p.quote > span").Text() score := selection.Find("div > div.info > div.bd > div > span.rating_num").Text() info := selection.Find("div > div.info > div.bd > p:nth-child(1)").Text() if ok { year, director, actor := InfoHandler(info) saveToDb(title, imgUrl, year, score, quote, director, actor) } }) } func saveToDb(title, imgUrl, year, score, quote, director, actor string) { err := engine.Sync(new(MovieData)) if err != nil { fmt.Println("保存数据出现错误") } moviedata := MovieData{ Title: title, Year: year, Score: score, Director: director, Actor: actor, Quote: quote, Picture: imgUrl, } affected, err := engine.Insert(&moviedata) if err != nil { fmt.Println("插入出现错误!") } fmt.Println("保存:", strconv.FormatInt(affected, 10)) } func InfoHandler(info string) (year, director, actor string) { year_re, _ := regexp.Compile(`(\d+)`) year = string(year_re.Find([]byte(info))) director_re, _ := regexp.Compile(`导演:(.*?)\s*(主演:|$)`) director_result := director_re.FindStringSubmatch(info) if len(director_result) > 1 { director = director_result[1] } actor_re, _ := regexp.Compile(`主演:(.*)`) actor_result := actor_re.FindStringSubmatch(info) if len(actor_result) > 1 { actor = actor_result[1] } return year, director, actor } 爬B站评论 Go语言爬虫还是挺累人，它在解析json数据要使用结构体的形式，这里可以使用json2struct.mervine.net网站来转换成结构体，并提取出评论部分。 package main import ( "fmt" "github.com/goccy/go-json" "io" "log" "net/http" ) type KingRankResp struct { Code int64 `json:"code"` Data struct { Replies []struct { Content struct { Device string `json:"device"` JumpURL struct{} `json:"jump_url"` MaxLine int64 `json:"max_line"` Members []interface{} `json:"members"` Message string `json:"message"` Plat int64 `json:"plat"` } `json:"content"` Count int64 `json:"count"` Folder struct { HasFolded bool `json:"has_folded"` IsFolded bool `json:"is_folded"` Rule string `json:"rule"` } `json:"folder"` Like int64 `json:"like"` Replies []struct { Action int64 `json:"action"` Assist int64 `json:"assist"` Attr int64 `json:"attr"` Content struct { Device string `json:"device"` JumpURL struct{} `json:"jump_url"` MaxLine int64 `json:"max_line"` Message string `json:"message"` Plat int64 `json:"plat"` } `json:"content"` Rcount int64 `json:"rcount"` Replies interface{} `json:"replies"` } `json:"replies"` Type int64 `json:"type"` } `json:"replies"` } `json:"data"` Message string `json:"message"` } func main() { client := &http.Client{} req, err := http.NewRequest("GET", "https://api.bilibili.com/x/v2/reply/wbi/main?oid=338159898&type=1&mode=3&pagination_str={\"offset\":\"\"}&plat=1&seek_rpid=&web_location=1315875&w_rid=1b7a0abc9704055facaafb28b36d864e&wts=1704203337", nil) if err != nil { log.Fatal(err) } req.Header.Set("authority", "api.bilibili.com") req.Header.Set("sec-ch-ua-mobile", "?0") req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36") req.Header.Set("accept", "*/*") req.Header.Set("accept-language", "zh-CN,zh;q=0.9") resp, err := client.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() bodyText, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } var resultList KingRankResp _ = json.Unmarshal(bodyText, &resultList) for _, result := range resultList.Data.Replies { fmt.Println("一级评论：", result.Content.Message) for _, reply := range result.Replies { fmt.Println("二级评论：", reply.Content.Message) } } } Gin Gin框架与flask十分的相似，都是路由、处理函数对应，感觉都十分的轻量化。 响应 启动方式 Gin框架的启动方式库有分为两种： 使用Run()函数启动，实际上也是第二种的一个封装 使用原生的http.ListenAndServe()启动 响应JSON、XML、yaml、html 这里对于响应的文件格式方式，都是几乎一致的，只需要使用context.JSON()、contenxt.XML()等换一个函数的形式返回即可。至于返回的数据，一般又三种方式： 使用struct包装json等形式返回，注意字段开头要大写才能进行json序列化解析 使用map包装json等形式返回 使用gin.H{}直接返回数据 至于HTML，只需要通过LoadHTMLGlob()函数指定存放的HTML文件的目录，随后通过context.HTML()指定返回的文件和数据即可在前端完成渲染。 响应静态文件 响应静态文件一般使用以下方式： 对应文件的路径，没有相对路径，只有基于项目的路径，并且两种静态文件的URL前缀是不可重复的。 StaticFile() StaticFile() 方法用于指定单个文件的静态文件服务。 语法：router.StaticFile("/path", "filepath") StaticFS() StaticFS() 方法用于指定整个文件系统的静态文件服务。 语法：router.StaticFS("/path", http.Dir("rootpath")) 重定向 重定向也是直接使用contenxt.Redirect()函数重定向来地址即可，但是要注意301和302重定向的不同： 301表示永久重定向，强缓存，会将响应的结果缓存起来 302是临时重定向，是不会进行缓存的。 查询参数 静态参数 query()获取URL中的参数，如果没有返回空 GetQuery()获取URL的参数key的值，没有返回false QueryArray()可以拿到多个相同的查询参数 动态参数 动态参数也就是可变的参数，比如说/example/1中的1可通过改变显示不同的查询数据，可通过/:key的形式定义查询的动态参数路径。 表单参数 PostFormArray()可获取多个表单参数，返回数组的形式 PostForm()获取表单参数，获取不到返回空，可以接受form-data和x-www-form-urlencoded两种形式 DefaultForm()假如没有接受到这个参数，可设置默认的值 MultipartForm()接受所有的post参数，包括文件等，返回Map的形式。 原始参数 GetRawData()可以获取原始参数，如果是x-www-form-urlencoded返回key=value的ASCII码值，如果是form-data返回Content-Dispostion的那种形式。 demo: package main import ( "encoding/json" "fmt" "github.com/gin-gonic/gin" "net/http" ) func Index(context *gin.Context) { context.String(200, "Hello World") } func Json(context *gin.Context) { //开头字母一定要大写才能使Json序列化可见 type User struct { Username string Nickname string Password string `json:"-"` //json不进行响应 } userInfo := User{Username: "Aiwin", Nickname: "Lau", Password: "123456"} context.JSON(200, userInfo) //map响应 /*userMap := map[string]string{ "username": "Aiwin", "nickename": "Lau", } context.JSON(200, userMap)*/ //直接响应json //context.JSON(200, gin.H{"username": "aiwin", "nickename": "Lau"}) } // Html 响应HTML页面 func Html(context *gin.Context) { context.HTML(200, "index.html", gin.H{"username": "Aiwin"}) } func redirect(context *gin.Context) { //永久重定向,强缓存,将响应结果缓存起来 context.Redirect(301, "http://www.aiwin.net.cn") //临时重定向,不会进行缓存 context.Redirect(302, "http://www.aiwin.net.cn") } // 获取静态查询参数 func query(context *gin.Context) { fmt.Println(context.GetQuery("user")) fmt.Println(context.GetQueryArray("user")) } // 获取动态路径参数 func param(context *gin.Context) { fmt.Println(context.Param("user_id")) } // form表单 func form(context *gin.Context) { fmt.Println(context.PostForm("name")) fmt.Println(context.PostFormArray("name")) fmt.Println(context.DefaultPostForm("age", "18")) } func raw(context *gin.Context) { body, _ := context.GetRawData() contentType := context.GetHeader("Content-Type") //解析json数据 switch contentType { case "application/json": type User struct { Username string `json:"username"` Password string `json:"password"` } var user User err := json.Unmarshal(body, &user) if err != nil { fmt.Println(err) } fmt.Println(user) } } func main() { Engine := gin.Default() Engine.LoadHTMLGlob("网络编程/gin/templates/*") //GoLang中只有相对项目路径,响应下载文件 Engine.StaticFile("/static/demo", "网络编程/gin/static/demo.jpg") //注意前缀不要重复,否则会报错 Engine.StaticFS("/fsfile", http.Dir("网络编程/gin/static")) //定于路由和对应的处理函数 Engine.GET("/index", Index) Engine.GET("/json", Json) Engine.GET("/html", Html) Engine.GET("/redirect", redirect) Engine.GET("/query", query) Engine.GET("/param/:user_id", param) Engine.POST("/form", form) Engine.POST("/raw", raw) //第一种启动方式 Engine.Run("127.0.0.1:8000") //第二种启动方式,用原生的http服务器,Engine.Run只是原生http的封装 //http.ListenAndServe("127.0.0.1:5000", Engine) } 四大请求方式 RESTful 风格是一种用于设计和构建网络应用程序的软件架构风格。它基于 HTTP 协议，并遵循一些设计原则和约束，旨在提供简单、可扩展、可靠和可轻松集成的 Web 服务。 GET:从服务器取除资源 POST在服务器中新建一个资源 PUT在服务器中更新资源 PATCH在服务器中更新资源 DELETE从服务器中删除资源 简单demo演示： GET 文章列表 GET /article/:id 文章详情 POST 添加文章 PUT /article/:id 更新文章 DELETE 删除文章 package main import ( "encoding/json" "fmt" "github.com/gin-gonic/gin" "strconv" ) type article struct { Title string `json:"title"` Detail string `json:"detail"` } //模拟数据库 var articlelist = []article{ {"西游记", "《九九八十一难》"}, {"水浒传", "《梁山一百零八好汉》"}, {"三国演义", "《三国鼎立传奇故事》"}, } type response struct { Code int `json:"code"` Data any `json:"data"` Msg string `json:"msg"` } func _bindJson(context *gin.Context, obj any) (err error) { body, _ := context.GetRawData() contentType := context.GetHeader("Content-Type") switch contentType { case "application/json": err = json.Unmarshal(body, &obj) if err != nil { fmt.Println(err.Error()) return err } } return nil } func getlist(context *gin.Context) { context.JSON(200, response{Code: 0, Data: articlelist, Msg: "成功"}) } func getDetail(context *gin.Context) { id, _ := strconv.Atoi(context.Param("id")) context.JSON(200, response{Code: 0, Data: articlelist[id], Msg: "成功"}) } func create(context *gin.Context) { var newarticle article err := _bindJson(context, &newarticle) if err != nil { fmt.Println(err) } articlelist = append(articlelist, newarticle) context.JSON(200, response{Code: 0, Data: nil, Msg: "添加成功"}) } func edit(context *gin.Context) { id, _ := strconv.Atoi(context.Param("id")) var editarticle article err := _bindJson(context, &editarticle) if err != nil { fmt.Println(err) } articlelist[id].Title = editarticle.Title articlelist[id].Detail = editarticle.Detail context.JSON(200, response{Code: 0, Data: editarticle, Msg: "修改成功"}) } func delete(context *gin.Context) { id, _ := strconv.Atoi(context.Param("id")) articlelist = append(articlelist[:id], articlelist[id+1:]...) context.JSON(200, response{Code: 0, Data: articlelist, Msg: "删除成功"}) } func main() { router := gin.Default() router.GET("/getlist", getlist) router.GET("/getDetail/:id", getDetail) router.POST("/create", create) router.PUT("/edit/:id", edit) router.DELETE("/delete/:id", delete) router.Run("0.0.0.0:8888") } 请求头相关 通过getHeader()获取请求头，不区分大小写，使用-来进行连接，通过Request.Header可获取全部请求头的map，但是要注意Request.Header如果用Map取值方式是必须大小写一致的(因为是从Map 中获取value的）。 package main import ( "fmt" "github.com/gin-gonic/gin" ) func index(context *gin.Context) { fmt.Println(context.GetHeader("user-agent")) fmt.Println(context.Request.Header.Get("user-agent")) fmt.Println(context.Request.Header["User-Agent"]) context.Header("token", "Hello-World") context.Header("Content-Type", "application/text;charset=utf-8") context.JSON(0, gin.H{"data": "自定义响应头"}) } func main() { router := gin.Default() router.GET("/index", index) router.Run("0.0.0.0:8080") } 绑定参数 gin中可以使用bind快速将前端传递来的数据与结构体进行参数绑定，以及参数校验。 ShouldBind：在绑定过程中如果出现错误，将错误信息返回给调用者，由调用者决定如何处理错误。 MustBind：在绑定过程中如果出现错误，将直接触发panic，使程序进入恐慌状态并终止运行。 package main import "github.com/gin-gonic/gin" // form指示可以接受form表单 // uri表示可以通过uri query查询 type User struct { Name string `json:"name" form:"name" uri:"name"` Age int `json:"age" form:"age" uri:"age"` Sex string `json:"sex" form:"sex" uri:"sex"` } func json(context *gin.Context) { var user User err := context.ShouldBindJSON(&user) if err != nil { context.JSON(200, gin.H{"msg": "出错了"}) return } context.JSON(200, user) } func uri(context *gin.Context) { var user User err := context.ShouldBindUri(&user) if err != nil { context.JSON(200, gin.H{"msg": "出错了"}) return } context.JSON(200, user) } func form(context *gin.Context) { var user User err := context.ShouldBind(&user) if err != nil { context.JSON(200, gin.H{"msg": "出错了"}) return } context.JSON(200, user) } func main() { router := gin.Default() router.GET("/json", json) router.POST("/form", form) router.GET("/uri/:name/:age/:sex", uri) router.Run("0.0.0.0:8888") } 常用验证器 required：字段必需存在且不能为空。例如，required:"true"。 min：字段的值必须大于或等于指定的最小值。可以用于数字，字符串和时间类型的验证。例如，min:"10"。 max：字段的值必须小于或等于指定的最大值。同样适用于数字，字符串和时间类型的验证。例如，max:"100"。 len：字段的长度必须等于指定的值。例如，len:"10"。 email：字段的值必须为有效的电子邮件地址。例如，email:"true"。 url：字段的值必须为有效的URL地址。例如，url:"true"。 regex：字段的值必须符合指定的正则表达式。例如，regex:"^[A-Za-z]+$"。 alpha：字段的值必须全部由字母组成，不允许包含空格、数字或特殊字符。例如，alpha:"true"。 alphanum：字段的值必须全部由字母和数字组成，不允许包含空格或特殊字符。例如，alphanum:"true"。 numeric：字段的值必须为数字型的数据，可以是整数或浮点数。例如，numeric:"true"。 oneof：字段的值必须属于提供的多个值中的一个。例如，oneof:"dog cat bird"。 gte：字段的值必须大于或等于指定的另一个字段的值。例如，gte:"Age"。 lte：字段的值必顶小于或等于指定的另一个字段的值。例如，lte:"EndTime"。 eqfield，nefield等于和不等于其它字段的值 contains 一定要包含的字符串 excludes 不包含的字符串 startswith 字符串前缀 endwith 字符串后缀 ip ipv4 ipv6 uri url 特定的格式 datetime 日期验证，如datetime=2002-03-04 15:00:01等表示日期验证 dive 针对数组 package main import "github.com/gin-gonic/gin" type Member struct { Username string `json:"username" binding:"required,min=5,max=10"` Password string `json:"password" binding:"required" ` Re_password string `json:"re_password" binding:"eqfield=Password"` } func verifyJson(context *gin.Context) { var member Member err := context.ShouldBindJSON(&member) if err != nil { context.JSON(200, gin.H{"msg": err.Error()}) return } context.JSON(200, member) } func main() { router := gin.Default() router.GET("/verifyJson", verifyJson) router.Run("0.0.0.0:8888") } 自定义错误信息 package main import ( "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "reflect" ) type MemberInfo struct { Username string `json:"username" binding:"required,min=5,max=10" msg:"用户名长度不符合"` Password string `json:"password" binding:"required" msg:"密码不能为空"` Re_password string `json:"re_password" binding:"eqfield=Password" msg:"两次密码必须一致"` } func GetValidMsg(err error, obj any) string { //obj是结构体指针 getObj := reflect.TypeOf(obj) //将err接口断言为是否为验证器返回的错误 if errs, ok := err.(validator.ValidationErrors); ok { //循环每一个错误信息 for _, e := range errs { //根据报错字段拿到自定义错误信息 if f, exist := getObj.Elem().FieldByName(e.Field()); exist { return f.Tag.Get("msg") } } } return err.Error() } func Mymessage(context *gin.Context) { var member MemberInfo err := context.ShouldBindJSON(&member) if err != nil { context.JSON(200, gin.H{"msg": GetValidMsg(err, &member)}) return } context.JSON(200, member) } func main() { router := gin.Default() router.GET("/verifyJson", Mymessage) router.Run("0.0.0.0:8888") } 自定义验证器 package main import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" "reflect" ) type Student struct { Name string `json:"name" binding:"required,sign" msg:"用户名校验失败"` Age int `json:"age" binding:"required"` } func _GetValidMsg(err error, obj any) string { getObj := reflect.TypeOf(obj) if errs, ok := err.(validator.ValidationErrors); ok { for _, e := range errs { if f, exist := getObj.Elem().FieldByName(e.Field()); exist { return f.Tag.Get("msg") } } } return err.Error() } func verify(context *gin.Context) { var student Student err := context.ShouldBindJSON(&student) if err != nil { context.JSON(200, gin.H{"msg": _GetValidMsg(err, &student)}) return } context.JSON(200, student) } func signValid(fl validator.FieldLevel) bool { var namelist = []string{"admin"} for _, namestr := range namelist { name := fl.Field().Interface().(string) if name == namestr { return false } } return true } func main() { Engine := gin.Default() if v, ok := binding.Validator.Engine().(*validator.Validate); ok { //自定义验证器 v.RegisterValidation("sign", signValid) } Engine.POST("/verify", verify) Engine.Run("0.0.0.0:8000") } 文件上传 saveUploadFile()直接保存文件到服务器 ，本质上还是Create+Copy Ceate+Copy 可以直接通过Create创建文件，通过Copy保存Open()出来的内容 package main import ( "fmt" "github.com/gin-gonic/gin" "io" "os" ) func upload(context *gin.Context) { files, err := context.FormFile("file") if err != nil { context.String(500, "上传失败") return } fileInterface, _ := files.Open() //通过SaveUploadedFile()进行处理, 本质上还是Create+Copy //err = context.SaveUploadedFile(files, "网络编程/文件上传/uploads/"+files.Filename) //if err != nil { // context.JSON(500, "上传失败") // return //} fmt.Println(files.Filename) result, _ := os.Create("网络编程/文件上传/uploads/" + files.Filename) defer result.Close() io.Copy(result, fileInterface) context.String(200, "上传成功") } func uploads(context *gin.Context) { form, err := context.MultipartForm() if err != nil { context.String(500, "上传错误") return } files := form.File["file[]"] for _, file := range files { context.SaveUploadedFile(file, "网络编程/文件上传/uploads/"+file.Filename) } context.String(200, "上传成功") } func main() { Engine := gin.Default() Engine.POST("/upload", upload) Engine.POST("/uploads", uploads) Engine.Run("0.0.0.0:8000") } 文件下载 通过Header()设置文件的方式，唤起浏览器进行下载。 func download(context *gin.Context) { context.Header("Content-Type", "application/octet-stream") context.Header("Content-Disposition", "attachment; filename=download.jpg") context.File("网络编程/文件上传/uploads/5.jpg") } 中间件 Gin框架允许开发者在处理请求的过程中，假如自己的函数也就是中间件，一般在函数处理的前面，可用于登录认证，权限校验，数据分页等等。 在定义路由的时候，其实可以定义多个函数按顺序进行处理，路由对应的处理函数其实也是一个中间件，但是一般处理路由不会进行验证拦截，而我们可以在真正的处理函数前面定义其它函数并进行一些Abort()验证拦截操作，当然也可以通过Next()指定下一个要走的中间件，如果后面没有中间件，则会走回去第一个中间件。一般用的比较多的是全局中间件。 这里记录下gin.Default()和gin.New()的配置: gin.Default() 方法会返回一个默认的 Gin 引擎实例，并且会自动注入 Logger 和 Recovery 中间件，同时也会使用默认的配置，包括日志输出到控制台等。 gin.New() 方法则会返回一个裸露的 Gin 引擎实例，不会自动注入任何中间件或默认配置，需要手动进行配置。 package main import "github.com/gin-gonic/gin" func middleware(context *gin.Context) { username, ok := context.GetQuery("username") if ok { if username == "admin" { context.Set("name", "admin") //中间件传递值 context.Next() } else { context.JSON(200, gin.H{"msg": "你不是admin"}) context.Abort() } } } func main() { router := gin.Default() router.Use(middleware) router.GET("/login", func(context *gin.Context) { name, _ := context.Get("name") context.JSON(200, gin.H{"msg": "欢迎" + name.(string) + "用户"}) }) router.Run("0.0.0.0:8000") } 路由分组 就是给路由匹配一些前缀路由，避免重复书写，比较方便，也比较美观。 package main import "github.com/gin-gonic/gin" type User struct { Name string `json:"name"` Age int `json:"age"` } type Article struct { Title string `json:"title"` Content string `json:"content"` } type Response struct { Code int `json:"code"` Data any `json:"data"` Msg string `json:"msg"` } func Usermiddleware(ctx *gin.Context) { token := ctx.GetHeader("token") if token == "admin" { ctx.Next() } else { ctx.JSON(403, gin.H{"msg": "未通过验证"}) ctx.Abort() } } func UserListView(ctx *gin.Context) { var userList = []User{ {"Aiwin", 20}, {"zhangsan", 21}, {"lisi", 22}, } ctx.JSON(200, Response{0, userList, "请求成功"}) } func getArticleList(ctx *gin.Context) { var articleList = []Article{ {"《变形记》", "讲变形的故事"}, {"《西游记》", "讲取经的故事"}, {"《小王子》", "讲王子的故事"}, } ctx.JSON(200, Response{0, articleList, "请求成功"}) } func main() { router := gin.Default() api := router.Group("api") //定义路由分组 api.GET("/getUserList", UserListView).Use(Usermiddleware) // /api/getUserList article := api.Group("article") //双重路由 article.GET("/getArticleList", getArticleList) ///api/article/getUserList router.Run("0.0.0.0:8000") } 日志 记录用户操作，猜测用户行为 记录bug 通过gin.DefaultWriter()来指定一个os.Create()的file，能够利用内置的日志器。 gin.DebugPrintRouteFunc()用于打印当前注册的所有路由信息，可以使用Route()函数打印所有的路由更快。 如果不想看debug信息，可以通过gin.SetMode()切换环境 package main import ( "fmt" "github.com/gin-gonic/gin" "io" "os" ) func LogFormatterParams(params gin.LogFormatterParams) string { return fmt.Sprintf( "[Aiwin] %s | %s%d%s | %s %s %s %s \n", params.TimeStamp.Format("2024-01-16 15:00:00"), params.StatusCodeColor(), params.StatusCode, params.ResetColor(), params.MethodColor(), params.Method, params.ResetColor(), params.Path) } func main() { gin.SetMode(gin.ReleaseMode) file, _ := os.Create("log.txt") gin.DefaultWriter = io.MultiWriter(file, os.Stdout) router := gin.Default() //自定义打印日志的格式 //router := gin.New() //router.Use(gin.LoggerWithFormatter(LogFormatterParams); //router.Use(gin.LoggerWithConfig(gin.LoggerConfig{Formatter: LogFormatterParams})) for _, tree := range router.Routes() { fmt.Println(tree.Method, tree.Path, tree.HandlerFunc) } router.GET("/index", func(context *gin.Context) {}) router.Run("0.0.0.0:8000") } logrus go get github.com/sirupsen/logrus 一些常用的命令，少于Info等级就不会打印出来，可以通过logrus.SetLevel()更改等级： logrus.Error()错误信息 logrus.Warnln()警告信息 logrus.Infof()普通信息 logrus.Debug()调试信息 logrus.Println()打印信息 logrus.SetFormatter()一些字段配置 名称 作用 ForceColors 是否强制使用颜色输出 DisableColors 是否禁用颜色输出 ForceQuote 是否强制引用所有值 DisableQuote 是否禁止引用所有值 DisableTimestamp 是否禁用时间戳记录 FullTimestamp 是否连接到TTY时输出完整的时间戳 TimestampFormat 用于输出完整时间戳的时间戳格式 一些颜色的输出如下： fmt.Println("\033[30m 黑色 \033[0m") fmt.Println("\033[31m 红色 \033[0m") fmt.Println("\033[32m 绿色 \033[0m") fmt.Println("\033[33m 黄色 \033[0m") fmt.Println("\033[34m 蓝色 \033[0m") fmt.Println("\033[35m 紫色 \033[0m") fmt.Println("\033[36m 青色 \033[0m") fmt.Println("\033[37m 灰色 \033[0m") // 背景色 fmt.Println("\033[40m 黑色 \033[0m") fmt.Println("\033[41m 红色 \033[0m") fmt.Println("\033[42m 绿色 \033[0m") fmt.Println("\033[43m 黄色 \033[0m") fmt.Println("\033[44m 蓝色 \033[0m") fmt.Println("\033[45m 紫色 \033[0m") fmt.Println("\033[46m 青色 \033[0m") fmt.Println("\033[47m 灰色 \033[0m") 自定义logrus输出格式: package main import ( "bytes" "fmt" "github.com/sirupsen/logrus" "os" "path" ) const ( red = 31 yellow = 33 blue = 36 gray = 37 ) type LogFormatter struct{} func (t *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) { var levelColor int switch entry.Level { case logrus.DebugLevel, logrus.TraceLevel: levelColor = gray case logrus.WarnLevel: levelColor = yellow case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel: levelColor = red default: levelColor = blue } var b *bytes.Buffer if entry.Buffer != nil { b = entry.Buffer } else { b = &bytes.Buffer{} } timestamp := entry.Time.Format("2006-01-02 15:04:05") if entry.HasCaller() { //自定义文件路径 funcVal := entry.Caller.Function fileVal := fmt.Sprintf("%s:%d", path.Base(entry.Caller.File), entry.Caller.Line) //自定义输出格式 fmt.Fprintf(b, "[%s] \x1b[%dm[%s]\x1b[0m %s %s %s\n", timestamp, levelColor, entry.Level, fileVal, funcVal, entry.Message) } else { fmt.Fprintf(b, "[%s] \x1b[%dm[%s]\x1b[0m %s\n", timestamp, levelColor, entry.Level, entry.Message) } return b.Bytes(), nil } var log *logrus.Logger func init() { log = NewLog() } func NewLog() *logrus.Logger { mLog := logrus.New() //新建一个实例 mLog.SetOutput(os.Stdout) //设置输出类型 mLog.SetReportCaller(true) //开启返回函数名和行号 mLog.SetFormatter(&LogFormatter{}) //设置自己定义的Formatter mLog.SetLevel(logrus.DebugLevel) //设置最低的Level return mLog } func main() { log.Errorln("你好") log.Infof("你好") log.Warnln("你好") log.Debugf("你好") }
一、什么是Node.js? 基于Chrome V8引擎的Javascript的运行环境，V8相对于火狐的OdinMonkey，Safri浏览器的JSCore都要快。 Node.js是Javascript的后端运行环境，无法调用DOM和BOM等浏览器内置API，仅仅提供了基础的功能和API，这些基础使很多强大的框架出现，可以基于Express框架构建Web应用，基于restify快速构建API项目，可以读写和操作数据库等。 二、fs文件系统模块 fs模块是Node.js官方提供的用来操作文件的模块，提供一系列的方法和属性，用来满足用户对文件的操作，有如下的一些常用命令。 命令 说明 open(path，mode，callback) 异步模式下打开文件 stat(path，callback) 异步模式下获取文件信息 writeFile(file，data，callback) 异步模式下写入文件 read(fd，buffer，offset，length，position，callback) 通过文件描述符读取文件 close(fd，callback) 通过文件描述符读取文件 ftruncate(fd，len，callback) 通过文件描述符读取文件 fs.unlink(path, callback) 删除文件 fs.mkdir(path[, options], callback) 创建目录 fs.readdir(path, callback) 读取目录 fs.rmdir(path, callback) 删除目录 fs.exists(path, callback) 文件是否存在 例子： const fs=require('fs') const buf = new Buffer.alloc(1024); fs.mkdir('../file/',function (err) { if(err){ console.log(err) } console.log("目录创建成功") }) fs.writeFile('../file/input.txt','通过writeFile写入文件内容',function (err) { if(err){ return console.log(err) } console.log('文件写入成功') }) fs.readdir("../file/",function (err,files) { if(err){ return console.error(err) } files.forEach(function (file) { console.log("目录读取开始") console.log(file) }) }) fs.open('../file/input.txt','r+',function (err,fd) { if(err){ return console.error(err) } console.log("开始文件读取") fs.read(fd,buf,0,buf.length,0,function (err,bytes) { if(err){ console.log(err) } if(bytes>0){ console.log(buf.subarray(0,bytes).toString()) } console.log(bytes+"字节被读取") }) fs.close(fd,function (err) { if(err){ console.log(err) } console.log("文件读取关闭") }) }) fs.unlink("../file/input.txt",function (err) { if(err){ return console.error(err) } console.log("文件删除成功") }) setTimeout(function() { fs.rmdir("../file/", function (err) { if (err) { return console.log(err) } console.log("目录删除成功") }) },3000) path路径模块 Node在读取或者写入文件，使用相对路径的时候，会以Node使用的目录为起点，直接拼接相对目录的字符串，可以使用绝对路径或者path模块解决。 命令 说明 sep 平台路径分割符 join() 拼接路径 basename() 最后的文件名 extname() 文件扩展名 isAbsolute() 判断是否为绝对路径 relative 转为相对路径 normalize 规范化路径 const path=require('path') console.log(path.sep) //路径分割符 const pathStr=path.join("/tmp","/Node","fs","../","path") console.log(pathStr) const pathStr1=path.join(__dirname,'../files/input.txt') console.log(pathStr1) console.log(path.basename("../files/input.txt")) //获取最后的文件名 console.log(path.extname("../files/input.txt")) //文件扩展名 console.log(path.isAbsolute("/tmp/node/path")) //判断是否是绝对路径 console.log(path.relative("/tmp/node/path","/tmp/node/http/")) //转换成相对路径 console.log(path.normalize('/test/test1//2slashes/1slash/tab/../')); //规范化路径 烟花分割案例 在index.html页面中，CSS和Javascript都写入了里面，没有分成index.css，index.js文件，使用fs和path对index.html分割成这几个文件。 原index.html: 烟花动画特效 const fs=require('fs') const path=require('path') const regStyle=/ ","") fs.writeFile(path.join(__dirname,"static/style.css"),newCss,function (err) { if(err) { return console.log("CSS文件写入失败"+err.message) } }) } function resoleJs(htmlStr){ const r=regJs.exec(htmlStr) const newJs=r[0].replace(" ","") fs.writeFile(path.join(__dirname,"static/index.js"),newJs,function (err) { if(err) { return console.log("JS文件写入失败"+err.message) } }) } function resoleHtml(htmlStr) { const r=regStyle.exec(htmlStr) const r1=regJs.exec(htmlStr) const pathStyle="./style.css" const pathStyle1="./index.js" const newHtml=htmlStr.toString().replace(r[0],' ').replace(r1[0],' ') fs.writeFile(path.join(__dirname,"static/index.html"),newHtml,function (err) { if(err){ return console.log("文件写入失败"+err.message) } }) } 三、Http模块 http模块是Node.js官方提供的，用来创建web服务器的模块，通过http模块提供的createServer()方法，将电脑变成Web服务器，对外提供Web资源服务。 烟花案例 const http=require('http') const fs=require('fs') const url = require("url") const path=require('path') server=http.createServer() server.on('request' ,function (req,res) { const url = req.url let pathname='' if(url==='/' || url==='/index.html'){ pathname=path.join(__dirname,'./static/index.html') } else{ pathname=path.join(__dirname,'./static',url) } fs.readFile(pathname,'utf-8',function (err,data) { if(err){ res.write("出现未知错误"+err.message) } res.end(data) }) }) server.listen(8080,function () { console.log("服务器运行在http://127.0.0.1:8080") }) Get请求 const http = require('http'); const url = require('url'); http.createServer(function(req, res){ res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'}); const params = url.parse(req.url, true).query; if(params.username==="admin"&&params.password==="admin123"){ res.write(" 登录成功 ") } res.end(); }).listen(8080); post请求 const http=require("http") const querystring=require("querystring") let postHTML=' Node.js 例子 ' + ' ' + ' ' + '用户名： ' + '密码： ' + ' ' + ' ' + ' '; server=http.createServer(function (req,res) { let body=""; req.on('data',function (chunk) { body+=chunk }) req.on('end',function () { body=querystring.parse(body) res.writeHead(200, {'Content-Type': 'text/html; charset=utf8'}); if(body.username==="admin"&&body.password==="admin123"){ res.write(" 登录成功 ") } else{ res.write(postHTML) } res.end() }) }).listen(8080) 四、模块化 模块化在编程中指的是遵守固定规则，把一个大文件拆成相互依赖的多个小模块，从而提高代码的复用性，可维护性，按需加载等。 模块化规范：对代码进行模块化拆分和组合时，要遵守的规则，从而方便了各大模块之间的相互调用。 Node.js的模块分类：内置模块(官方提供），自定义模块(用户自己创建)，第三方模块(第三方开发，使用前下载） 模块作用域：为了防止全局变量污染，自定义模块中定义的变量和方法等成员，只能在当前模块被访问。 向外共享模块作用域成员：每个.js自定义模块中都有module对象，存储了和当前模块的有关信息，使得模块内成员可以通过export被模块外访问到。 向外共享成员例子： //向module.exports对象上挂载username属性或方法 module.exports.username='Aiwin' module.exports.say=function () { console.log("您好,exports向外共享") } function NoInvoke() { console.log("这里不能暴露出去") } const m=require("./test") console.log(m) 使用require()方法导入模块时，导入的结果永远以module.exports指向的新对象为准，即exports对象可以被重覆盖。 //向module.exports对象上挂载username属性或方法 module.exports.username='Aiwin' module.exports.say=function () { console.log("您好,exports向外共享") } //require指向永远以新的对象为准 module.exports={ name:'Aiwin', say(){ console.log("Hello") } } function NoInvoke() { console.log("这里不能暴露出去") } 为了简化向外共享成员的代码，Node提供了exports对象，默认情况下，module.exports和exports指向同一对象，最终共享的结果以module.exports指向对象为准。 npm和包：NodeJs中的包就是由第三方个人或团队开发出来的模块，免费供所有人使用，包基于内置模块封装出来，提供了更高级，更方便的API，极大的提高了开发效率。 全球最大的包共享平台 npm初体验 传统初始化时间 function dataFormat(timeStr){ const data=new Date(timeStr) const y=data.getFullYear() const m=padZero(data.getMonth()) const d=padZero(data.getDate()) const hh=padZero(data.getHours()) const mm=padZero(data.getMinutes()) const ss=padZero(data.getSeconds()) return `${y}-${m}-${d} ${hh}:${mm}:${ss}` } function padZero(n) { return n>9?n:'0'+n } module.exports={ dataFormat } const time=require("./dataFormat") const t=new Date() console.log(t) console.log(time.dataFormat(t)) npm的一些使用： 命令 说明 npm install 包完整名称@版本号 安装npm包 npm i 包名称@版本号 安装npm包 npm init -y 创建package.json文件 npm config get registry 查看镜像源 npm config set registry=https://registry.npm.taobao.org/ 安装淘宝镜像源 npm i nrm -g 下载nrm npm uninstall 包名 -g 卸载全局安装的包 nrm ls 查看可下载镜像源 nrm use 名称 更换镜像源 npm uninstall 卸载包 npm i 包名 -D 将包记录到devDependencies节点 npm i / npm install 自动从dependencies中识别包下载 devDependencies中的包是指项目只在开发阶段用到的包，项目上线后遇不到，dependencies中有项目安装的npm包。 使用npm const moment=require('moment') const time=moment().format('YYYY-MM-DD HH:mm:ss') console.log(time) 规范的包结构，应包含以下3点要求： （1）包必须以单独的目录存在。 （2）包的顶级目录必须包含package.json管理配置文件。 （3）package.json必须包含name、version、main这三个属性，代表包名、版本、入口。 五、开发属于自己的包 功能要求：1.转义HTML中的特殊字符 2.还原转义字符 步骤如下：1. 创建新目录作为包的根目录 2.在根目录下创建入口js，package.json，README.MD 3.编写package.json和代码 注意包名必须唯一，创建包名前须查看是否存在相同包名 创建package.json package.json的一些基本信息，包名，版本号，入口，关键词，协议 编写index.js代码 function htmlEscape(html) { return html.replace(/<|>|"|&/g,match=>{ switch (match) { case "<": return "<" case ">": return ">" case '"': return """ case "&": return "&" } }) } function htmlUnEscape(html) { return html.replace(/<|>|"|&/g,match=>{ switch (match) { case "<": return "<" case ">": return ">" case '"': return '"' case "&": return "&" } }) } module.exports={ htmlEscape, htmlUnEscape } 测试： const {htmlEscape, htmlUnEscape} = require("../Aiwin-tools"); const htmlStr=' 这是一次测试Test ' const str=htmlEscape(htmlStr) console.log(str) console.log(htmlUnEscape(str)) 编写README.MD文档 将包用法说清楚即可，没有特别的规范 将包发布到npm 前往npm官网注册一个账号 npm login登录npm账号,注意登录服务器必须为官网的服务器 npm publish 发布包 npm unpublish 包名 --force 删除发布72小时内发布的包 模块加载机制 模块加载机制，优先从缓存中加载，即多次调用require()不会导致模块代码被执行多次。 内置模块加载机制，内置模块的加载优先级最高，如require('fs')始终返回内置的fs模块，即可node_modules目录下有相同的包叫fs。 自定义模块加载机制，使用require()加载自定义模块，必须指定./或../开头的路径标识符，如果省略文件名，则会按照顺序为确切文件名，补.js，.json，.node扩展名加载。 第三方模块加载机制，如果传递给require()的模块标识符不是内置模块，也没有./或者../开头，则会从当前目录向上寻找node_modules文件夹加载第三方模块。 目录作为模块标识符，会在被加载目录下寻找package.json文件，寻找main属性作为加载入口，若没有，则加载目录下index.js文件，若两者都没有，则报错。 六、Express 1.初识Express Express基于Node.js平台，快读、开放、极简的Web开发框架，通俗的说就是http模块的加强版，是基于http模块进一步封装出来，极大提高开发效率，专门用于创建Web服务器。 安装express安装到项目中，直接npm install下载即可。 创建基本Web服务器 const express = require('express'); const app = express(); app.get('/', function (req, res) { res.send('Hello express'); }) const server = app.listen(8081, function () { const host = server.address().address; const port = server.address().port; console.log("express server running at https://%s:%s", host, port) }); GET/POST const express = require('express'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.urlencoded({ extends: false })) app.use('./public',express.static('public')) app.get('/', function (req, res) { res.sendFile( __dirname + "/public/" + "index.html" ); }) app.post('/post',function (req,res) { let response={ "username":req.body.username, "password":req.body.password } res.end(JSON.stringify(response)) }) //动态参数 app.get('/user/:username/:password',function (req,res) { res.send(req.params) }) const server = app.listen(8081, function () { const {address,port} = server.address(); console.log("express server running at https://%s:%s", address, port) }); Express Username: Password: express.static()创建静态资源服务器，如果有多个静态资源目录，会按添加顺序查找所需文件，注意post请求中的body要解析才能提取参数。 nodemon 用于在编写调试Node.js项目时，修改了项目代码，工具会自动监听项目文件变动，当代码被修改后，自动重启项目，方便开发和调试，类似于springboot的devtools。 安装，npm install -g nodemon 使用nodeman，将启动项目的node命令替换为nodeman命令即可。 2.路由模块化 为了方便路由进行模块化管理，Express不建议将路由直接挂载到app，推荐将路由抽离为单独模块，步骤如下： 创建路由模块对应的js文件 调用express.Router()函数创建路由对象 向路由对象上挂载具体路由 使用module.exports向外共享路由对象 使用app.use()函数注册路由模块 router.js: const express=require('express') const router=express.Router() router.get('/get',function (req,res) { res.send('这是Get请求') }) router.post('/post',function (req,res) { res.send('这是post请求') }) module.exports=router express.js: const router=require('./router') const express=require('express') const app=express() app.use(router) const server = app.listen(8081, function () { const {address,port} = server.address(); console.log("express server running at https://%s:%s", address, port) }); 3.中间件 当一个请求到达Express的服务器之后，可以连续调用多个中间件，从而对这次请求进行预处理。 Express中间件，本质上就是一个function处理函数，格式如下function(req,res,next)，必须包含next参数，next表示把流转关系转交给下一个中间件或路由。 全局中间件：到达服务器后，都会触发的中间件。 例子：登录拦截器 const express=require('express') const app=express() const bodyParser = require('body-parser'); const cookieParser = require("cookie-parser"); app.use(bodyParser.urlencoded({ extends: false })) app.use(express.static('public')) app.use(cookieParser()) const middleware=function (req,res,next) { let url=req.url const cookie=req.cookies if(url!=='/login'&&cookie.name!=='Aiwin'){ res.location('/') res.setHeader('Location','/') res.send(301) } next() } app.use(middleware) app.get('/',function (req,res) { res.sendFile(__dirname+"index.html") }) app.post('/login',function(req,res){ const username=req.body.username const password=req.body.password if(username==='admin'&&password==='admin'){ res.cookie("name","Aiwin",{maxAge:3600}) res.setHeader('Location','/admin') res.location('/admin') res.send(301) } }) app.get('/admin',function (req,res) { res.sendFile(__dirname+"/public/admin.html") }) const server = app.listen(8082, function () { const {address,port} = server.address(); console.log("express server running at https://%s:%s", address, port) }); admin.html: Express Username: Password: admin.html: Admin 这是登录后的页面 所有的请求都会经过中间件，在没有登录成功，获取cookie的时候，无法访问/admin页面 局部中间件，不使用app.use()定义中间件，使用app.METHOD(PATH，中间件，function)定义局部中间件，只对该路由生效。 const express=require('express') const app=express() const mw=function (req,res,next) { req.a='局部中间件生效' next() } app.get('/',mw,function (req,res) { res.send(req.a) }) app.get('/other',function (req,res) { res.send('局部中间件不生效') }) const server = app.listen(8081, function () { const {address,port} = server.address(); console.log("express server running at https://%s:%s", address, port) }); 中间件注意事项 在路由之前注册中间件(错误级别除外)。 中间件一定要执行next()函数。 连续调用多个中间件，多个中间件共享req，res对象。 中间件分类 应用级别中间件，通过app实例上的中间件。 路由级别中间件，绑定到Router()上的中间件。 错误级别中间件，捕获整个项目发生的异常作物，防止项目崩溃。 const express = require('express') const app = express() // 注意：除了错误级别的中间件，其他的中间件，必须在路由之前进行配置 // 通过 express.json() 这个中间件，解析表单中的 JSON 格式的数据 app.use(express.json()) app.use(express.urlencoded({extended:false})) app.post('/', (req, res) => { // 在服务器，可以使用 req.body 这个属性，来接收客户端发送过来的请求体数据 // 默认情况下，如果不配置解析表单数据中间件，则 req.body 默认等于 undefined console.log(req.body) res.send('OK') }) app.post('/url',function (req,res) { console.log(req.body) res.send('OK') }) app.listen(8081) 错误级别中间件注册到路由之后，res.send()没有执行，因为错误中间件中断掉了后面的执行。 内置中间件 中间件 作用 express.static 托管静态资源中间件 express.json 解析JSON格式请求体数据 express.urlencoded 解析URL-encoded格式请求体数据 const express = require('express') const app = express() // 注意：除了错误级别的中间件，其他的中间件，必须在路由之前进行配置 // 通过 express.json() 这个中间件，解析表单中的 JSON 格式的数据 app.use(express.json()) app.post('/', (req, res) => { // 在服务器，可以使用 req.body 这个属性，来接收客户端发送过来的请求体数据 // 默认情况下，如果不配置解析表单数据中间件，则 req.body 默认等于 undefined console.log(req.body) res.send('OK') }) app.listen(8081) 第三方中间件，非官方内置，由别人开发，可以进行下载使用的，跟第三方模块差不多。 自定义中间件 手动模拟express.urlencoded中间件，解析POST提交到服务器的表单数据。 编写代码 封装为模块 自定义中间件.js const querystring = require("querystring"); const mw=function (req,res,next) { let body='' //监听data事件,把数据切割,避免数据太大,无法接受 req.on('data',(chunk)=>{ body+=chunk }) req.on('end',()=>{ req.body=querystring.parse(body) next() }) } module.exports=mw express.js const express=require('express') const mw=require('./自定义中间件') const app=express() app.use(express.static('public')) app.get('/',(req,res)=>{ console.log(1) res.sendFile(__dirname+"index.html") }) app.post('/login',mw,(req,res)=>{ let response={ "name":req.body.name, "gender":req.body.gender } res.send(response) }) app.listen(3000) 4. 跨域问题 CORS相关响应头： 名称 作用 Access-Control-Allow-Origin 只允许访问该资源的外域URL Access-Control-Allow-Headers 仅支持服务端发送9个请求头，设置此参数额外增加 Access-Control-Allow-Methods 允许客户端请求服务器的方法 CORS跨域请求资源分类： 简单请求，只允许get，post，head，且请求头不超过合规的9个字段，无自定义头部。（一次） 预检请求，请求方式为get，post，head外的请求，请求头包含自定义头部，向服务器发送application/json格式数据。 浏览器会发送OPTION请求进行预检，以获知服务器是否允许该实际请求，服务器响应预检请求后，才会发送真正的请求。（两次） 解决方案：1. CORS中间件，JSONP(有缺陷，只支持POST请求) npm install cors直接使用 router.js: const express=require('express') const router=express.Router() router.get('/get',function (req,res) { const query=req.query res.send({ status:0, message:'GET 请求成功!', data:query }) }) router.post('/post',function (req,res) { const query=req.body res.send({ status:0, message:'POST 请求成功!', data:query }) }) router.delete('/delete',function (req,res) { res.send({ status:0, message:'Delete 请求成功' }) }) module.exports=router express.js: const express=require('express') const app=express() const router=require('./router') const cors=require('cors') app.use(cors()) app.use(express.urlencoded({extended:false})) app.use('/api',router) app.listen(8080) index.html: 跨域问题 GET POST Delete 七、Mysql模块 安装与配置 安装操作Mysql数据库的第三方模块 通过mysql模块连接到Mysql数据库 通过mysql模块执行SQL语句 const mysql=require('mysql') const database=mysql.createPool({ host:'127.0.0.1', //数据库的IP地址 user:'root', //登录用户名 password:'root', //登录密码 database:'node' //要操作的数据库 }) //测试mysql是否连接成功 database.query('SELECT 1',(err,results)=>{ if(err){ return console.log(err.message) } console.log(results) }) 基本使用 mysql模块进行mysql的操作，其实就是通过数据库语句对数据库进行查询，只不过mysql模块负责与数据库交互。 const connect=require('mysql') const database=connect.createPool({ host:'127.0.0.1', //数据库的IP地址 user:'root', //登录用户名 password:'root', //登录密码 database:'node' //要操作的数据库 }) //SELECT查询 database.query('select *from users',(err,results)=>{ if(err){ return console.log(err.message)} console.log(results) }) //INSERT插入数据 const user={username:'Tom',password:'IamTom'} const sqlStr=`INSERT INTO users (username,password) VALUES (?,?)` //const sqlStr=`INSERT INTO users SET?` 插入数据字段对应的时候 //database.query(sqlStr,user,function) database.query(sqlStr,[user.username,user.password],(err,results)=>{ if(err){ return console.log(err.message) } if(results.affectedRows===1){ //results是一个对象,内置了fieldCount、affectedRows、InsertId、serverStatus、 warningCount、 message、 protocol41、changedRows属性 console.log('数据插入成功') } }) //更新数据库对象 const UpdateUser={id:2,username:'Jerry',password:'IamJerry'} const updateStr=`UPDATE users SET username=?,password=? where id=?` // const updateStr=`UPDATE users SET ? where id=?` database.query(updateStr,[UpdateUser.username,UpdateUser.password,UpdateUser.id],(err,results)=>{ if(err){ return console.log(err.message) } if(results.affectedRows===1){ console.log('更新成功') } }) //Delete删除 const DeleteStr=`Delete FROM users where id=?` database.query(DeleteStr,2,(err,results)=>{ if(err){ return console.log(err.message) } if(results.affectedRows===1){ console.log('删除成功') } }) 删除推荐使用标记删除，即使用status等字段标记删除，避免误操作delete直接真删除了 Web开发模式 服务器渲染的Web开发：服务器发送给客户端HTML页面，是在服务器通过字符串拼接，动态生成的，不需要使用ajax额外请求数据 优点： 前端耗时少，服务器负责动态生成HTML内容，浏览器直接渲染页面即可。 利于SEO，服务器响应的是完整的HTML页面内容，爬虫更容易爬取。 缺点： 占用服务器资源，服务器端完成HTML页面拼接，如果请求较多，服务器压力过大。 不利于前后端分离，开发效率低，使用服务器渲染无法完成分工合作，对于前端复杂度高的项目，不利于开发。 前后端分离的Web开发：依赖于Ajax技术的应用，后端只负责提供API接口，前端使用ajax调用接口的技术。 优点： 开发体验好，利于分工，前端负责UI开发，后端专注于API开发，前端选择性更多 用户体验好，实现页面局部刷新。 减轻服务器端渲染压力。 缺点： 不利于SEO，完整的HTML页面需要在客户端动态拼接完成，爬虫无法爬取页面有效信息。 Session认证 通过session-express中间件使用session。 const express=require('express') const app=express() const session=require('express-session') app.use(express.urlencoded({extended:false})) app.use(session({ secret:'Aiwin', resave:false, //是否允许并行发送多个请求 saveUninitialized:true //初始化session时是否保存到存储。默认为true })) app.post('/api/login',(req,res)=>{ if(req.body.username!=='admin'||req.body.password!=='admin'){ return res.send({status:1,message:'登录失败'}) } req.session.user=req.body req.session.islogin=true res.send({status:0,message:'登录成功'}) }) app.get('/api/username',(req,res)=>{ if(!req.session.islogin){ return res.send({status:1,message:'fail'}) } return res.send({status:0,username:req.session.user}) }) app.get('/api/logout',(req,res)=>{ req.session.destroy() res.send({ status:0, message:'退出Session成功' }) }) app.listen(8080) 未登录时： 登录后： JWT Session认证机制需要配合Cookie才能实现，由于Cookie不支持跨域访问，当涉及前端跨域请求后端接口时，需要使用很多额外的配置，才能实现Session认证，此时推荐使用JWT。 工作原理：用户的信息通过Token字符串的形式，保存在客户端浏览器，服务器通过还原token字符串的形式来认证用户身份。 JWT组成部分： JWT=Header(头部).Payload(有效载荷).Signature(签名) 部分 说明 头部Header JWT类型+加密算法，JWT主要加密算法又HS256 RS256 载荷Payload 携带存放的数据和注册声明，包括签发者，接收者，签发时间，过期时间等 签名Signature 使用头部定义的加密算法和密钥生成对数据进行加密生成 使用中间件 jsonwebtoken 生成JWT字符串 express-jwt 用于将JWT字符串解析还原成JSON对象 const express=require('express') const jwt=require('jsonwebtoken') const {expressjwt: expressJWT} = require("express-jwt"); const bodyParser=require('body-parser') const app=express() app.use(bodyParser.urlencoded({ extends: false })) const secretKey='Aiwin' //.unless()指定那些接口不需要访问权限 app.use( expressJWT({ secret: secretKey, algorithms: ["HS256"], }).unless({path:[/^\/api\//]}) ); app.post('/api/login',(req,res)=>{ const user=req.body if(user.username!=='admin'||user.password!=='admin'){ return res.send({status:1,message:'登录失败'}) } const token=jwt.sign({username:user.username},secretKey,{expiresIn:'300s'}) res.send({status:200,message:'登录成功',token:token}) }) app.get('/admin/getinfo',(req,res)=> { res.send({ status:200, message:'获取用户信息成功', data:req.auth }) }) //捕捉解析JWT解析失败 app.use((err,req,res,next)=>{ if(err.name==='UnauthorizedError'){ return res.send({status:401,message:'无效token'}) } res.send({status:500,message:'未知错误'}) }) app.listen(8080) 八、multer模块 用于处理multipart/form-data类型的表单数据,它主要用于上传文件 例子： app.js: const express=require('express') const app=express() app.use('/multer',require("./multer")); app.listen(3000) multer.js: const router = require("express").Router(); const multer = require("multer"); const path=require('path') path.extname('jpg') const fs=require('fs') const upload = multer({ dest:"./upload"//上传文件存放路径 }); const singleMidle = upload.single("singleFile");//一次处理一张 //可同时处理多个上传控件的上传 //实际项目中根据自己的情况，使用以上三种用法之一即可！ router.get("/",(req,res)=>{ res.sendFile(path.join(__dirname,'/public/index.html')) }) router.post("/singup", singleMidle, function (req, res) { let oldname=req.file.path let newname=req.file.path+ path.extname(req.file.originalname) fs.renameSync(oldname,newname) res.send(req.file); }); module.exports = router; index.html: multer的使用方案 九、大事件项目 参考文档：某🐎大事件项目 完成的API接口： 链接：https://pan.baidu.com/s/1CXZvE1uq7AFo5iggCDjKFg?pwd=s483 提取码：s483 十、总结 学的东西都比较基础，上手也是非常快，最后的项目也是相当于把前面复习的都用了一遍，感觉NodeJS开发效率也是很高，有很多第三方中间件能直接引用完成很多东西，不需要再自己写，算是站在别人的肩膀上，自己学的比较粗糙，因为只要求自己能看懂基本的代码即可，真正要学好还得细学。
前言 这学期学习了Linux课程，对Linux的常用命令写个总结。 一、为什么Linux如此流行？ 1.原因 作为目前世界上最受欢迎的系统，近乎世界超级计算机Top10中全是Linux系统的身影。 Linux流行的背后，原因是什么呢?： 1、Linux稳定且有效率。 2、Linux开源且免费，有强大的社区支持。 3、Linux拥有更加安全的用户与文件权限策略。 4、Linux易于维护，拥有超强的定制性 当然，个人认为，Linux流行的关键原因在于开源且免费，强大的社区支持。 2.Linux系统的版本 Linux的发行版本有很多，主要流行的如下： 1.Red Hat Linux 2.CentOs，与RedHat企业版完全兼容 3、Ubuntu、Debian、Kali 4、国内的OpenEuler、OpenAnolis 二、Linux的基础命令 1.目录结构 Linux的目录结构如上，/etc存放着系统配置文件的目录，/home是除root外的其它用户的目录，/dev存放设备文件，dev存放设备文件，boot存启动文件，tmp是临时目录，usr存放库文件，文档，命令，用户数据等，var存放变化的文件，如日志文件等， 目录名 解析 bin 存放普通用户可执行命令 etc 系统的配置文件 root 超级用户目录 dev 系统的一些设备文件，驱动等 boot 系统的启动文件 tmp 临时文件存放目录 usr 存放用户的应用程序和文件 var 存放一些变化的文件，如程序日志等 home 普通用户的目录 run 存放进程产生的临时文件 mnt 临时的挂载文件系统 sbin 存放系统管理员才能执行的命令 2.文件命令 1.ls 列出该目录下的子目录与文件，常用参数： -a 以.开头的隐藏项目也列出 -l 列出目录的详细信息，可直接使用ll 2.pwd 显示当前所处目录 3.cd 切换工作的目录，可使用相对路径与绝对路径，相对路径切换多个目录可以使用../..这种格式 4.touch、mkdir touch是不存在文件则创建文件，存在则修改文件时间。 mkdir是创建目录 5.cat、tail、head、tail、od、tee、more、less cat 直接输出文件的全部内容 head 输出文件开头的内容、-n 指定行数 tail 输出文件末尾的内容，-n输出尾n行 more 按页显示文本文件内容，只允许向前浏览 less 与more类似，允许向前向后浏览 tee 将文件的输出复制到另一个文件 od 将文件内容以指定的禁止显示，-t指定进制 6.rm、cp、mv rm删除某个文件，常用rm -r 用于删除目录 cp复制文件，-r可以复制目录 mv移动文件 7.find、grep、xargs find 搜索文件命令，可通过通配符搜索某个文件 grep，以正则表达式进程全局查找匹配行 命令传递参数的过滤器，可将管道或标准输入数据转换成命令行参数，也可从文件中读取数据 三者合一、可用于搜索文件的内容 8.tar、zip、uzip、gzip 1.tar将文件打包或压缩或解包，压缩文件后缀有gz、bz、xz，常用参数 -c产生.tar打包文件，-x解包、-zjJ分别以gz、bz2、xz打包或解包文件。 2.zip将文件打包，常用-r递归打包目录 3.uzip将文件解包 4.gzip压缩命令，只能压缩文件，不能压缩目录 9.ln 创建软或硬连接，可以理解为相当于创建快捷方式，硬连接的链接文件和被链接文件必须在同一目录且不能建立指向目录的硬连接，链接的源文件消失，链接即失效。常用参数：-s 创建符号链接即软连接，-P，创建直接指向符号链接的硬链接，-f删除已存在的目标文件。 10.lsattr和chattr chattr 与 lsattr 分别用于改变和显示文件属性，与 chmod 命令相比，chmod 只改变文件的读写、执行权限，更底层的属性控制是由 chattr 来改变。 chattr主要常用的参数有：-R 递归改变指定目录下的文件属性，+在原有的参数设定基础上、追加参数，-在原有的参数设定基础上、移除参数。 lsattr常用参数：-R 递归显示目录和文件属性、-a 显示所有隐藏文件属性、 chattr可以改变的文件属性有： 属性 含义 a 只允许向文件追加数据，不允许删除和修改文件内容。如果目录有具有这个属性，系统将只允许在目录下简历和修改文件，不允许删除任何文件。只有root才能设置该属性 c 自动将文件压缩，在读取时自动解压缩 d 在进行文件系统备份时，dump指令忽略此文件 e 在ext文件系统中，表示该文件使用区段(extents)映射磁盘上的块 i 不允许对文件进行任何的修改。对于目录而言，只能修改目录之下的文件，不允许建立和删除文件。只有root能设置此属性 j 设定此参数使得当通过mount参数：data=ordered 或者 data=writeback 挂 载的文件系统，文件在写入时会先被记录(在journal中)。如果filesystem被设定参数为 data=journal，则该参数自动失效 s 系统在删除文件时，使用0填充文件所在的区域 t 文件拥有t属性时，与其它文件合并时末端不会存在局部块碎片 u 与s相反，删除文件时，文件内容其实还存在磁盘中，以便以后能够恢复删除的文件 A 不能修改文件的最后访问时间 D 如果一个目录设置了D属性，任何改变将同步到磁盘；这等价于mount命令中的dirsync选项，同步目录 S Sync，一旦应用程序对文件执行了写操作，则立刻将改动同步到磁盘 T 目录设置T属性，Orlov块分配器将该目录视为目录层次结构的顶部，提示块分配器该目录下的子目录是无关的，将被分散的分配 11.Linux重定向与管道 linux执行命令行即shell命令时会有三种标准文件： 标准输入文件即0，使用<或<< 标准输出文件即代码1，使用>或>> 标准错误输出文件即代码2，使用2>或2>> 注：双符号表示追加 管道： | 将上一次的输出作为下一次的输入 || 当左边命令为假，右边命令才会执行 & 表示任务在后台执行 &&前一条命令执行成功才会执行后门的命令 ;隔开命令，从左到右执行 3.Vim编辑器 一个强大的文本编辑器，常用的操作如下 命令 操作 k/j 向上或下查找 gg 移动到文件的第一行 G 移动到文件的最后一行 /word 搜索某个字符串，n表示到下一个 n 光标向下移动n行 0或$ 移动到当前行首字符或尾字符 dd/ggdG 删除当前行，ndd表示连续删除后n行，ggdG表示删除全部 yy 复制当前行，nyy表示复制下面n行 p/P 将复制内容在下一行粘贴上/在上一行粘贴上 u 还原上一次操作 ctrl+r 重复上一次动作 i 光标前插入文本 x 删除光标字符 R+word 连续替换多个字符 :%s/old/new/g 有$替换整个文档的内容，无%替换该行全部，去掉g表示替换首个 set number 显示行数 vim编辑器异常退出会产生.swp临时文件，需要删除掉 4.关于Linux用户 linux的用户账号的基本信息主要存放在/etc/passwd中，存放方式为 字段1：用户帐号的名称(euler) 字段2：用户密码字串或者密码占位符“x” 字段3：用户帐号的UID号(用户ID，1000) 字段4：所属基本组帐号的GID号(组ID，1000) 字段5：用户全名(euler) 字段6：宿主目录(/home/euler) 字段7：登录Shell信息(/bin/bash) 一般密码文件存放在/etc/shadow中,进行了hash加密 组账号信息一般存放在/etc/group中，存放形式为 字段1：组名。 字段2：组密码：组密码，用x占位。 字段3：组标识符：简称GID，唯一标识一个组。 字段4：组内用户列表：属于这个组的所有用户的列表 其中上两张图的0都表示超级用户或超级用户所在组。 1.添加删除用户/组 useradd：添加用户 usedel：删除用户，常用-r删除用户和主目录 usermod：将用户添加到某个组 groupadd：增加某个组 groupdel：删除某个组 注：以上操作皆要通过root用户完成。 2.关于文件权限（chmod、chown） r 表示读权限，数字4 w 表示写权限，数字2 x 表示执行权限，数字1 图中-rw-r--r--的解析： -表示普通文件 rw-表示文件拥有者权限 r-- 表示文件所属组成员权限 r--表示其它用户的权限 权限赋予命令：chmod 可通过数字直接赋予 chmod 数字 文件名 可通过对用户权限直接赋予 chmod u/g/o/a +/- /r/w/x 文件名 字母 解析 u 所有者 g 组成员 o 其他成员 a 所有人 更改文件和目录所有者和所属组：chown 需要root用户执行，命令格式： chown [-R] [user][:group] 5.进程与系统管理 1.进程管理 一般linux启动一个进程后，会在proc/PID创建一个文件夹，用于进程的管理 常用查看进程的命令，ps aux，a表示显示所有进程，u表示面向用户格式，x表示显示后台进程信息。 字符 解析 USER 表示进程所属用户 PID 进程唯一标识号 %CPU CPU的占用率 %MEM 内存的占用率 VSZ 虚拟内容的占用大小 RSS 实际内存占用大小 TTY 终端类型 Stat 进程状态 START 进程启动时刻 TIME 进程运行时长 top或者htop命令也可用于查看进程的情况，会每隔一段时间进行更新，htop要相对简单一些 终止进程命令：kill PID即可终止进程，-9参数表示强制终止 命令行终止:Ctrl+C与Ctrl+Z，区别在于前者是中断正在执行的命令，后者是将进程挂起，调入后台并停止执行，可通过jobs命令查看后台，或直接fg +序号将后台终止进程恢复 2.定时任务 一般linux的定时任务都会存放在/etc/crontab中，不同的系统会有微笑的不同，日、月、小时任务都分别有不同的文件夹，如cron.daily、cron.hourly等 linux可通过crontab设置定时任务，常用参数-e 设置任务，-l 查看任务计划 设置任务的时间格式为： 分钟 小时 日期 月份 星期 命令 表示周一到周五的早上6点输出wakeup， 代表通配符，表示该范围的任意时间。 3.一些其它命令 free 查看内存使用情况 df 查看磁盘使用情况 du 显示指定目录或文件所占用的磁盘空间 alias 设置别名，支队当前中断生效，要永久使用，需要写入到~/.bashrc中并保存，然后执行 source ~/.bashrc 4.关于环境变量 linux系统与windows系统一样，根据环境变量去寻找命令的执行命令，一直往下找，找不到即报错，找到即执行。 linux的环境变量以:为分割符，可以知道我们命令执行的诸如ls等命令都在/usr/bin目录中，在/usr/bin目录找到原文件进而进行命令行的命令执行。 关于环境变量的设置：利用命令export在原有环境变量上:目录即可，同样要永久生效，需要写入~/.bashrc中 5.Linux防火墙(ufw、iptables) 不同的linux系统一般会使用不同的防火墙工具，如Openeuler/centos会使用firewalld、ubuntu使用ufw等，也可以直接使用iptables进行规则的配置，iptables配置要较为复杂，一般执行有关防火墙的操作都需要sudo权限。 ufw常用命令如下： 命令 解析 ufw status 查看防火墙状态 ufw enable 开启ufw防火墙 ufw disable 关闭ufw防火墙 ufw default allow 默认通过 ufw allow port 允许某个端口 ufw deny port 拒绝某个端口 ufw allow from IP 允许某个IP地址访问本机全部端口 ufw delete allow port 删除一条规则 ufw allow from IP to any port PORT 允许特定的IP/端口组合 关于iptables： iptables是采用数据包过滤机制工作的，所以它会对请求的数据包的包头进行分析，并根据我们预先设定的规则进行匹配来决定是否可以进入主机，一般来说，会一层一层的匹配，若上免的规则明确表示阻止或通过，则不会再匹配下面的规则，配置一般采用先精细后粗犷的规则。 iptables包含了四表五链，分别是： Filter表：过滤数据包 NAT表：用于网络地址转换(IP、端口) Mangle表：修改数据包的服务类型、TTL、并且可以配置路由实现QOS Raw表：决定数据包是否被状态跟踪机制处理 INPUT链 ：处理输入数据包。 OUTPUT链 ：处理输出数据包。 FORWARD链 ：处理转发数据包。 PREROUTING链 ：用于目标地址转换(DNAT)。 POSTOUTING链 ：用于源地址转换(SNAT)。 不同的链路存放着用户设置的规则，这些规则根据功能不同被存放到不同的表中，会按照表的优先级取出对应的规则，进行匹配，表的优先级是 raw->mangle->nat->filter。 各链对应的表如下： 链名 表名 PREROUTING rao、mangle、nat FORWARD mangle、filter INPUT manle、filter OUPUT raw、mangle、nat、filter POSTROUTING mangle、nat 一般数据库进入网卡的处理流程 : 1、首先匹配PREROUTING链，系统根据数据包的目的地址判断接下来的数据过程，可能有三种情况： （1）数据包目的地址是本机，发往INPUT链，匹配INPUT链规则，通过则发给相应本地处理，没有则丢弃。 （2）数据包目的地址不是本机，包被发往FORWARD链，匹配规则，通过则发往相应本地处理，没有则丢弃。 （3）数据包是本地产生的，则发往OUPUT链，匹配OUPUT链的规则，通过则发往相应本地处理，没有则丢弃。 iptables常用命令： iptables -A INPUT -p tcp --dport 80 -j ACCPET 追加规则开放80或某端口 iptables -I INPUT -p icmp -j ACCEPT 根据协议插入一条记录 iptables -D INPUT number 删除INPUT链中第number条记录 iptables --list 查看所有规则 iptables -nL -t 表名 查看某个表名的规则 iptables中包含了可以查看ufw防火墙设置的规则信息。 6.一些基本的系统信息、日志查询 last：查看用户最近登录信息，主要存放在/var/log/wtmp|btmp|utmp中，wtmp存储登录成功信息、btml存储登录失败 lastb，查看显示用户错误的登录列表 lscpu，查看cpu的相关信息，包括型号，内核 uname -a，查看关于操作系统的信息 cat /proc/version，查看操作系统版本 lsmod，查看所有载入系统的模块信息 netstat，查看端口的开放和连接信息 6.磁盘管理 关于磁盘的设备一般存储再/dev/或者/snap/文件夹中。 查看磁盘信息常用命令：lsblk、fdisk sr0是光驱设备即光盘，也成为cdrom，只支持读，不支持写入操作 磁盘分区 磁盘分区可以将硬盘驱动器划分多个逻辑存储单元，单元称为分区。磁盘分区的好处： （1）限制应用或用户可用空间 （2）将操作系统和程序文件与用户文件分隔 （3）创建用于操作系统虚拟内存交换的单独区域 （4）允许同一磁盘不同操作系统启动 关于分区的命名规则： 分区的方案： MPT分区中： SCSI接口硬盘最多有16个分区，主分区最多4个，逻辑分区最多12个。 IDE接口硬盘最多只能有63个分区，主分区最多4个，逻辑分区最多60个 分区最大2T。 GPT分区中： 由LBA规划，每个硬盘前面有34个LBA，后门有33个LBA用于前面的备份。 LBA0用于兼容MBR，不能修改，LBA1称为GPT表头记录，用于记录备份的对应关系和记录表本身的大小，LBA2-LBA33用于记录实际分区信息。 每一个LBA可用记录4个分区表，一共4*32=128个分区表。 每个硬盘最多128个分区(不分类型)，GPT逻辑块地址分配64位，最大支持18EB分区大小。 磁盘分区操作： 首先，增加虚拟磁盘，VMvare操作如下： 命令 解析 parted /磁盘目录 对磁盘进行分区 mklabel+类型 创建新的分区表 mkpart +name+start+end 创建分区 rm+number 删除分区 quit 退出 磁盘挂载 命令 解析 mkfs -t +类型+目录 创建磁盘挂载 mount +目录+挂载点 将磁盘挂载(临时，重启即消失) umount +挂载点 取消磁盘挂载 blkid +磁盘目录 查看磁盘的信息，包括UUID /etc/fstab 永久挂载磁盘，要将挂载目录等写入该文件 非交互式磁盘 简单说，就是交互式需要进入parted的命令行进行操作，非交互式就是不用进去就能操作，加个--script，后面命令跟parted里面的一样即可，如： sudo parted --script 目录 mklabel gpt，创建gpt分区 sudo parted --script 目录 +命令 关于逻辑卷 LVM-逻辑卷管理是Linux环境中对磁盘分区进行管理的一种机制，是建立在硬盘和分区之上、文件系统之下的一个逻辑层，用于提高磁盘分区管理的灵活性，主要有以下概念： 逻辑卷的划分： 常用命令 命令 解释 pvcreate /DEV 创建物理卷 pvs 查看物理卷 pvremove /DEV 删除物理卷 vgcreate VGNAME /DEV 创建卷组 vgextend /DEV 扩展卷组 vgreduce VGNAME /DEV 收缩卷组 vgremove VGNAME 删除卷组 lvcreate -L 10G -n LVNAME VGNAME 创建指定逻辑卷大小和逻辑卷名称的名称 lvs 查看逻辑卷 lvextend -L +10G /DEV 增加10G逻辑卷大小 lvremove /DEV/VGNAME/LVNAME 删除逻辑卷 7.下载软件包命令 linux下载软件包的命令很多，根据Linux系统的不同会使用不同的命令。 OpenEuler：dnf、rpm。 Ubuntu：apt、yum。 Centos：rpm、yum。 Debian：apt。 常用的命令如下： 命令 作用 apt-get update/apt update 更新软件包列表 yum/dnf check-update 更新软件包列表 apt-get/apt upgrade 更新已安装的包 yum/dnf upgrade 更新已安装的包 apt/yum/dnf search 搜索某个包 apt show package 显示包的安装状态 yum info/deplist package 列出包依赖 dnf info package 列出包依赖 dnf repoquery --requires package 列出包依赖 apt/yum/dnf/pkg install package 安装包 apt/yum remove package 删除包 dnf erase package 删除包 rpm -ivh 可视化安装进度安装软件包 rpm -qa 查询所有已安装软件包 rmp -e 卸载指定软件包 三、Linux的Shell编程开发 1.符号变量 符号 介绍 # 注释符 $ 变量符 '' 单引号字符，全部普通字符 "" 双引号字符，可以包括特殊字符如反引号`，变量符$ $? 返回上一个变量返回值 $# 传送给shell程序位置参数的数量 $0 当前执行的进程名 $! 后台运行的最后一个进程号 $$ 当前进程的进程号 $n 第n个参数 $@、 $* 全部位置参数做列表，*做变量 $() 获取命令输出结果 `` 获取命令输出结果 $(()) 算数拓展运算符 -eq、-ne、-lt、le、-gt、-ge 表示相等、不等于、小于、小于或等于、大于、大于或等于 [] 条件表达式 -d 是否是目录，如 if [-f a.sh] -f 是否是普通文件，如if [-d a.txt] -a、-o 逻辑和与逻辑或 2.常用方法 命令 解析 set 查看环境变量 env 查看全局变量 declare 定义变量 unset 删除变量 export 定义环境变量 ~/.bashrc 用户级环境 四、关于git git是分布式的版本控制系统，说白了，就是一种代码管理工具，能够方便程序员在编写代码，未完成时候能够保存但却不影响项目的运行。 git的流程： git的常用命令： 命令 解释 git init 初始化git仓库 git config user.name name 设置目标用户名 git config user.email email 设置目标邮箱 git config --list 查看配置信息 git add 文件名 将文件添加到暂存区 git checkout 暂存区内容恢复到工作区 git commit -m "提交说明" 将文件从暂存区添加到仓库区 git log 查看提交日志 git reset --hard 版本号 将代码恢复到已经提交的某一个版本 git branch 分支名 创建分支 git checkout -b 分支名称 切换分支 git branch -d 分支名称 删除分支 git push URL master 将缓存区内容提交到远程仓库 git pull 将远程代码下载到本地 git remote add 仓库别名 仓库地址 为远程仓库添加别名 git push -u 别名 分支 将缓存区内容提交到仓库 具体操作如提交到Github： 先在github创建仓库，然后在Settings->develop settings->Personal Access token->Generate new token创建一个提交的token 然后就可以在Linux中通过命令行提交。 这里输入的用户名和密码就是刚才生成Token的用户名和token。
十一、综合训练（购物车功能） 实现功能： 手机商品页面展示，加入购物车功能，商品详情页面，清空购物车，删除购物车商品。 数据库准备，准备的数据库有商品数据库，还有存储购物车信息的数据库，使用SqliteOpenHelper进行操作，商品数据库的字段有id，description，price，picPath，购物车字段id，goodsId，count，实现如下： package com.example.shoppingcart.entity; //购物车信息 public class CartInfo { public int id; // 商品编号 public int goodsId; // 商品数量 public int count; public CartInfo(){} public CartInfo(int id, int goodsId, int count) { this.id = id; this.goodsId = goodsId; this.count = count; } } package com.example.shoppingcart.entity; import com.example.shoppingcart.R; import java.util.ArrayList; public class Goodsinfo { public int id; public String name; public String description; public float price; public String picPath; public int pic; private static String[] mNameArray = { "iPhone11", "Mate30", "小米10", "OPPO Reno3", "vivo X30", "荣耀30S" }; // 声明一个手机商品的描述数组 private static String[] mDescArray = { "Apple iPhone11 256GB 绿色 4G全网通手机", "华为 HUAWEI Mate30 8GB+256GB 丹霞橙 5G全网通 全面屏手机", "小米 MI10 8GB+128GB 钛银黑 5G手机 游戏拍照手机", "OPPO Reno3 8GB+128GB 蓝色星夜 双模5G 拍照游戏智能手机", "vivo X30 8GB+128GB 绯云 5G全网通 美颜拍照手机", "荣耀30S 8GB+128GB 蝶羽红 5G芯片 自拍全面屏手机" }; // 声明一个手机商品的价格数组 private static float[] mPriceArray = {6299, 4999, 3999, 2999, 2998, 2399}; // 声明一个手机商品的大图数组 private static int[] mPicArray = { R.drawable.iphone, R.drawable.huawei, R.drawable.xiaomi, R.drawable.oppo, R.drawable.vivo, R.drawable.rongyao }; public static ArrayList<Goodsinfo> getDeafultList(){ ArrayList<Goodsinfo> goodsList=new ArrayList<Goodsinfo>(); for(int i=0;i<mNameArray.length;i++){ Goodsinfo goodsinfo=new Goodsinfo(); goodsinfo.id=i; goodsinfo.name=mNameArray[i]; goodsinfo.description=mDescArray[i]; goodsinfo.price=mPriceArray[i]; goodsinfo.pic=mPicArray[i]; goodsList.add(goodsinfo); } return goodsList; } } package com.example.shoppingcart.database; import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import com.example.shoppingcart.entity.CartInfo; import com.example.shoppingcart.entity.Goodsinfo; import java.util.ArrayList; import java.util.List; public class ShoppingDBHelper extends SQLiteOpenHelper { private static final String DB_NAME="shopping.db"; private static final String TABLE_GOODS_INFO="goods_info"; private static final String TABLE_CART_INFO="cart_info"; private static final int DB_VERSION=2; private static ShoppingDBHelper databaseUserHelper=null; public ShoppingDBHelper(Context context) { super(context,DB_NAME,null,DB_VERSION); } //单例模式获取数据库实例 public static ShoppingDBHelper getInstance(Context context){ if(databaseUserHelper==null){ databaseUserHelper=new ShoppingDBHelper(context); } return databaseUserHelper; } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { String sql=""; sql= "CREATE TABLE IF NOT EXISTS "+TABLE_GOODS_INFO+"(_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,name VARCHAR NOT NULL,description VARCHAR NOT NULL,price FLOAT NOT NULL,pic_path VARCHAR NOT NULL);"; sqLiteDatabase.execSQL(sql); //购物车信息表 sql= "CREATE TABLE IF NOT EXISTS "+TABLE_CART_INFO+"(_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,good_id INTEGER NOT NULL,count INTEGER NOT NULL);"; sqLiteDatabase.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { } public void insertGoodsinfos(List<Goodsinfo> list){ SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); try{ database.beginTransaction(); for(Goodsinfo goodsinfo:list){ ContentValues contentValues=new ContentValues(); contentValues.put("name",goodsinfo.name); contentValues.put("description",goodsinfo.description); contentValues.put("price",goodsinfo.price); contentValues.put("pic_path",goodsinfo.picPath); database.insert(TABLE_GOODS_INFO,null,contentValues); } database.setTransactionSuccessful(); } catch (Exception e){ e.printStackTrace(); }finally { database.endTransaction(); } } public List<Goodsinfo> queryAllGoodsInfo(){ String sql="SELECT *FROM "+TABLE_GOODS_INFO; List<Goodsinfo> list=new ArrayList<Goodsinfo>(); SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); Cursor cursor=database.rawQuery(sql,null); while(cursor.moveToNext()) { Goodsinfo goodsinfo = new Goodsinfo(); goodsinfo.id = cursor.getInt(0); goodsinfo.name = cursor.getString(1); goodsinfo.description = cursor.getString(2); goodsinfo.price = cursor.getFloat(3); goodsinfo.picPath = cursor.getString(4); list.add(goodsinfo); } cursor.close(); return list; } public void insertCartInfo(int goodsId) { SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); //购物车不存在该商品,则添加一条信息 CartInfo cartInfo=queryCartInfoByGoodsId(goodsId); ContentValues values=new ContentValues(); values.put("good_id",goodsId); if(cartInfo==null){ values.put("count",1); database.insert(TABLE_CART_INFO,null,values); } //如果购物车有该商品,则更新商品数量 else{ values.put("_id",cartInfo.id); values.put("count",++cartInfo.count); database.update(TABLE_CART_INFO,values,"_id=?",new String[]{String.valueOf(cartInfo.id)}); } } private CartInfo queryCartInfoByGoodsId(int goodsId) { SQLiteDatabase database=databaseUserHelper.getReadableDatabase(); Cursor cursor=database.query(TABLE_CART_INFO,null,"good_id=?",new String[]{String.valueOf(goodsId)},null,null,null,null); CartInfo info=null; if(cursor.moveToNext()){ info=new CartInfo(); info.id=cursor.getInt(0); info.goodsId=cursor.getInt(1); info.count=cursor.getInt(2); } return info; } //统计购物车商品的总数量 public int countCartInfo() { SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); int count=0; String sql="SELECT sum(count) FROM " +TABLE_CART_INFO; @SuppressLint("Recycle") Cursor cursor=database.rawQuery(sql,null); if(cursor.moveToNext()){ cursor.getInt(0); } return count; } public List<CartInfo> queryAllCartInfo() { SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); List<CartInfo> list=new ArrayList<CartInfo>(); @SuppressLint("Recycle") Cursor cursor=database.query(TABLE_CART_INFO,null,null,null,null,null,null); while(cursor.moveToNext()){ CartInfo info=new CartInfo(); info.id=cursor.getInt(0); info.goodsId=cursor.getInt(1); info.count=cursor.getInt(2); list.add(info); } return list; } public Goodsinfo queryAllGoodsInfoById(int goodsId) { Goodsinfo info=null; SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); @SuppressLint("Recycle") Cursor cursor=database.query(TABLE_GOODS_INFO,null,"_id=?",new String[]{String.valueOf(goodsId)},null,null,null); if(cursor.moveToNext()){ info = new Goodsinfo(); info.id = cursor.getInt(0); info.name = cursor.getString(1); info.description = cursor.getString(2); info.price = cursor.getFloat(3); info.picPath = cursor.getString(4); } return info; } //根据商品ID删除购物车信息 public void deleteCartInfoByGoodsId(int goodsId) { SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); database.delete(TABLE_CART_INFO,"good_id=?",new String[]{String.valueOf(goodsId)}); } //删除所有购物车信息 public void deleteAllCartInfo(){ SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); database.delete(TABLE_CART_INFO,"1=1",null); } } Activity类主要有4个，分别为MyApplication，购物车类，商品市场类，商品详情类，MyApplication启动时自动将商品数据插入到数据库，并设置购物车全局变量为0. 商品类的展示再通过从数据库中获取存储的商品信息，通过获取商品的子视图，动态的嵌入到页面中，当点击加入购物车时，将点击的商品加入到购物车数据库，随后购物车全局变量+1，显示出来。 购物车功能实现思路：从购物车数据库中取出商品的good_id，从商品数据库中查询出商品的全部信息，然后动态嵌入子视图进行展示。清空功能则直接清空购物车数据库，并将购物车全局变量归0，长按点击删除实现思路则是对嵌入的view进行长监听，长按点击则弹窗，如果删除则从购物车数据库中删除，并将view子视图移除，Myapplication全局变量-1。 商品详情页实现思路：利用Intent传一个商品的good_id进入到商品详情页，通过good_id查询商品的信息，并赋到控件中展示。 MyApplication类： package com.example.shoppingcart; import android.app.Application; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Environment; import android.util.Log; import com.example.shoppingcart.database.ShoppingDBHelper; import com.example.shoppingcart.entity.Goodsinfo; import com.example.shoppingcart.util.FileUtil; import com.example.shoppingcart.util.SharedUtil; import java.io.File; import java.util.List; public class MyApplication extends Application { private static MyApplication mApp; public int goodsCount; public static MyApplication getInstance(){ return mApp; } public void onCreate() { super.onCreate(); mApp=this; Log.d("App状态:","App启动"); InitGoodsInfo(); } private void InitGoodsInfo() { boolean isFirst= SharedUtil.getInstance(this).readBoolean("first",true); String directory=getExternalFilesDir((Environment.DIRECTORY_DOWNLOADS)).toString()+ File.separatorChar; if(isFirst){ List<Goodsinfo> list=Goodsinfo.getDeafultList(); for(Goodsinfo goodsinfo:list){ Bitmap bitmap= BitmapFactory.decodeResource(getResources(),goodsinfo.pic); String path=directory+goodsinfo.id+".jpg"; FileUtil.saveImage(path,bitmap); bitmap.recycle(); goodsinfo.picPath=path; } //打开数据库将商品信息插入表中 ShoppingDBHelper shoppingDBHelper=ShoppingDBHelper.getInstance(this); shoppingDBHelper.getWritableDatabase(); shoppingDBHelper.insertGoodsinfos(list); shoppingDBHelper.close(); SharedUtil.getInstance(this).writeBoolean("first",false); } } } 商品市场类： package com.example.shoppingcart; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; import android.widget.GridLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.example.shoppingcart.database.ShoppingDBHelper; import com.example.shoppingcart.entity.Goodsinfo; import com.example.shoppingcart.util.ToastUtil; import java.util.List; public class ShoppingChannelActivity extends AppCompatActivity implements View.OnClickListener{ private ShoppingDBHelper shoppingDBHelper; private TextView tv_count; private GridLayout gl_Channel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_shopping_channel); shoppingDBHelper=ShoppingDBHelper.getInstance(this); shoppingDBHelper.getWritableDatabase(); shoppingDBHelper.getReadableDatabase(); TextView tv_title=findViewById(R.id.tv_title); tv_title.setText("手机商场"); tv_count=findViewById(R.id.tv_count); gl_Channel=findViewById(R.id.gl_channel); findViewById(R.id.iv_back).setOnClickListener(this); findViewById(R.id.iv_cart).setOnClickListener(this); showGoods(); } //通过手机视图根文件的形式,动态查询数据库,并嵌入到页面中 private void showGoods() { //获取屏幕宽度 int screenWidth=getResources().getDisplayMetrics().widthPixels; LinearLayout.LayoutParams params=new LinearLayout.LayoutParams(screenWidth/2,LinearLayout.LayoutParams.WRAP_CONTENT); //查询数据库所有商品记录 List<Goodsinfo> list=shoppingDBHelper.queryAllGoodsInfo(); gl_Channel.removeAllViews(); for(Goodsinfo goodsinfo:list){ //获取布局文件item_goods.xml的根视图 @SuppressLint("InflateParams") View view= LayoutInflater.from(this).inflate(R.layout.item_goods,null); ImageView iv_thumb=view.findViewById(R.id.iv_thumb); TextView tv_name=view.findViewById(R.id.tv_name); TextView tv_price=view.findViewById(R.id.tv_price); Button btn_add=view.findViewById(R.id.btn_add); iv_thumb.setImageURI(Uri.parse(goodsinfo.picPath)); tv_name.setText(goodsinfo.name); tv_price.setText(String.valueOf((int) goodsinfo.price)); btn_add.setOnClickListener(v->{ addToCart(goodsinfo.id,goodsinfo.name); }); iv_thumb.setOnClickListener(v->{ Intent intent=new Intent(ShoppingChannelActivity.this,ShoppingDetailActivity.class); intent.putExtra("good_id",goodsinfo.id); startActivity(intent); }); //把商品视图加到网格布局,设置屏幕的宽高 gl_Channel.addView(view,params); } } private void addToCart(int goodsId,String name){ shoppingDBHelper.insertCartInfo(goodsId); int count=++MyApplication.getInstance().goodsCount; tv_count.setText(String.valueOf(count)); ToastUtil.show(this,"已添加一部"+name+"到购物车"); } @Override protected void onResume() { super.onResume(); Log.d("返回了","返回了页面"); //查询商品总数并展示 showCartInfoTotal(); } private void showCartInfoTotal() { int count=shoppingDBHelper.countCartInfo(); MyApplication.getInstance().goodsCount=count; Log.d("count:", String.valueOf(count)); tv_count.setText(String.valueOf(count)); } @Override protected void onDestroy() { super.onDestroy(); shoppingDBHelper.close(); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.iv_back: finish(); break; case R.id.iv_cart: Intent intent=new Intent(this,ShoppingcartActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); break; } } } 购物车类： package com.example.shoppingcart; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import com.example.shoppingcart.database.ShoppingDBHelper; import com.example.shoppingcart.entity.CartInfo; import com.example.shoppingcart.entity.Goodsinfo; import com.example.shoppingcart.util.ToastUtil; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; public class ShoppingcartActivity extends AppCompatActivity implements View.OnClickListener { private TextView tv_count; private LinearLayout ll_cart; private ShoppingDBHelper shoppingDBHelper; private List<CartInfo> mCartList; private TextView tv_total_price; private LinearLayout ll_empty; private LinearLayout ll_content; //声明一个根据商品编号查找商品信息的映射,把商品信息缓存起来 private final Map<Integer,Goodsinfo> goodsinfoMap=new HashMap<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_shoppingcart); tv_count=findViewById(R.id.tv_count); TextView tv_title=findViewById(R.id.tv_title); tv_title.setText("购物车"); ll_cart=findViewById(R.id.ll_cart); tv_total_price=findViewById(R.id.tv_total_price); findViewById(R.id.iv_back).setOnClickListener(this); findViewById(R.id.btn_shopping_channel).setOnClickListener(this); findViewById(R.id.btn_clear).setOnClickListener(this); findViewById(R.id.btn_settle).setOnClickListener(this); tv_count.setText(String.valueOf(MyApplication.getInstance().goodsCount)); shoppingDBHelper=ShoppingDBHelper.getInstance(this); ll_empty=findViewById(R.id.ll_empty); ll_content=findViewById(R.id.ll_content); } @Override protected void onResume() { super.onResume(); showCart(); } //展示购物车中的商品列表 private void showCart() { //移除下面所有子视图 ll_cart.removeAllViews(); mCartList=shoppingDBHelper.queryAllCartInfo(); if(mCartList.size()==0){ return ; } for(CartInfo info:mCartList ){ //根据商品编号查询商品数据库中的记录 Goodsinfo goods=shoppingDBHelper.queryAllGoodsInfoById(info.goodsId); goodsinfoMap.put(info.goodsId,goods); View view=LayoutInflater.from(this).inflate(R.layout.item_cart,null); ImageView iv_thumb=view.findViewById(R.id.iv_thumb); TextView tv_name=view.findViewById(R.id.tv_name); TextView tv_desc=view.findViewById(R.id.tv_desc); TextView tv_count=view.findViewById(R.id.tv_price); @SuppressLint("CutPasteId") TextView tv_price=view.findViewById(R.id.tv_price); TextView tv_sum=view.findViewById(R.id.tv_sum); iv_thumb.setImageURI(Uri.parse(goods.picPath)); tv_name.setText(goods.name); tv_desc.setText(goods.description); tv_count.setText(String.valueOf(info.count)); tv_price.setText(String.valueOf((int) goods.price)); tv_sum.setText(String.valueOf((int) (info.count*goods.price))); //给商品行添加长按事件,长按商品就删除该商品 view.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(final View v) { AlertDialog.Builder builder = new AlertDialog.Builder(ShoppingcartActivity.this); builder.setMessage("是否从购物车删除"+goods.name+"？"); builder.setPositiveButton("是", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ll_cart.removeView(v); // 移除当前视图 deleteGoods(info); // 删除该商品 } }); builder.setNegativeButton("否", null); builder.create().show(); // 显示提醒对话框 return true; } }); view.setOnClickListener(v->{ Intent intent = new Intent(ShoppingcartActivity.this, ShoppingDetailActivity.class); intent.putExtra("good_id", goods.id); startActivity(intent); }); ll_cart.addView(view); } //重新计算购物车商品总金额 refreshTotalPrice(); } private void deleteGoods(CartInfo info) { MyApplication.getInstance().goodsCount-=info.count; //从购物车数据库中删除商品 shoppingDBHelper.deleteCartInfoByGoodsId(info.goodsId); //从购物车列表中删除商品 CartInfo removed=null; for(CartInfo cartInfo:mCartList){ if(cartInfo.goodsId==info.goodsId){ removed=cartInfo; break; } } Log.d("delete执行:","执行"); mCartList.remove(removed); //显示最新商品数量 showCount(); ToastUtil.show(this,"已从购物车中删除"+ Objects.requireNonNull(goodsinfoMap.get(info.goodsId)).name); goodsinfoMap.remove(info.goodsId); refreshTotalPrice(); } private void showCount() { tv_count.setText(String.valueOf(MyApplication.getInstance().goodsCount)); //购物车中没有商品,显示空空如也 if(MyApplication.getInstance().goodsCount==0){ ll_empty.setVisibility(View.VISIBLE); ll_content.setVisibility(View.GONE); ll_cart.removeAllViews(); }else{ ll_content.setVisibility(View.VISIBLE); ll_empty.setVisibility(View.GONE); } } private void refreshTotalPrice() { int totalPrice=0; for(CartInfo info:mCartList){ Goodsinfo goods=goodsinfoMap.get(info.goodsId); totalPrice+=goods.price*info.count; } tv_total_price.setText(String.valueOf(totalPrice)); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.iv_back: finish(); break; case R.id.btn_shopping_channel: Intent intent=new Intent(this,ShoppingChannelActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); break; case R.id.btn_clear: shoppingDBHelper.deleteAllCartInfo(); MyApplication.getInstance().goodsCount=0; showCount(); break; case R.id.btn_settle: AlertDialog.Builder builder=new AlertDialog.Builder(this); builder.setTitle("结算商品"); builder.setMessage("支付功能尚未开通"); builder.setPositiveButton("我知道了",null); builder.create().show(); break; } } } 商品详情页类： package com.example.shoppingcart; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import com.example.shoppingcart.database.ShoppingDBHelper; import com.example.shoppingcart.entity.Goodsinfo; import com.example.shoppingcart.util.ToastUtil; import org.w3c.dom.Text; public class ShoppingDetailActivity extends AppCompatActivity implements View.OnClickListener { private TextView tv_count; private TextView tv_goods_price; private TextView tv_goods_desc; private ImageView tv_goods_pic; private ShoppingDBHelper shoppingDBHelper; private int mGoodsId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_shopping_detail); tv_count=findViewById(R.id.tv_count); tv_goods_desc=findViewById(R.id.tv_goods_desc); tv_goods_price=findViewById(R.id.tv_goods_price); tv_goods_pic=findViewById(R.id.iv_goods_pic); TextView tv_title=findViewById(R.id.tv_title); tv_title.setText("商品详情"); findViewById(R.id.iv_back).setOnClickListener(this); findViewById(R.id.iv_cart).setOnClickListener(this); findViewById(R.id.btn_add_cart).setOnClickListener(this); shoppingDBHelper=ShoppingDBHelper.getInstance(this); } @Override protected void onResume() { super.onResume(); showDetail(); } private void showDetail() { //获取传来的商品编号,通过商品编号来展示页面 mGoodsId=getIntent().getIntExtra("good_id",0); if(mGoodsId>0){ Goodsinfo goodsinfo=shoppingDBHelper.queryAllGoodsInfoById(mGoodsId); tv_goods_desc.setText(goodsinfo.description); tv_goods_price.setText(String.valueOf(goodsinfo.price)); tv_goods_pic.setImageURI(Uri.parse(goodsinfo.picPath)); } } @Override public void onClick(View view) { switch (view.getId()){ case R.id.iv_back: finish(); break; case R.id.iv_cart: Intent intent = new Intent(this, ShoppingcartActivity.class); startActivity(intent); break; case R.id.btn_add_cart: addToCart(mGoodsId); break; } } private void addToCart(int mGoodsId) { int count = ++MyApplication.getInstance().goodsCount; tv_count.setText(String.valueOf(count)); shoppingDBHelper.insertCartInfo(mGoodsId); ToastUtil.show(this, "成功添加至购物车"); } } 商品展示视图： <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/orange" android:orientation="vertical" > <include layout="@layout/title_shopping"/> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <GridLayout android:id="@+id/gl_channel" android:layout_width="match_parent" android:layout_height="wrap_content" android:columnCount="2"/> </ScrollView> </LinearLayout> 购物车展示视图： <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/orange" android:orientation="vertical"> <include layout="@layout/title_shopping" /> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:id="@+id/ll_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="visible"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="85dp" android:layout_height="wrap_content" android:gravity="center" android:text="图片" android:textColor="@color/black" android:textSize="15sp" /> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="3" android:gravity="center" android:text="名称" android:textColor="@color/black" android:textSize="15sp" /> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="数量" android:textColor="@color/black" android:textSize="15sp" /> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="单价" android:textColor="@color/black" android:textSize="15sp" /> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="总价" android:textColor="@color/black" android:textSize="15sp" /> </LinearLayout> <LinearLayout android:id="@+id/ll_cart" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="0dp"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center|right" android:text="总金额：" android:textColor="@color/black" android:textSize="17sp" /> <TextView android:id="@+id/tv_total_price" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:gravity="center|left" android:textColor="@color/red" android:textSize="25sp" /> </LinearLayout> </LinearLayout> <LinearLayout android:id="@+id/ll_empty" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="gone" tools:visibility="visible"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="100dp" android:layout_marginBottom="100dp" android:gravity="center" android:text="哎呀，购物车空空如也，快去选购商品吧" android:textColor="@color/black" android:textSize="17sp" /> </LinearLayout> </RelativeLayout> </ScrollView> </LinearLayout> 商品详情视图： <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/orange" android:orientation="vertical"> <include layout="@layout/title_shopping" /> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <ImageView android:id="@+id/iv_goods_pic" android:layout_width="match_parent" android:layout_height="350dp" android:scaleType="fitCenter" tools:src="@drawable/xiaomi" /> <TextView android:id="@+id/tv_goods_price" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="5dp" android:textColor="@color/red" android:textSize="22sp" tools:text="1990" /> <TextView android:id="@+id/tv_goods_desc" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="5dp" android:textColor="@color/black" android:textSize="15sp" tools:text="小米 MI10 8GB+128GB 钛银黑 5G手机 游戏拍照手机" /> </LinearLayout> </ScrollView> </LinearLayout> 单一商品视图： <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/ll_item" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:background="@color/white" android:gravity="center" android:orientation="vertical"> <TextView android:id="@+id/tv_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textColor="@color/black" android:textSize="17sp" tools:text="小米手机" /> <ImageView android:id="@+id/iv_thumb" android:layout_width="180dp" android:layout_height="150dp" android:scaleType="fitCenter" tools:src="@drawable/xiaomi" /> <LinearLayout android:layout_width="match_parent" android:layout_height="45dp" android:orientation="horizontal"> <TextView android:id="@+id/tv_price" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" android:gravity="center" android:textColor="@color/red" android:textSize="15sp" tools:text="20" /> </LinearLayout> </LinearLayout> 购物车标志视图： <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="50dp" android:background="#aaaaff"> <ImageView android:id="@+id/iv_back" android:layout_width="50dp" android:layout_height="match_parent" android:layout_alignParentLeft="true" android:padding="10dp" android:scaleType="fitCenter" android:src="@drawable/ic_back"/> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_centerInParent="true" android:gravity="center" android:textColor="@color/black" android:textSize="20sp" /> <ImageView android:id="@+id/iv_cart" android:layout_width="50dp" android:layout_height="match_parent" android:layout_alignParentRight="true" android:scaleType="fitCenter" android:src="@drawable/cart" /> <TextView android:id="@+id/tv_count" android:layout_width="20dp" android:layout_height="20dp" android:layout_alignParentTop="true" android:layout_toRightOf="@+id/iv_cart" android:layout_marginLeft="-20dp" android:gravity="center" android:background="@drawable/shape_oval_red" android:text="0" android:textColor="@color/white" android:textSize="15sp" /> </RelativeLayout> 单一商品子视图： <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/ll_item" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:background="@color/white" android:gravity="center" android:orientation="vertical"> <TextView android:id="@+id/tv_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textColor="@color/black" android:textSize="17sp" tools:text="小米手机" /> <ImageView android:id="@+id/iv_thumb" android:layout_width="180dp" android:layout_height="150dp" android:scaleType="fitCenter" tools:src="@drawable/xiaomi" /> <LinearLayout android:layout_width="match_parent" android:layout_height="45dp" android:orientation="horizontal"> <TextView android:id="@+id/tv_price" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" android:gravity="center" android:textColor="@color/red" android:textSize="15sp" tools:text="20" /> </LinearLayout> </LinearLayout> 购物车展示单一商品子视图： <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/white" android:orientation="horizontal"> <ImageView android:id="@+id/iv_thumb" android:layout_width="85dp" android:layout_height="85dp" android:scaleType="fitCenter" tools:src="@drawable/xiaomi"/> <LinearLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="3" android:orientation="vertical"> <TextView android:id="@+id/tv_name" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="2" android:gravity="left|center" android:textColor="@color/black" android:textSize="17sp" tools:text="小米手机"/> <TextView android:id="@+id/tv_desc" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="3" android:gravity="left|center" android:textColor="@color/black" android:textSize="12sp" tools:text="小米 MI10 8GB+128GB 钛银黑 5G手机 游戏拍照手机"/> </LinearLayout> <TextView android:id="@+id/tv_count" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:textColor="@color/black" android:textSize="17sp" tools:text="2"/> <TextView android:id="@+id/tv_price" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="right|center" android:textColor="@color/black" android:textSize="15sp" tools:text="1000"/> <TextView android:id="@+id/tv_sum" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1.2" android:gravity="right|center" android:textColor="@color/red" android:textSize="17sp" tools:text="2000"/> </LinearLayout> 十二、内容提供者Provider 为App存取内部数据提供统一的外部接口，让不同应用之间得以共享数据，将用户的输入内容，通过ContentProvider跨进程通信传递到Server App，主要通过Uri作为地址传输，客户端通过ProviderResolver类进行操作，Uri格式如下： content://authority/data_path/id content:// 通用前缀，标识Uri用于ContentProvider定位资源 authority 授权者名称，用于确定具体由哪一个ContentProvider提供资源 data_path 数据路径 id 数据编号，用来请求单条数据 例子：创建server和client端，从client端操作server端的数据库，达到添加，查询，删除用户功能 Server: DatabaseUserHelper.java package com.example.server_provider.database; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import java.util.ArrayList; import java.util.List; public class DatabaseUserHelper extends SQLiteOpenHelper { private static final String DB_NAME="user.db"; public static final String TABLE_NAME="user_info"; private static final int DB_VERSION=2; private static DatabaseUserHelper databaseUserHelper=null; public DatabaseUserHelper(Context context) { super(context,DB_NAME,null,DB_VERSION); } //单例模式获取数据库实例 public static DatabaseUserHelper getInstance(Context context){ if(databaseUserHelper==null){ databaseUserHelper=new DatabaseUserHelper(context); } return databaseUserHelper; } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { String sql= "CREATE TABLE IF NOT EXISTS "+TABLE_NAME+"(_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,name VARCHAR NOT NULL,age INTEGER NOT NULL,height LONG NOT NULL,weight float NOT NULL,married INTEGER NOT NULL);"; sqLiteDatabase.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { } } provider类: package com.example.server_provider.provider; import android.content.ContentProvider; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.util.Log; import com.example.server_provider.database.DatabaseUserHelper; public class UserInfoProvider extends ContentProvider { private DatabaseUserHelper databaseUserHelper; public static final String AUTHORITIES = "com.example.server_provider.provider.UserInfoProvider"; private static final UriMatcher URI_MATCHER=new UriMatcher(UriMatcher.NO_MATCH); private static final int USERS=1; private static final int USER=2; static { //Uri匹配器 URI_MATCHER.addURI(AUTHORITIES,"/user",USERS); URI_MATCHER.addURI(AUTHORITIES,"user/#",USER); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { int count=0; switch (URI_MATCHER.match(uri)){ case USERS: SQLiteDatabase db1=databaseUserHelper.getWritableDatabase(); count=db1.delete(DatabaseUserHelper.TABLE_NAME,selection,selectionArgs); db1.close(); break; case USER: String id=uri.getLastPathSegment(); SQLiteDatabase db2=databaseUserHelper.getWritableDatabase(); count=db2.delete(DatabaseUserHelper.TABLE_NAME,"_id=?",new String[]{id}); db2.close(); break; } return count; } @Override public String getType(Uri uri) { // TODO: Implement this to handle requests for the MIME type of the data // at the given URI. throw new UnsupportedOperationException("Not yet implemented"); } @Override public Uri insert(Uri uri, ContentValues values) { if(URI_MATCHER.match(uri)==USERS) { SQLiteDatabase db = databaseUserHelper.getWritableDatabase(); db.insert(DatabaseUserHelper.TABLE_NAME, null, values); } return uri; } @Override public boolean onCreate() { Log.d("Provider","Provider启动"); databaseUserHelper=DatabaseUserHelper.getInstance(getContext()); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if(URI_MATCHER.match(uri)==USERS) { SQLiteDatabase db = databaseUserHelper.getWritableDatabase(); return db.query(DatabaseUserHelper.TABLE_NAME, projection, selection, selectionArgs, null, null, null); } return null; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // TODO: Implement this to handle requests to update one or more rows. throw new UnsupportedOperationException("Not yet implemented"); } } client端操作类： package com.example.client_provider; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import com.example.client_provider.entity.User; import com.example.client_provider.util.ToastUtil; public class ContentWriteActivity extends AppCompatActivity implements View.OnClickListener { private EditText name; private EditText age; private EditText weight; private EditText height; private CheckBox married; private Button save; private Button delete; private Button query; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_content_write); name=findViewById(R.id.name); age=findViewById(R.id.age); weight=findViewById(R.id.weight); height=findViewById(R.id.height); married=findViewById(R.id.married); save=findViewById(R.id.save); delete=findViewById(R.id.delete); query=findViewById(R.id.query); save.setOnClickListener(this); delete.setOnClickListener(this); query.setOnClickListener(this); } @SuppressLint({"NonConstantResourceId", "Range"}) @Override public void onClick(View view) { switch (view.getId()){ case R.id.save: ContentValues values=new ContentValues(); values.put(UserInfoContent.NAME,name.getText().toString()); values.put(UserInfoContent.AGE,Integer.parseInt(age.getText().toString())); values.put(UserInfoContent.HEIGHT,height.getText().toString()); values.put(UserInfoContent.WEIGHT,weight.getText().toString()); values.put(UserInfoContent.MARRIED,married.isChecked()); getContentResolver().insert(UserInfoContent.CONTENT_Uri,values); ToastUtil.show(this,"保存成功"); break; case R.id.query: Cursor cursor=getContentResolver().query(UserInfoContent.CONTENT_Uri,null,null,null,null); if(cursor!=null){ while (cursor.moveToNext()){ User info=new User(); info.id=cursor.getInt(cursor.getColumnIndex(UserInfoContent._ID)); info.name=cursor.getString(cursor.getColumnIndex(UserInfoContent.NAME)); info.age=cursor.getInt(cursor.getColumnIndex(UserInfoContent.AGE)); info.height=cursor.getInt(cursor.getColumnIndex(UserInfoContent.HEIGHT)); info.weight=cursor.getFloat(cursor.getColumnIndex(UserInfoContent.WEIGHT)); info.married= cursor.getInt(cursor.getColumnIndex(UserInfoContent.MARRIED)) == 1; Log.d("查询结果:",info.toString()); } cursor.close(); } break; case R.id.delete: //删除id为2的 Uri uri= Uri.parse("content://com.example.server_provider.provider.UserInfoProvider/user/2"); //删除全 int count = getContentResolver().delete(UserInfoContent.CONTENT_Uri, "name=?", new String[]{"Aiwin"}); if(count>0){ ToastUtil.show(this,"删除成功"); } break; } } } 运行时动态申请权限 Android系统未了防止某些App滥用权限，从6.0开始引入运行时权限管理机制，允许App在允许过程中拥有某项权限，一旦缺少，可自动弹出小窗口提示开启权限， 也可在应用启动时候即申请全部权限，一般流程如下： 检查App是否开启指定权限，调用ContextCompat的checkSelfPermission方法 请求系统弹窗，便于用户选择，调用ActivityCompat的requestPermissions方法，命令系统弹出申请窗口 重写获得页面的权限请求回调方法onRequestPermissionsResult，在该方法内部处理用户权限选择结果。 例子：申请短信读写、联系人读写权限 package com.example.client_provider; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import android.Manifest; import android.annotation.SuppressLint; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.provider.Settings; import android.util.Log; import android.view.View; import com.example.client_provider.util.PermissionUtil; import com.example.client_provider.util.ToastUtil; public class PermissionHungryActivity extends AppCompatActivity implements View.OnClickListener{ private static final int RequestAll=0; private static final String[] PERMISSION_ALL=new String[]{ Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_SMS, Manifest.permission.SEND_SMS }; private static final String[] PERMISSION_CONTACT=new String[]{ Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS }; private static final int RequestCodeConTact=1; private static final String[] PERMISSION_SMS=new String[]{ Manifest.permission.READ_SMS, Manifest.permission.SEND_SMS }; private static final int RequestCodeSMS=2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_permission_lazy); findViewById(R.id.btn_contact).setOnClickListener(this); findViewById(R.id.btn_sms).setOnClickListener(this); PermissionUtil.CheckPermissions(this,PERMISSION_ALL,RequestAll); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.btn_contact: PermissionUtil.CheckPermissions(this,PERMISSION_CONTACT,RequestCodeConTact); break; case R.id.btn_sms: PermissionUtil.CheckPermissions(this,PERMISSION_SMS,RequestCodeSMS); break; } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode){ case RequestAll: if(PermissionUtil.checkGrant(grantResults)){ Log.d("权限情况","所有权限获取成功"); } else{ for(int i=0;i<permissions.length;i++){ if(grantResults[i]!= PackageManager.PERMISSION_GRANTED){ switch (permissions[i]){ case Manifest.permission.READ_CONTACTS: case Manifest.permission.WRITE_CONTACTS: ToastUtil.show(this,"通讯录读写获取不成功"); JumpToSettings(); return ; case Manifest.permission.READ_SMS: case Manifest.permission.SEND_SMS: ToastUtil.show(this,"短信读写获取不成功"); JumpToSettings(); return ; } } } } case RequestCodeConTact: if(PermissionUtil.checkGrant(grantResults)) { Log.d("权限情况:","通讯录权限请求成功"); } else{ ToastUtil.show(this,"通讯录获取失败"); JumpToSettings(); } break; case RequestCodeSMS: if(PermissionUtil.checkGrant(grantResults)) { Log.d("权限情况:","短信权限请求成功"); } else{ ToastUtil.show(this,"短信获取失败"); JumpToSettings(); } break; } } public void JumpToSettings(){ Intent intent=new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.fromParts("package", getPackageName(), null)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } } package com.example.client_provider.util; import android.app.Activity; import android.content.pm.PackageManager; import android.os.Build; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; public class PermissionUtil { public static boolean CheckPermissions(Activity activity, String[] permissions, int RequestCode){ if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.M){ int check= PackageManager.PERMISSION_GRANTED; for(String permission:permissions ){ check=ContextCompat.checkSelfPermission(activity,permission); if(check!=PackageManager.PERMISSION_GRANTED){ break; } } //未开启权限则自动弹窗 if(check!=PackageManager.PERMISSION_GRANTED){ ActivityCompat.requestPermissions(activity,permissions,RequestCode); return false; } } return true; } public static boolean checkGrant(int[] grantResults) { if(grantResults!=null){ for(int grantResult :grantResults){ if(grantResult!=PackageManager.PERMISSION_GRANTED){ return false; } } return true; } return false; } } 添加查询联系人 利用ContentResolver读写联系人，联系人分为两张表： raw_contacts表：记录联系人的_id号，状态等信息 data表：记录用户通讯录的所有数据，根据mimetype_id表示不同的数据类型，raw_contact_id与raw_contacts表中的_id对应，根据这种外键联系， 一个联系人可添加多个邮箱，多个电话号码。 例子：添加和查询联系人，前提：权限已开启 package com.example.client_provider; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.OperationApplicationException; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; import android.util.Log; import android.view.View; import android.widget.EditText; import android.provider.ContactsContract; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.CommonDataKinds; import com.example.client_provider.entity.Contact; import java.util.ArrayList; public class ContactAddActivity extends AppCompatActivity implements View.OnClickListener { private EditText et_contact_name; private EditText et_contact_phone; private EditText et_contact_email; @SuppressLint({"WrongViewCast", "MissingInflatedId"}) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_contact_add); et_contact_name=findViewById(R.id.tv_name); et_contact_phone=findViewById(R.id.phone); et_contact_email=findViewById(R.id.email); findViewById(R.id.add).setOnClickListener(this); findViewById(R.id.query).setOnClickListener(this); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.add: Contact contact=new Contact(); contact.name=et_contact_name.getText().toString(); contact.phone=et_contact_phone.getText().toString(); contact.email=et_contact_email.getText().toString(); //使用ContentSolver多次写入 //addContacts(getContentResolver(),contact); //批量处理联系人,好处是，要么全部成功，要么全部失败，保证了事务的一致性 addFullContacts(getContentResolver(), contact); break; case R.id.query: readPhoneContact(getContentResolver()); break; } } private void readPhoneContact(ContentResolver resolver) { Cursor cursor = resolver.query(ContactsContract.RawContacts.CONTENT_URI, new String[]{ContactsContract.RawContacts._ID}, null, null, null, null); while (cursor.moveToNext()) { int rawContactId = cursor.getInt(0); Uri uri = Uri.parse("content://com.android.contacts/contacts/" + rawContactId + "/data"); Cursor dataCursor = resolver.query(uri, new String[]{Contacts.Data.MIMETYPE, Contacts.Data.DATA1, Contacts.Data.DATA2}, null, null, null); Contact contact = new Contact(); while (dataCursor.moveToNext()) { @SuppressLint("Range") String data1 = dataCursor.getString(dataCursor.getColumnIndex(Contacts.Data.DATA1)); @SuppressLint("Range") String mimeType = dataCursor.getString(dataCursor.getColumnIndex(Contacts.Data.MIMETYPE)); switch (mimeType) { case CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE: contact.name = data1; break; case CommonDataKinds.Email.CONTENT_ITEM_TYPE: contact.email = data1; break; case CommonDataKinds.Phone.CONTENT_ITEM_TYPE: contact.phone = data1; break; } } dataCursor.close(); if (contact.name != null) { Log.d("联系人:", contact.toString()); } } cursor.close(); } private void addFullContacts(ContentResolver contentResolver, Contact contact) { ContentProviderOperation op_main=ContentProviderOperation .newInsert(ContactsContract.RawContacts.CONTENT_URI) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME,null) .build(); //联系人姓名记录操作器 ContentProviderOperation op_name = ContentProviderOperation .newInsert(ContactsContract.Data.CONTENT_URI) // 将第0个操作的id，即 raw_contacts 的 id 作为 data 表中的 raw_contact_id .withValueBackReference(Contacts.Data.RAW_CONTACT_ID, 0) .withValue(Contacts.Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) .withValue(Contacts.Data.DATA2, contact.name) .build(); ContentProviderOperation op_phone=ContentProviderOperation .newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(Contacts.Data.RAW_CONTACT_ID,0) .withValue(Contacts.Data.MIMETYPE,CommonDataKinds.Phone.CONTENT_ITEM_TYPE) .withValue(Contacts.Data.DATA1, contact.email) .withValue(Contacts.Data.DATA2, CommonDataKinds.Email.TYPE_WORK) .build(); ContentProviderOperation op_email = ContentProviderOperation .newInsert(ContactsContract.Data.CONTENT_URI) // 将第0个操作的id，即 raw_contacts 的 id 作为 data 表中的 raw_contact_id .withValueBackReference(Contacts.Data.RAW_CONTACT_ID, 0) .withValue(Contacts.Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE) .withValue(Contacts.Data.DATA1, contact.email) .withValue(Contacts.Data.DATA2, CommonDataKinds.Email.TYPE_WORK) .build(); ArrayList<ContentProviderOperation> operations = new ArrayList<>(); operations.add(op_main); operations.add(op_name); operations.add(op_phone); operations.add(op_email); try { contentResolver.applyBatch(ContactsContract.AUTHORITY,operations); } catch (OperationApplicationException | RemoteException e) { throw new RuntimeException(e); } } private void addContacts(ContentResolver resolver, Contact contact) { ContentValues values=new ContentValues(); Uri uri=resolver.insert(ContactsContract.RawContacts.CONTENT_URI,values); //获取RawContacts表中的ID long rawContactId= ContentUris.parseId(uri); ContentValues name=new ContentValues(); //关联联系人编号 name.put(Contacts.Data.RAW_CONTACT_ID,rawContactId); //姓名的数据类型 name.put(Contacts.Data.MIMETYPE,CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE); //联系人姓名 name.put(Contacts.Data.DATA2,contact.name); resolver.insert(ContactsContract.Data.CONTENT_URI,name); ContentValues phone=new ContentValues(); phone.put(Contacts.Data.RAW_CONTACT_ID,rawContactId); phone.put(Contacts.Data.MIMETYPE,CommonDataKinds.Phone.CONTENT_ITEM_TYPE); phone.put(Contacts.Data.DATA1,contact.phone); //联系类型,1表示家庭,2表示工作 phone.put(Contacts.Data.DATA2,CommonDataKinds.Phone.TYPE_MOBILE); resolver.insert(ContactsContract.Data.CONTENT_URI, phone); ContentValues email=new ContentValues(); email.put(Contacts.Data.RAW_CONTACT_ID,rawContactId); email.put(Contacts.Data.MIMETYPE,CommonDataKinds.Email.CONTENT_ITEM_TYPE); email.put(Contacts.Data.DATA1,contact.email); email.put(Contacts.Data.DATA2,CommonDataKinds.Email.TYPE_WORK); resolver.insert(ContactsContract.Data.CONTENT_URI,email); } } 监听短信内容 ContentObserver监听短信 内容观察期ContentObserver给目标内容注册一个观察器，目标内容一旦发生变化，规定好 的动作马上触发，从而执行开发者预先定义的代码 例子： package com.example.client_provider; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.util.Log; public class MonitorsmsActivity extends AppCompatActivity { private SmsGetObserver mObserver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_monitorsms); Uri uri=Uri.parse("content://sms"); mObserver = new SmsGetObserver(this); //true表示会同时匹配uri的派生uri getContentResolver().registerContentObserver(uri,true,mObserver); } @Override protected void onDestroy() { super.onDestroy(); getContentResolver().unregisterContentObserver(mObserver); } private static class SmsGetObserver extends ContentObserver { private final Context mContext; public SmsGetObserver(Context context) { super(new Handler(Looper.getMainLooper())); this.mContext = context; } @SuppressLint("Range") @Override public void onChange(boolean selfChange, @Nullable Uri uri) { super.onChange(selfChange, uri); // onChange会多次调用，收到一条短信会调用两次onChange // mUri===content://sms/raw/20 // mUri===content://sms/inbox/20 // 安卓7.0以上系统，点击标记为已读，也会调用一次 // mUri===content://sms // 收到一条短信都是uri后面都会有确定的一个数字，对应数据库的_id，比如上面的20 if (uri == null) { return; } if (uri.toString().contains("content://sms/raw") || uri.toString().equals("content://sms")) { return; } // 通过内容解析器获取符合条件的结果集游标 Cursor cursor = mContext.getContentResolver().query(uri, new String[]{"address", "body", "date"}, null, null, "date DESC"); if (cursor.moveToNext()) { // 短信的发送号码 String sender = cursor.getString(cursor.getColumnIndex("address")); // 短信内容 String content = cursor.getString(cursor.getColumnIndex("body")); Log.d("监听到的短信:", String.format("发送方:%s,内容为:%s", sender, content)); } cursor.close(); } } } 发送彩信 通过Intent和 registerForActivityResult跳转到系统相册选择图片后返回图片到附件，再通过Intent设置权限发送彩信。 package com.example.client_provider; 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.annotation.SuppressLint; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.Toast; import com.example.client_provider.util.ToastUtil; public class SendMmsActivity extends AppCompatActivity implements View.OnClickListener { private ImageView iv_appendix; private ActivityResultLauncher<Intent> mResultLauncher; private EditText et_phone; private EditText et_title; private EditText et_content; private Button btn_send; private Uri picUri; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_send_mms); iv_appendix=findViewById(R.id.iv_appenddix); iv_appendix.setOnClickListener(this); et_phone=findViewById(R.id.phone); et_title=findViewById(R.id.title); et_content=findViewById(R.id.content); btn_send=findViewById(R.id.btn_send_sms); btn_send.setOnClickListener(this); mResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() { @Override public void onActivityResult(ActivityResult result) { if (result.getResultCode() == RESULT_OK) { Intent intent = result.getData(); picUri = intent.getData(); if (picUri != null) { iv_appendix.setImageURI(picUri); Log.d("选中的图片是", picUri.toString()); } } } }); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.iv_appenddix: Intent intent=new Intent(Intent.ACTION_GET_CONTENT); intent.setType("image/*"); mResultLauncher.launch(intent); break; case R.id.btn_send_sms: sendMms(et_phone.getText().toString(),et_title.getText().toString(), et_content.getText().toString()); break; } } private void sendMms(String phone,String title,String message){ Intent intent=new Intent(Intent.ACTION_SEND); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // Intent 的接受者将被准许读取Intent 携带的URI数据 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.putExtra("address",phone); intent.putExtra("title",title); intent.putExtra("message",message); intent.putExtra(Intent.EXTRA_STREAM,picUri); intent.setType("image/*"); startActivity(intent); ToastUtil.show(this,"请在弹窗中选择短信或者信息应用"); } } 通过MediaStore查询图片 需求如下，发送彩信，从手机中查询6张合适的图片显示到屏幕中供用户选择，用户点击图片即可发送彩信，需通过FileProvider，将文件暴露给第三方，并授予读写权限。 <?xml version="1.0" encoding="utf-8"?> <paths> <external-path name="external_storage_download" path="Download"/> </paths> <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <queries> <provider android:authorities="com.example.server_provider.provider.UserInfoProvider" /> </queries> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.WRITE_CONTACTS" /> <uses-permission android:name="android.permission.SEND_SMS" /> <uses-permission android:name="android.permission.READ_SMS" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.MyApplication"> <activity android:name=".ProviderMmsAcivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <provider android:authorities="@string/file_provider" android:name="androidx.core.content.FileProvider" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"/> </provider> </application> </manifest> package com.example.client_provider; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.FileProvider; import android.Manifest; import android.annotation.SuppressLint; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.provider.MediaStore; import android.util.Log; import android.view.ViewGroup; import android.widget.EditText; import android.widget.GridLayout; import android.widget.ImageView; import com.example.client_provider.entity.ImageInfo; import com.example.client_provider.util.FileUtil; import com.example.client_provider.util.PermissionUtil; import com.example.client_provider.util.ToastUtil; import com.example.client_provider.util.Utils; import java.io.File; import java.util.ArrayList; import java.util.List; public class ProviderMmsAcivity extends AppCompatActivity { private static final String[] PERMISSIONS = new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE }; private static final int PERMISSION_REQUEST_CODE = 1; private final List<ImageInfo> imageInfoList=new ArrayList<>(); private GridLayout gl_appendix; private EditText et_phone; private EditText et_title; private EditText et_content; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_provider_mms_acivity); gl_appendix=findViewById(R.id.gl_appendix); et_phone=findViewById(R.id.phone); et_title=findViewById(R.id.title); et_content=findViewById(R.id.content); MediaScannerConnection.scanFile(this,new String[]{Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()},null,null); if(PermissionUtil.CheckPermissions(this,PERMISSIONS,PERMISSION_REQUEST_CODE)){ loadImagelist(); showImageGrid(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if(requestCode==PERMISSION_REQUEST_CODE && PermissionUtil.checkGrant(grantResults)){ loadImagelist(); showImageGrid(); } } private void showImageGrid() { gl_appendix.removeAllViews(); for (ImageInfo info:imageInfoList){ ImageView iv_appendix=new ImageView(this); Bitmap bitmap= BitmapFactory.decodeFile(info.path); iv_appendix.setImageBitmap(bitmap); iv_appendix.setScaleType(ImageView.ScaleType.FIT_CENTER); int px= Utils.dip2px(this,110); ViewGroup.LayoutParams params=new ViewGroup.LayoutParams(px,px); iv_appendix.setLayoutParams(params); int padding=Utils.dip2px(this,5); iv_appendix.setPadding(padding,padding,padding,padding); iv_appendix.setOnClickListener(v->{ sendMms(et_phone.getText().toString(),et_title.getText().toString(), et_content.getText().toString(),info.path); }); gl_appendix.addView(iv_appendix); } } @SuppressLint("Range") private void loadImagelist() { String[] columns=new String[]{ MediaStore.Images.Media._ID, MediaStore.Images.Media.TITLE, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATA, }; //根据Media获取图片的信息 @SuppressLint("Recycle") Cursor cursor=getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, "_size< 409600", null, "_size DESC" ); int count=0; if(cursor!=null) { while(cursor.moveToNext() && count < 6) { ImageInfo info = new ImageInfo(); info.id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID)); info.name = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.TITLE)); info.size = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.SIZE)); info.path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); //检查文件是否合法,是否允许访问。 if(FileUtil.checkFileUri(this,info.path)){ count++; imageInfoList.add(info); } Log.d("获取的图片有",info.toString()); } } } @SuppressLint("StringFormatInvalid") private void sendMms(String phone, String title, String message, String path){ Uri uri; uri= FileProvider.getUriForFile(this,getString(R.string.file_provider),new File(path)); Intent intent=new Intent(Intent.ACTION_SEND); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // Intent 的接受者将被准许读取Intent 携带的URI数据 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.putExtra("address",phone); intent.putExtra("title",title); intent.putExtra("message",message); intent.putExtra(Intent.EXTRA_STREAM,uri); intent.setType("image/*"); startActivity(intent); ToastUtil.show(this,"请在弹窗中选择短信或者信息应用"); } } 应用安装 要使用应用安装也要通过FileProvider提供,其中安卓11之后要额外获取权限android.permission.MANAGE_EXTERNAL_STORAGE的权限，安卓8的时候 要获取READ_EXTERNAL_STORAGE权限，因此这里要注意版本与权限对应。 例子：点击Install按钮自动安卓指定的应用 package com.example.client_provider; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.FileProvider; import android.Manifest; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.provider.Settings; import android.view.View; import android.widget.Toast; import com.example.client_provider.util.PermissionUtil; import com.example.client_provider.util.ToastUtil; import java.io.File; public class ProviderApkActivity extends AppCompatActivity implements View.OnClickListener { //安卓8以后要获取的权限 private static final String[] PERMISSIONS=new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE }; private static final int PERMISSION_REQUEST_CODE=1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_provider_apk); findViewById(R.id.install).setOnClickListener(this); } @Override public void onClick(View view) { //安卓11后要获取MANAGE_EXTERNAL_STORAGE权限 if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.R){ checkAndInstall(); } else { if (PermissionUtil.CheckPermissions(this, PERMISSIONS, PERMISSION_REQUEST_CODE)) { installApk(); } } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if(requestCode==PERMISSION_REQUEST_CODE&& PermissionUtil.checkGrant(grantResults)){ installApk(); } } @RequiresApi(api = Build.VERSION_CODES.R) private void checkAndInstall() { if(!Environment.isExternalStorageManager()){ Intent intent=new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setData(Uri.fromParts("package",getPackageName(),null)); startActivity(intent); } else{ installApk(); } } private void installApk() { String apkPath= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()+"app-release.apk"; //应用包管理器 PackageManager packageManager=getPackageManager(); //获取apk的文件信息 PackageInfo info=packageManager.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES); if(info==null){ ToastUtil.show(this,"安装文件损坏"); return; } Uri uri; uri=FileProvider.getUriForFile(this,getString(R.string.file_provider),new File(apkPath)); Intent intent=new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setDataAndType(uri,"application/vnd.android.package-archive"); startActivity(intent); } } 十三、高级控件 关于高级控件的一些视图处理，会有一个总处理器Adapter，它的继承结构如下： 1. 下拉列表Spinner 下拉列表有两种模式，分别为dropdown和dialog模式即弹出框，设置下拉列表的时候，直接设置子视图后转交给ArrayAdapter处理即可， ArrayAdapter是最简单的适配器，只能显示一行文本。 例子：设置城市下拉框，监听选择的城市 package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Spinner; import com.example.highcontrolleractivity.util.ToastUtil; public class SpinnerDropdownActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener { private static final String[] CityArray=new String[]{"广州","上海","北京","深圳","杭州"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_spinner_dropdown); Spinner sp_dropdown=findViewById(R.id.sp_dropdown); ArrayAdapter<String> stringArrayAdapter=new ArrayAdapter<>(this,R.layout.item_selector,CityArray); sp_dropdown.setAdapter(stringArrayAdapter); sp_dropdown.setSelection(0); sp_dropdown.setOnItemSelectedListener(this); } @Override public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { ToastUtil.show(this,"你选择的是"+CityArray[i]); } @Override public void onNothingSelected(AdapterView<?> adapterView) { } } 2.简单适配器 因为数组适配器只能显示一行文本，因此可以使用简单适配器，同时放置图片和文本，可以使用简单适配器 package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.SimpleAdapter; import android.widget.Spinner; import com.example.highcontrolleractivity.util.ToastUtil; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class SpinnerIconActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener { private static final int[] iconArray = { R.drawable.shuixing, R.drawable.jinxing, R.drawable.diqiu, R.drawable.huoxing, R.drawable.muxing, R.drawable.tuxing }; private static final String[] starArray = {"水星", "金星", "地球", "火星", "木星", "土星"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_spinner_icon); Spinner sp_icon=findViewById(R.id.sp_icon); List<Map<String,Object>> list=new ArrayList<>(); for (int i = 0; i < iconArray.length; i++) { Map<String,Object> stringObjectMap=new HashMap<>(); stringObjectMap.put("icon",iconArray[i]); stringObjectMap.put("name",starArray[i]); list.add(stringObjectMap); } SimpleAdapter simpleAdapter=new SimpleAdapter(this,list, R.layout.item_simple, new String[]{"icon","name"}, new int[]{R.id.iv_icon,R.id.tv_name}); sp_icon.setAdapter(simpleAdapter); sp_icon.setSelection(0); sp_icon.setOnItemSelectedListener(this); } @Override public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { ToastUtil.show(this, "您选择的是" + starArray[i]); } @Override public void onNothingSelected(AdapterView<?> adapterView) { } } 3. BaseAdapter 假如现在需要设置的下拉列表为，左边是图片，右边是星球名字，右下角要附上简述，就得使用BaseAdapter这种更上层的Adapter适配器，BaseAdapter是一个抽象方法， 主要通过重写getView()方法设置下拉列表的视图信息，因此BaseAdapter拥有更高的适应性。 例子： PlaneBaseAdapter类： package com.example.highcontrolleractivity.adapter; import android.annotation.SuppressLint; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import com.example.highcontrolleractivity.R; import com.example.highcontrolleractivity.entity.Planet; import java.util.List; public class PlaneBaseAdapter extends BaseAdapter { private Context context; private List<Planet> planetList; public PlaneBaseAdapter(Context context, List<Planet> planetList) { this.context = context; this.planetList = planetList; } @Override public int getCount() { return planetList.size(); } @Override public Object getItem(int i) { return planetList.get(i); } @Override public long getItemId(int i) { return i; } @Override public View getView(int i, View view, ViewGroup viewGroup) { @SuppressLint("ViewHolder") View view1= LayoutInflater.from(context).inflate(R.layout.item_list,null); ImageView imageView=view1.findViewById(R.id.iv_icon); TextView tv_name=view1.findViewById(R.id.tv_name); TextView tv_desc=view1.findViewById(R.id.tv_desc); Planet planet=planetList.get(i); imageView.setImageResource(planet.image); tv_name.setText(planet.name); tv_desc.setText(planet.desc); return view1; } } Activity类： package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.Spinner; import com.example.highcontrolleractivity.adapter.PlaneBaseAdapter; import com.example.highcontrolleractivity.entity.Planet; import com.example.highcontrolleractivity.util.ToastUtil; import java.util.List; public class BaseAdapterAcivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener { private List<Planet> planetList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_base_adapter_acivity); Spinner sp_planet = findViewById(R.id.sp_baseAdapter); planetList = Planet.getDefaultList(); // 构建一个行星列表的适配器 PlaneBaseAdapter adapter = new PlaneBaseAdapter(this, planetList); sp_planet.setAdapter(adapter); sp_planet.setSelection(0); sp_planet.setOnItemSelectedListener(this); } @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { ToastUtil.show(this, "您选择的是" + planetList.get(position).name); } @Override public void onNothingSelected(AdapterView<?> parent) { } } plane类： package com.example.highcontrolleractivity.entity; import com.example.highcontrolleractivity.R; import java.util.ArrayList; import java.util.List; public class Planet { public int image; // 行星图标 public String name; // 行星名称 public String desc; // 行星描述 public Planet(int image, String name, String desc) { this.image = image; this.name = name; this.desc = desc; } private static int[] iconArray = {R.drawable.shuixing, R.drawable.jinxing, R.drawable.diqiu, R.drawable.huoxing, R.drawable.muxing, R.drawable.tuxing}; private static String[] nameArray = {"水星", "金星", "地球", "火星", "木星", "土星"}; private static String[] descArray = { "水星是太阳系八大行星最内侧也是最小的一颗行星，也是离太阳最近的行星", "金星是太阳系八大行星之一，排行第二，距离太阳0.725天文单位", "地球是太阳系八大行星之一，排行第三，也是太阳系中直径、质量和密度最大的类地行星，距离太阳1.5亿公里", "火星是太阳系八大行星之一，排行第四，属于类地行星，直径约为地球的53%", "木星是太阳系八大行星中体积最大、自转最快的行星，排行第五。它的质量为太阳的千分之一，但为太阳系中其它七大行星质量总和的2.5倍", "土星为太阳系八大行星之一，排行第六，体积仅次于木星" }; public static List<Planet> getDefaultList() { List<Planet> planetList = new ArrayList<Planet>(); for (int i = 0; i < iconArray.length; i++) { planetList.add(new Planet(iconArray[i], nameArray[i], descArray[i])); } return planetList; } } ConvertView复用 即使是下拉列表，在屏幕中展示的条数也是有限的，因此需要复用convertview使得屏幕能够通过拉动的形式展示更多的条数。 对上面做出优化： @Override public View getView(int i, View view, ViewGroup viewGroup) { //当一个屏幕显示的条数构建完,再需要构建就会走else那部分,复用并刷新数据 ViewHolder viewHolder; if(view==null) { view = LayoutInflater.from(context).inflate(R.layout.item_list, null); viewHolder= new ViewHolder(); viewHolder.iv_icon = view.findViewById(R.id.iv_icon); viewHolder.tv_name = view.findViewById(R.id.tv_name); viewHolder.tv_desc = view.findViewById(R.id.tv_desc); view.setTag(viewHolder); } else { viewHolder=(ViewHolder) view.getTag(); } Planet planet=planetList.get(i); viewHolder.iv_icon.setImageResource(planet.image); viewHolder.tv_name.setText(planet.name); viewHolder.tv_desc.setText(planet.desc); return view; } public static final class ViewHolder{ public ImageView iv_icon; public TextView tv_name; public TextView tv_desc; } 4.列表视图listView ListView是一个显示滚动项列表的示视图组(viewgroup)，通过使用适配器(Adapter)把这些列表项自动插入到列表中。适配器比如从一个数组或是数据库查询获取到数据，然后转换每一项成为可放入到列表的视图，一般也是联合BaseAdapter一起使用，要注意的一点是当条目事件里面存在Button等点击事件的时候，会出现条目事件冲突，因为条目点击事件会交给子容器处理，因此不生效，要在子条目视图中使用descendantFocusability="blocksDescendants">阻止下级控件的获取，解决冲突问题。 例子：设置两个checkbox，分别设置是否显示列表视图的分割线和按压背景，按压背景默认是灰色，可自定义。 package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.ListView; import com.example.highcontrolleractivity.adapter.PlaneBaseAdapter; import com.example.highcontrolleractivity.entity.Planet; import com.example.highcontrolleractivity.util.ToastUtil; import com.example.highcontrolleractivity.util.Utils; import java.util.List; public class ListViewActivity extends AppCompatActivity implements AdapterView.OnItemClickListener, CompoundButton.OnCheckedChangeListener { private ListView iv_planet; private List<Planet> planetList; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_list_view); iv_planet=findViewById(R.id.iv_planet); planetList=Planet.getDefaultList(); PlaneBaseAdapter planeBaseAdapter=new PlaneBaseAdapter(this,planetList); iv_planet.setAdapter(planeBaseAdapter); iv_planet.setOnItemClickListener(this); CheckBox ck_divider=findViewById(R.id.ck_divider); CheckBox ck_selector=findViewById(R.id.ck_selector); ck_divider.setOnCheckedChangeListener(this); ck_selector.setOnCheckedChangeListener(this); } @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { ToastUtil.show(this,"您选择的星球是"+planetList.get(i).name); } @SuppressLint("NonConstantResourceId") @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { switch (compoundButton.getId()){ case R.id.ck_divider: if(compoundButton.isChecked()){ @SuppressLint("UseCompatLoadingForDrawables") Drawable divider=getResources().getDrawable(R.color.black,getTheme()); iv_planet.setDivider(divider); iv_planet.setDividerHeight(Utils.dip2px(this,1)); } else{ iv_planet.setDivider(null); iv_planet.setDividerHeight(0); } break; case R.id.ck_selector: if(compoundButton.isChecked()){ @SuppressLint("UseCompatLoadingForDrawables") Drawable selector=getResources().getDrawable(R.drawable.list_selector,getTheme()); iv_planet.setSelector(selector); } else{ @SuppressLint("UseCompatLoadingForDrawables") Drawable selector=getResources().getDrawable(R.color.transparent,getTheme()); iv_planet.setSelector(selector); } break; } } } 5.网格视图GirdView GirdView的属性： XML中的属性 GirdView类中的设置方法 说明 horizontalSpace setHorizontalSpacing 指定网格项在水平方向的间距 verticalSpacing setVerticalSpacing 指定网格项在垂直方向的间距 numColumns setNumColumns 指定列数目 stretchMode setStretchMode 指定剩余空间的拉伸模式 columnWidth setColumnWidth 指定每列宽度 关于stretchMode的设置： XML中的属性 GirdView类中的设置方法 说明 none NO_STRETCH 不拉伸 columnWidth STRETCH_COLUMN_WIDTH 若有剩余空间，拉伸列宽挤掉空隙 spacingWidth STRETCH_SPACING 若有剩余空间，则列宽不变，把空间分配到每列间的空隙 spacingWidthUniform STRETCH_SPACING_UNIFORM 若有剩余空间，列宽不变，把空间分配到列作业间隙 例子：还是星球的例子 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".GirdViewActivity" android:orientation="vertical"> <GridView android:id="@+id/gv_planet" android:layout_width="match_parent" android:layout_height="wrap_content" android:numColumns="2" android:verticalSpacing="3dp" android:horizontalSpacing="3dp" android:columnWidth="100dp" android:stretchMode="columnWidth"/> </LinearLayout> <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:padding="5dp"> <TextView android:id="@+id/tv_name" android:layout_width="match_parent" android:layout_height="wrap_content" tools:text="地球" android:textSize="17sp" android:textColor="@color/black" android:gravity="center"/> <ImageView android:id="@+id/iv_icon" android:layout_width="match_parent" android:layout_height="100dp" android:scaleType="fitCenter" tools:src="@drawable/diqiu"/> <TextView android:id="@+id/tv_desc" android:layout_width="match_parent" android:layout_height="95dp" android:textSize="13sp" android:textColor="@color/black" android:gravity="start|top" tools:text="木星是太阳系八大行星中体积最大、自转最快的行星，排行第五。它的质量为太阳的千分之一，但为太阳系中其它七大行星质量总和的2.5倍。"/> </LinearLayout> package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.GridView; import com.example.highcontrolleractivity.adapter.PlaneBaseWithButtonAdapter; import com.example.highcontrolleractivity.entity.Planet; import com.example.highcontrolleractivity.util.ToastUtil; import java.util.List; public class GirdViewActivity extends AppCompatActivity implements AdapterView.OnItemClickListener { private GridView gridView; private List<Planet> planetList; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_gird_view); gridView = findViewById(R.id.gv_planet); planetList = Planet.getDefaultList(); PlaneBaseWithButtonAdapter planeBaseWithButtonAdapter=new PlaneBaseWithButtonAdapter(this, planetList); gridView.setAdapter(planeBaseWithButtonAdapter); gridView.setOnItemClickListener(this); } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { ToastUtil.show(this, "您选择了：" + planetList.get(position).name); } } 6.翻页视图ViewPager 同样，需要从PagetAdapter类中继承，然后重写类的方法。 例子：对星球的视图进行翻页处理，并在上方启用标签栏。 Adapater类： package com.example.highcontrolleractivity.adapter; import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import com.example.highcontrolleractivity.ViewPagerActivity; import com.example.highcontrolleractivity.entity.Planet; import java.util.ArrayList; import java.util.List; public class ImagePagerAdapter extends PagerAdapter { private final ViewPagerActivity context; private final List<Planet> planetList; private final List<ImageView> ViewList=new ArrayList<>(); public ImagePagerAdapter(ViewPagerActivity context, List<Planet> planetList) { this.context=context; this.planetList=planetList; for (Planet planet : planetList) { ImageView view=new ImageView(context); view.setImageResource(planet.image); view.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT )); ViewList.add(view); } } @Override public int getCount() { return ViewList.size(); } @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { return view==object; } @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { //添加view到container,后返回一个与view关联的对象,这里直接返回自身,与isViewFromObject方法中的object相等 ImageView view=ViewList.get(position); container.addView(view); return view; } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { container.removeView(ViewList.get(position)); } //返回文字到标签栏 @Nullable @Override public CharSequence getPageTitle(int position) { return planetList.get(position).name; } } Activity类： package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerTabStrip; import androidx.viewpager.widget.ViewPager; import android.graphics.Color; import android.os.Bundle; import android.util.TypedValue; import com.example.highcontrolleractivity.adapter.ImagePagerAdapter; import com.example.highcontrolleractivity.entity.Planet; import com.example.highcontrolleractivity.util.ToastUtil; import java.util.ArrayList; import java.util.List; public class ViewPagerActivity extends AppCompatActivity implements ViewPager.OnPageChangeListener { private ViewPager vp_content; private List<Planet> planetList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_view_pager); initPagerStrip(); initViewPager(); } //初始化翻页视图 private void initViewPager() { vp_content = findViewById(R.id.vp_content); planetList = Planet.getDefaultList(); PagerAdapter pagerAdapter= new ImagePagerAdapter(this, planetList); vp_content.setAdapter(pagerAdapter); vp_content.addOnPageChangeListener(this); vp_content.setCurrentItem(2); } //初始化翻页标签栏 private void initPagerStrip() { PagerTabStrip pagerTabStrip=findViewById(R.id.pts_tab); //此包源于androidx,相当于android的扩展,因此能够直接使用sp,不需要自己转换 pagerTabStrip.setTextSize(TypedValue.COMPLEX_UNIT_SP,20); pagerTabStrip.setTextColor(Color.BLACK); } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { ToastUtil.show(this, "您翻到的手机品牌是：" + planetList.get(position).name); } @Override public void onPageScrollStateChanged(int state) { } } 7.ViewPager设置引导页 一般进入程序后会现出现引导页，引导页面进入，原理其实就是通过Viewpager翻页的形式制作引导页，通过PageAdapter嵌入view视图。 引导页的视图： <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/iv_launch" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="fitXY"/> <RadioGroup android:id="@+id/rg_indicate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:paddingBottom="20dp" android:orientation="horizontal" /> </RelativeLayout> Activity视图： <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".SimpleLaunchActivity"> <androidx.viewpager.widget.ViewPager android:id="@+id/vp_launch" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout> Adapter类： package com.example.highcontrolleractivity.adapter; import android.os.Build; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.RadioButton; import android.widget.RadioGroup; import androidx.annotation.NonNull; import androidx.viewpager.widget.PagerAdapter; import com.example.highcontrolleractivity.R; import com.example.highcontrolleractivity.SimpleLaunchActivity; import com.example.highcontrolleractivity.util.ToastUtil; import java.util.ArrayList; import java.util.List; public class SimpleLaunchAdapter extends PagerAdapter { private final List<View> viewList=new ArrayList<>(); public SimpleLaunchAdapter(SimpleLaunchActivity context, int[] arrayPic) { for (int i = 0; i < arrayPic.length; i++) { View view= LayoutInflater.from(context).inflate(R.layout.item_launch,null); ImageView iv_launch=view.findViewById(R.id.iv_launch); RadioGroup rg_indicate=view.findViewById(R.id.rg_indicate); Button btn_start=view.findViewById(R.id.btn_start); iv_launch.setImageResource(arrayPic[i]); //每个页面的RadioGroup要有四个RadioButton for (int j = 0; j < arrayPic.length; j++) { RadioButton radioButton=new RadioButton(context); radioButton.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT )); radioButton.setPadding(10,10,10,10); rg_indicate.addView(radioButton); } //当前位置的单选按钮要高亮显示 ((RadioButton)rg_indicate.getChildAt(i)).setChecked(true); //最后一个引导页则开启开关 if(i==arrayPic.length-1){ btn_start.setVisibility(View.VISIBLE); btn_start.setOnClickListener(v->{ ToastUtil.show(context,"开启美好生活,跳入应用程序主页面"); }); } viewList.add(view); } } @Override public int getCount() { return viewList.size(); } @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { return view==object; } @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { View view=viewList.get(position); container.addView(view); return view; } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { container.removeView(viewList.get(position)); } } Activity类： package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import androidx.viewpager.widget.ViewPager; import android.annotation.SuppressLint; import android.os.Bundle; import com.example.highcontrolleractivity.adapter.SimpleLaunchAdapter; public class SimpleLaunchActivity extends AppCompatActivity { private final int[] ArrayPic=new int[]{ R.drawable.guide_bg1, R.drawable.guide_bg2, R.drawable.guide_bg3, R.drawable.guide_bg4 }; @SuppressLint({"MissingInflatedId", "LocalSuppress"}) private ViewPager vp_launch; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_simple_launch); vp_launch = findViewById(R.id.vp_launch); SimpleLaunchAdapter launchAdapter=new SimpleLaunchAdapter(this,ArrayPic); vp_launch.setAdapter(launchAdapter); } } 8.碎片Fragment 传统的Activity不能很好的处理大屏问题,急需碎片化的东西能够划区域展示内容，且有属于自己的独立可操作空间，因此诞生了Fragment，Fragment注册方式分为动态注册和静态注册： 静态注册：直接在布局文件中直接指定Fragment，动态注册：直接在代码中动态添加Fragment。 Fragment生命周期：onAttach->onCreate->OnCreateView->onViewCreate->onStart->onResume->onPause->onStop->onDestroyView->onDestory->onDetach 静态注册 当加载layout的fragment页面时，会自动加载指向的fragment类，进而加载fragment页面 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <fragment android:layout_width="match_parent" android:layout_height="60dp" android:id="@+id/fragment_static" android:name="com.example.highcontrolleractivity.fragment.StaticFragment"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="17sp" android:textColor="@color/black" android:gravity="center" android:text="fragment展示页"/> </LinearLayout> <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#ddffdd" android:padding="5dp" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="广告图片" android:textColor="@color/black" android:textSize="17sp"/> <ImageView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="4" android:src="@drawable/adv" android:scaleType="fitCenter"/> </LinearLayout> 动态注册 同样需要从FragmentAdapter继承类，重写此类承载ViewPager，与之前有所不同的是getItem需要返回的是Fragment类，所以需要重写Fragment方法， 利用onCreateView创建视图效果。 例子：之前星球的例子 主Activity类： package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerTabStrip; import androidx.viewpager.widget.ViewPager; import android.graphics.Color; import android.os.Bundle; import android.util.TypedValue; import com.example.highcontrolleractivity.adapter.DynamicFragmentAdapter; import com.example.highcontrolleractivity.entity.Planet; import java.util.List; public class FragmentDynamicActivity extends AppCompatActivity { private ViewPager vp_content; private List<Planet> planetList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fragment_dynamic); initPagerStrip(); initViewPager(); } private void initViewPager() { vp_content = findViewById(R.id.vp_content); planetList = Planet.getDefaultList(); DynamicFragmentAdapter dynamicFragmentAdapter=new DynamicFragmentAdapter(getSupportFragmentManager(),planetList); vp_content.setAdapter(dynamicFragmentAdapter); vp_content.setCurrentItem(2); } private void initPagerStrip() { PagerTabStrip pagerTabStrip=findViewById(R.id.pts_tab); //此包源于androidx,相当于android的扩展,因此能够直接使用sp,不需要自己转换 pagerTabStrip.setTextSize(TypedValue.COMPLEX_UNIT_SP,20); pagerTabStrip.setTextColor(Color.BLACK); } } FragmentAdapter类： package com.example.highcontrolleractivity.adapter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import com.example.highcontrolleractivity.entity.Planet; import com.example.highcontrolleractivity.fragment.DynamicFragment; import java.util.List; public class DynamicFragmentAdapter extends FragmentPagerAdapter { private List<Planet> planetList; public DynamicFragmentAdapter(@NonNull FragmentManager fm, List<Planet> planetList) { //将当前的fragment设置为resume状态，上一个fragment设置为start //从而通过fragment的onResume()来懒加载数据 super(fm,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); this.planetList=planetList; } @NonNull @Override public Fragment getItem(int position) { Planet planet=planetList.get(position); return DynamicFragment.newInstance(position,planet.image,planet.desc); } @Override public int getCount() { return planetList.size(); } @Nullable @Override public CharSequence getPageTitle(int position) { return planetList.get(position).name; } } DynamicFragment类： package com.example.highcontrolleractivity.adapter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import com.example.highcontrolleractivity.entity.Planet; import com.example.highcontrolleractivity.fragment.DynamicFragment; import java.util.List; public class DynamicFragmentAdapter extends FragmentPagerAdapter { private List<Planet> planetList; public DynamicFragmentAdapter(@NonNull FragmentManager fm, List<Planet> planetList) { //将当前的fragment设置为resume状态，上一个fragment设置为start //从而通过fragment的onResume()来懒加载数据 super(fm,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); this.planetList=planetList; } @NonNull @Override public Fragment getItem(int position) { Planet planet=planetList.get(position); return DynamicFragment.newInstance(position,planet.image,planet.desc); } @Override public int getCount() { return planetList.size(); } @Nullable @Override public CharSequence getPageTitle(int position) { return planetList.get(position).name; } } 主Activity的xml与之前Viewpager相同，Fragment的xml与之前地球子视图相同。 使用Fragment提高引导页的性能 之前的引导页，事实上就是将全部页面都存到了内存之中，每切换一个页面则从内存中提取，一旦页面很多，性能就十分不好， 通过Fragment搭配ViewPager，一旦发生页面切换，则相邻页面会被加载，非相邻页面会被回收，提高了启动速度，也不会造成资源的浪费。 主要Fragment代码如下，其它代码与上方例子相似： package com.example.highcontrolleractivity.fragment; import android.content.Context; import android.os.Bundle; import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.RadioButton; import android.widget.RadioGroup; import com.example.highcontrolleractivity.R; import com.example.highcontrolleractivity.util.ToastUtil; public class LaunchFragment extends Fragment { public static LaunchFragment newInstance(int count,int position,int image) { LaunchFragment fragment = new LaunchFragment(); Bundle args = new Bundle(); args.putInt("count",count); args.putInt("position",position); args.putInt("image",image); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Bundle bundle=getArguments(); Context context=getContext(); assert bundle != null; int count=bundle.getInt("count"); int image=bundle.getInt("image"); int position=bundle.getInt("position"); View view= LayoutInflater.from(context).inflate(R.layout.fragment_launch,container,false); ImageView iv_launch=view.findViewById(R.id.iv_launch); RadioGroup rg_indicate=view.findViewById(R.id.rg_indicate); Button btn_start=view.findViewById(R.id.btn_start); iv_launch.setImageResource(image); //每个页面的RadioGroup要有四个RadioButton for (int j = 0; j < count; j++) { RadioButton radioButton=new RadioButton(context); radioButton.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT )); radioButton.setPadding(10,10,10,10); rg_indicate.addView(radioButton); } //当前位置的单选按钮要高亮显示 ((RadioButton)rg_indicate.getChildAt(position)).setChecked(true); //最后一个引导页则开启开关 if(position==count-1){ btn_start.setVisibility(View.VISIBLE); btn_start.setOnClickListener(v->{ ToastUtil.show(context,"开启美好生活,跳入应用程序主页面"); }); } return view; } } 9.综合训练(记账本功能) 实现的功能： 记账功能，能够选择日期，选择收入或支出，填入事项说明，金额插入数据库 查询功能，能够根据根据，展示出当月的所有支出和收入 数据库设计： 账单表：id，date(日期)，type(支出、收入)，amount(金额)，remark(说明) 功能实现： 通过OpenDBHelper初始化数据库，并通过页面设置添加进入数据库，查询功能使用ViewPager+ListView实现，通过ViewPager+Fragment的形式 承载ListView进行实现。 实现如下： 添加账单bill_add页面： <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".BillPagerActivity"> <include layout="@layout/title_booking"/> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center|top" android:orientation="vertical" android:paddingLeft="5dp" android:paddingRight="5dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="40dp" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center|right" android:text="请选择月份：" android:textColor="@color/black" android:textSize="17sp" /> <TextView android:id="@+id/tv_month" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" android:drawableRight="@drawable/arrow_down" android:gravity="center" android:textColor="@color/black" android:textSize="17sp" tools:text="2035-05"/> </LinearLayout> <androidx.viewpager.widget.ViewPager android:id="@+id/vp_bill" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.viewpager.widget.PagerTabStrip android:id="@+id/pts_bill" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </androidx.viewpager.widget.ViewPager> </LinearLayout> </LinearLayout> bill_pager页面： <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".BillPagerActivity"> <include layout="@layout/title_booking"/> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center|top" android:orientation="vertical" android:paddingLeft="5dp" android:paddingRight="5dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="40dp" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center|right" android:text="请选择月份：" android:textColor="@color/black" android:textSize="17sp" /> <TextView android:id="@+id/tv_month" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" android:drawableRight="@drawable/arrow_down" android:gravity="center" android:textColor="@color/black" android:textSize="17sp" tools:text="2035-05"/> </LinearLayout> <androidx.viewpager.widget.ViewPager android:id="@+id/vp_bill" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.viewpager.widget.PagerTabStrip android:id="@+id/pts_bill" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </androidx.viewpager.widget.ViewPager> </LinearLayout> </LinearLayout> item_bill页面: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <TextView android:id="@+id/tv_date" android:layout_width="0dp" android:layout_height="45dp" android:layout_weight="1" android:gravity="center" android:textColor="@color/black" android:textSize="16sp" tools:text="2035-09" /> <TextView android:id="@+id/tv_remark" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" android:gravity="center" android:textColor="@color/black" android:textSize="16sp" tools:text="foods" /> <TextView android:id="@+id/tv_amount" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:textColor="@color/black" android:textSize="16sp" tools:text="200" /> </LinearLayout> fragment_bill页面: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center|top" android:orientation="vertical"> <ListView android:id="@+id/lv_bill" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout> title_booking页面： <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="50dp" xmlns:tools="http://schemas.android.com/tools" android:background="#aaaaff"> <ImageView android:id="@+id/iv_back" android:layout_width="50dp" android:layout_height="match_parent" android:layout_alignParentLeft="true" android:padding="5dp" android:scaleType="fitCenter" android:src="@drawable/ic_back"/> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="match_parent" android:textSize="20sp" android:textColor="@color/black" android:layout_centerInParent="true" android:gravity="center" tools:text="账单列表"/> <TextView android:id="@+id/tv_option" android:layout_width="wrap_content" android:layout_height="match_parent" android:textSize="17sp" android:textColor="@color/black" android:layout_alignParentEnd="true" android:layout_marginEnd="15dp" android:gravity="center" tools:text="添加账单"/> </RelativeLayout> BillInfo实体类： package com.example.highcontrolleractivity.entity; public class BillInfo { public int id; public String date; public int type; public double amount; public String remark; // 账单类型，0 收入，1 支出 public static final int BILL_TYPE_INCOME = 0; public static final int BILL_TYPE_COST = 1; @Override public String toString() { return "BillInfo{" + "id=" + id + ", date='" + date + '\'' + ", type=" + type + ", amount=" + amount + ", remark='" + remark + '\'' + '}'; } } BillDBHelper类： package com.example.highcontrolleractivity.database; import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import androidx.annotation.Nullable; import com.example.highcontrolleractivity.entity.BillInfo; import java.util.ArrayList; import java.util.List; public class BillDBHelper extends SQLiteOpenHelper { private static final String DB_NAME="bill.db"; private static final int DB_VERSION=1; private static BillDBHelper dbHelper=null; private static final String TABLE_BILLS_INFO = "bill_info"; private SQLiteDatabase mRDB=null; private SQLiteDatabase mWDB=null; public BillDBHelper(Context context) { super(context,DB_NAME,null,DB_VERSION); } public static BillDBHelper getInstance(Context context){ if(dbHelper==null){ dbHelper=new BillDBHelper(context); } return dbHelper; } public SQLiteDatabase openReadLink(){ if(mRDB==null || !mRDB.isOpen()){ mRDB=dbHelper.getReadableDatabase(); } return mRDB; } public SQLiteDatabase openWriteLink() { if (mWDB == null || !mWDB.isOpen()) { mWDB = dbHelper.getWritableDatabase(); } return mWDB; } public void closeLink() { if (mRDB != null && mRDB.isOpen()) { mRDB.close(); mRDB = null; } if (mWDB != null && mWDB.isOpen()) { mWDB.close(); mWDB = null; } } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { String sql = "CREATE TABLE IF NOT EXISTS " + TABLE_BILLS_INFO + "(_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + " date VARCHAR NOT NULL," + " type INTEGER NOT NULL," + " amount DOUBLE NOT NULL," + " remark VARCHAR NOT NULL);"; sqLiteDatabase.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { } public long save(BillInfo billInfo){ ContentValues values=new ContentValues(); values.put("date",billInfo.date); values.put("type",billInfo.type); values.put("amount",billInfo.amount); values.put("remark",billInfo.remark); return mRDB.insert(TABLE_BILLS_INFO,null,values); } @SuppressLint("Range") public List<BillInfo> queryByMonth(String yearMonth){ List<BillInfo> list=new ArrayList<>(); String sql = "select * from " + TABLE_BILLS_INFO + " where date like '" + yearMonth + "%'"; @SuppressLint("Recycle") Cursor cursor=mRDB.rawQuery(sql,null); while (cursor.moveToNext()) { BillInfo bill = new BillInfo(); bill.id = cursor.getInt(cursor.getColumnIndex("_id")); bill.date = cursor.getString(cursor.getColumnIndex("date")); bill.type = cursor.getInt(cursor.getColumnIndex("type")); bill.amount = cursor.getDouble(cursor.getColumnIndex("amount")); bill.remark = cursor.getString(cursor.getColumnIndex("remark")); list.add(bill); } return list; } } BillAddActivity类： package com.example.highcontrolleractivity; import static com.example.highcontrolleractivity.R.id.btn_save; import static com.example.highcontrolleractivity.R.id.et_remark; import static com.example.highcontrolleractivity.R.id.iv_back; import static com.example.highcontrolleractivity.R.id.rg_type; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.app.DatePickerDialog; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.DatePicker; import android.widget.EditText; import android.widget.RadioGroup; import android.widget.TextView; import com.example.highcontrolleractivity.database.BillDBHelper; import com.example.highcontrolleractivity.entity.BillInfo; import com.example.highcontrolleractivity.util.DateUtil; import com.example.highcontrolleractivity.util.ToastUtil; import java.util.Calendar; public class BillAddActivity extends AppCompatActivity implements View.OnClickListener, DatePickerDialog.OnDateSetListener { private Calendar calendar; private TextView tv_date; private BillDBHelper billDBHelper; private RadioGroup rg_type; private EditText et_remark; private EditText et_amount; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_bill_add); TextView tv_title = findViewById(R.id.tv_title); TextView tv_option=findViewById(R.id.tv_option); tv_date = findViewById(R.id.tv_date); rg_type = findViewById(R.id.rg_type); et_remark = findViewById(R.id.et_remark); et_amount = findViewById(R.id.et_amount); findViewById(btn_save).setOnClickListener(this); tv_title.setText("添加账单"); tv_option.setText("账单列表"); tv_option.setOnClickListener(this); findViewById(iv_back).setOnClickListener(this); tv_date.setOnClickListener(this); calendar = Calendar.getInstance(); String date = DateUtil.getDate(calendar); tv_date.setText(date); billDBHelper = BillDBHelper.getInstance(this); billDBHelper.openReadLink(); billDBHelper.openWriteLink(); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.tv_date: DatePickerDialog datePickerDialog=new DatePickerDialog(this,this, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)); datePickerDialog.show(); break; case R.id.btn_save: BillInfo billInfo=new BillInfo(); billInfo.date=tv_date.getText().toString(); billInfo.type=rg_type.getCheckedRadioButtonId()==R.id.rb_income ? BillInfo.BILL_TYPE_INCOME : BillInfo.BILL_TYPE_COST; billInfo.remark=et_remark.getText().toString(); billInfo.amount= Double.parseDouble(et_amount.getText().toString()); if(billDBHelper.save(billInfo)>0){ ToastUtil.show(this,"保存账单成功"); } break; case R.id.tv_option: // 跳转到添加账单页面 Intent intent = new Intent(this, BillPagerActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); break; case R.id.iv_back: // 关闭当前页面 finish(); break; } } @Override public void onDateSet(DatePicker datePicker, int year, int month, int day) { calendar.set(Calendar.YEAR,year); calendar.set(Calendar.MONTH,month); calendar.set(Calendar.DAY_OF_MONTH,day); tv_date.setText(DateUtil.getDate(calendar)); } } BillPager类： package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import androidx.viewpager.widget.PagerTabStrip; import androidx.viewpager.widget.ViewPager; import android.annotation.SuppressLint; import android.app.DatePickerDialog; import android.content.Intent; import android.os.Bundle; import android.util.TypedValue; import android.view.View; import android.widget.DatePicker; import android.widget.TextView; import com.example.highcontrolleractivity.adapter.BillPagerAdapter; import com.example.highcontrolleractivity.database.BillDBHelper; import com.example.highcontrolleractivity.util.DateUtil; import java.util.Calendar; public class BillPagerActivity extends AppCompatActivity implements View.OnClickListener, DatePickerDialog.OnDateSetListener { private Calendar calendar; private TextView tv_month; private ViewPager vp_bill; private BillDBHelper mDBHelper; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_bill_pager); TextView tv_title=findViewById(R.id.tv_title); TextView tv_option=findViewById(R.id.tv_option); tv_month = findViewById(R.id.tv_month); tv_title.setText("账单列表"); tv_option.setText("添加账单"); tv_option.setOnClickListener(this); tv_month.setOnClickListener(this); calendar = Calendar.getInstance(); tv_month.setText(DateUtil.getMonth(calendar)); findViewById(R.id.iv_back).setOnClickListener(this); mDBHelper = BillDBHelper.getInstance(this); mDBHelper.openReadLink(); mDBHelper.openWriteLink(); initViewPager(); } private void initViewPager() { PagerTabStrip pts_bill=findViewById(R.id.pts_bill); pts_bill.setTextSize(TypedValue.COMPLEX_UNIT_SP, 17); vp_bill=findViewById(R.id.vp_bill); BillPagerAdapter adapter=new BillPagerAdapter(getSupportFragmentManager(), calendar.get(Calendar.YEAR)); vp_bill.setAdapter(adapter); vp_bill.setCurrentItem(calendar.get(Calendar.MONTH)); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.tv_month: DatePickerDialog datePickerDialog=new DatePickerDialog(this,this, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)); datePickerDialog.show(); break; case R.id.tv_option: // 跳转到添加账单页面 Intent intent = new Intent(this, BillAddActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); break; case R.id.iv_back: // 关闭当前页面 finish(); break; } } @Override public void onDateSet(DatePicker datePicker, int year, int month, int day) { calendar.set(Calendar.YEAR,year); calendar.set(Calendar.MONTH,month); calendar.set(Calendar.DAY_OF_MONTH,day); tv_month.setText(DateUtil.getMonth(calendar)); vp_bill.setCurrentItem(month); } @Override protected void onDestroy() { super.onDestroy(); mDBHelper.closeLink(); } } BillPagerAdapter类： package com.example.highcontrolleractivity.adapter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import com.example.highcontrolleractivity.fragment.BillFragment; public class BillPagerAdapter extends FragmentPagerAdapter { private final int mYear; public BillPagerAdapter(@NonNull FragmentManager fm,int year) { super(fm,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); this.mYear = year; } @NonNull @Override public Fragment getItem(int position) { int month=position+1; String zeroMonth = month < 10 ? "0" + month : String.valueOf(month); String yearMonth = mYear + "-" + zeroMonth; return BillFragment.newInstance(yearMonth); } @Override public int getCount() { return 12; } @Nullable @Override public CharSequence getPageTitle(int position) { return (position+1)+"月份"; } } BillFragment类： package com.example.highcontrolleractivity.fragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ListView; import androidx.fragment.app.Fragment; import com.example.highcontrolleractivity.R; import com.example.highcontrolleractivity.adapter.BillListAdapter; import com.example.highcontrolleractivity.database.BillDBHelper; import com.example.highcontrolleractivity.entity.BillInfo; import java.util.List; public class BillFragment extends Fragment { public static BillFragment newInstance(String yearMonth){ BillFragment fragment = new BillFragment(); Bundle args = new Bundle(); args.putString("yearMonth", yearMonth); fragment.setArguments(args); return fragment; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_bill, container, false); ListView lv_bill=view.findViewById(R.id.lv_bill); BillDBHelper mDBHelper = BillDBHelper.getInstance(getContext()); Bundle arguments = getArguments(); String yearMonth = arguments.getString("yearMonth"); List<BillInfo> billInfoList = mDBHelper.queryByMonth(yearMonth); BillListAdapter adapter = new BillListAdapter(getContext(), billInfoList); lv_bill.setAdapter(adapter); return view; } } BillListAdapter类： package com.example.highcontrolleractivity.adapter; import android.annotation.SuppressLint; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; import com.example.highcontrolleractivity.R; import com.example.highcontrolleractivity.entity.BillInfo; import java.util.List; public class BillListAdapter extends BaseAdapter { private final Context mContext; private final List<BillInfo> mBillList; public BillListAdapter(Context context, List<BillInfo> billInfoList) { this.mContext = context; this.mBillList = billInfoList; } @Override public int getCount() { return mBillList.size(); } @Override public Object getItem(int position) { return mBillList.get(position); } @Override public long getItemId(int position) { return mBillList.get(position).id; } @SuppressLint("DefaultLocale") @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { holder = new ViewHolder(); convertView = LayoutInflater.from(mContext).inflate(R.layout.item_bill, null); holder.tv_date = convertView.findViewById(R.id.tv_date); holder.tv_remark = convertView.findViewById(R.id.tv_remark); holder.tv_amount = convertView.findViewById(R.id.tv_amount); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } BillInfo bill = mBillList.get(position); holder.tv_date.setText(bill.date); holder.tv_remark.setText(bill.remark); holder.tv_amount.setText(String.format("%s%d元", bill.type == 0 ? "+" : "-", (int) bill.amount)); return convertView; } public final class ViewHolder{ public TextView tv_date; public TextView tv_remark; public TextView tv_amount; } } 实现效果如下： 十五、广播 标准广播(无序广播) 收发过程分为三个步骤： 1.发送标准广播 2.定义广播接收器 3.开关广播接收器 package com.example.broadcast; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.view.View; import com.example.broadcast.receiver.StandardReceiver; public class BroadStandardActivity extends AppCompatActivity implements View.OnClickListener { private StandardReceiver standardReceiver; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_broad_standard); findViewById(R.id.bt_boardcast).setOnClickListener(this); } @Override public void onClick(View view) { //发送标准广播 Intent intent=new Intent(StandardReceiver.STANDARD_ACTION); sendBroadcast(intent); } @Override protected void onStart() { super.onStart(); standardReceiver = new StandardReceiver(); //意图过滤器 IntentFilter filter=new IntentFilter(StandardReceiver.STANDARD_ACTION); //注册接收器,注册后正常接收 registerReceiver(standardReceiver,filter); } @Override protected void onStop() { super.onStop(); //注册接收器,注销后不再接收广播 unregisterReceiver(standardReceiver); } } package com.example.broadcast.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; public class StandardReceiver extends BroadcastReceiver { public static final String STANDARD_ACTION="com.example.broadcast.standard"; @Override public void onReceive(Context context, Intent intent) { if(intent!=null && intent.getAction().equals(STANDARD_ACTION)){ Log.d("广播:","收到一条广播"); } } } 有序广播 多个接收器接收广播的先后顺序规则： 优先级越大的接收器，越早收到有序广播 优先级相同的时候，越早注册的接收器越早收到有序广播 当然，先接收到的接收者，即可让其它接收器继续接收，也可中断广播不让其它接收者接收 package com.example.broadcast; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.view.View; import com.example.broadcast.receiver.OrderAReceiver; import com.example.broadcast.receiver.OrderBReceiver; public class BroadOrderActivity extends AppCompatActivity implements View.OnClickListener { public final static String ORDER_ACTION="com.example.broadcast.order"; private OrderAReceiver orderAReceiver; private OrderBReceiver orderBReceiver; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_broad_order); findViewById(R.id.btn_orderboardcast).setOnClickListener(this); } @Override public void onClick(View view) { Intent intent=new Intent(ORDER_ACTION); sendOrderedBroadcast(intent,null); } @Override protected void onStart() { super.onStart(); orderAReceiver = new OrderAReceiver(); IntentFilter Afilter=new IntentFilter(ORDER_ACTION); Afilter.setPriority(11); //设置优先级 registerReceiver(orderAReceiver,Afilter); orderBReceiver = new OrderBReceiver(); IntentFilter Bfilter=new IntentFilter(ORDER_ACTION); Bfilter.setPriority(10); registerReceiver(orderBReceiver,Bfilter); } @Override protected void onStop() { super.onStop(); unregisterReceiver(orderAReceiver); unregisterReceiver(orderBReceiver); } } package com.example.broadcast.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; import com.example.broadcast.BroadOrderActivity; public class OrderAReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if(intent!=null&intent.getAction().equals(BroadOrderActivity.ORDER_ACTION)){ abortBroadcast(); Log.d("信息:","A接收到广播"); } } } package com.example.broadcast.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; import com.example.broadcast.BroadOrderActivity; public class OrderBReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if(intent!=null&intent.getAction().equals(BroadOrderActivity.ORDER_ACTION)){ Log.d("信息:","B接收到广播"); } } } 设置了优先级,A的优先级比B的优先级高,所以A可以中断广播,结果显示B接收不到广播。 广播静态注册 静态注册与代码中直接注册差异不大，，同样需要继承BroadcastReceiver并且写onReceiver方法，但 <receiver android:name=".receiver.ShockReceiver" android:enabled="true" android:exported="true"></receiver> package com.example.broadcast.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Vibrator; import android.util.Log; public class ShockReceiver extends BroadcastReceiver { public static final String SHOCK_ACTION="com.example.broadcat.shock"; @Override public void onReceive(Context context, Intent intent) { if(intent!=null&&intent.getAction().equals(SHOCK_ACTION)){ Log.d("广播","震动"); //获取震动服务 Vibrator vb= (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); //震动多少ms vb.vibrate(500); } } } package com.example.broadcast; import androidx.appcompat.app.AppCompatActivity; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.view.View; import com.example.broadcast.receiver.ShockReceiver; public class BroadStaticActivity extends AppCompatActivity implements View.OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_broad_static); findViewById(R.id.btn_send_shock).setOnClickListener(this); } @Override public void onClick(View view) { String fullName="com.example.broadcast.receiver.ShockReceiver"; Intent intent=new Intent(ShockReceiver.SHOCK_ACTION); ComponentName componentName=new ComponentName(this,fullName); intent.setComponent(componentName); sendBroadcast(intent); } } 网络变更广播 主要通过意图过滤器android.net.conn.CONNECTIVITY_CHANGE来对网络变更服务进行广播。 制式类型类： package com.example.broadcast.util; import android.telephony.TelephonyManager; public class NetworkUtil { // 获取数据连接的制式类型 public static String getNetworkClass(int subType) { switch (subType) { case TelephonyManager.NETWORK_TYPE_GPRS: case TelephonyManager.NETWORK_TYPE_EDGE: case TelephonyManager.NETWORK_TYPE_CDMA: case TelephonyManager.NETWORK_TYPE_1xRTT: case TelephonyManager.NETWORK_TYPE_IDEN: return "2G"; case TelephonyManager.NETWORK_TYPE_UMTS: case TelephonyManager.NETWORK_TYPE_EVDO_0: case TelephonyManager.NETWORK_TYPE_EVDO_A: case TelephonyManager.NETWORK_TYPE_HSDPA: case TelephonyManager.NETWORK_TYPE_HSUPA: case TelephonyManager.NETWORK_TYPE_HSPA: case TelephonyManager.NETWORK_TYPE_EVDO_B: case TelephonyManager.NETWORK_TYPE_EHRPD: case TelephonyManager.NETWORK_TYPE_HSPAP: return "3G"; case TelephonyManager.NETWORK_TYPE_LTE: return "4G"; case TelephonyManager.NETWORK_TYPE_NR: return "5G"; default: return "未知"; } } } 接收者类： package com.example.broadcast.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.net.NetworkInfo; import android.util.Log; import com.example.broadcast.util.NetworkUtil; public class SystemnetworkReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if(intent!=null){ NetworkInfo networkInfo=intent.getParcelableExtra("networkInfo"); String text = String.format("收到一个网络变更广播，网络大类为%s，" + "网络小类为%s，网络制式为%s，网络状态为%s", networkInfo.getTypeName(), networkInfo.getSubtypeName(), NetworkUtil.getNetworkClass(networkInfo.getSubtype()), networkInfo.getState().toString()); Log.d("网络变更情况", text); } } } package com.example.broadcast; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import com.example.broadcast.receiver.SystemnetworkReceiver; public class SystemnetworkActivity extends AppCompatActivity { private SystemnetworkReceiver systemnetworkReceiver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_systemnetwork); } @Override protected void onStart() { super.onStart(); systemnetworkReceiver = new SystemnetworkReceiver(); IntentFilter filter = new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE"); registerReceiver(systemnetworkReceiver, filter); } @Override protected void onStop() { super.onStop(); unregisterReceiver(systemnetworkReceiver); } } 定时管理器 AlarmManager定时管理器： set：设置一次性定时器(安卓6.0之后，在空闲状态下，不保证会发送广播) setAndAllowWhiledle：设置一次性定时器，即使设备处于空闲状态(暗屏等)，也保证执行定时器 setRepeating：设置重复定时器，系统不保证按时发送广播 cancel：取消指定延迟的意图定时器(PendingIntent) 例子： Activity类： package com.example.broadcast; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.view.View; import com.example.broadcast.receiver.AlarmReceiver; public class AlarmActivity extends AppCompatActivity implements View.OnClickListener { private AlarmReceiver alarmReceiver; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_alarm); findViewById(R.id.btn_send_alarm).setOnClickListener(this); } @Override public void onClick(View view) { Intent intent=new Intent(alarmReceiver.ALARM_ACTION); sendBroadcast(intent); } @Override protected void onStart() { super.onStart(); alarmReceiver = new AlarmReceiver(getApplicationContext()); IntentFilter intentFilter=new IntentFilter(alarmReceiver.ALARM_ACTION); registerReceiver(alarmReceiver,intentFilter); } @Override protected void onStop() { super.onStop(); unregisterReceiver(alarmReceiver); } } 接收者类： package com.example.broadcast.receiver; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Build; import android.util.Log; public class AlarmReceiver extends BroadcastReceiver { private final Context mcontext; public final String ALARM_ACTION="com.example.broadcast.alarm"; public AlarmReceiver(Context applicationContext) { this.mcontext=applicationContext; } @Override public void onReceive(Context context, Intent intent) { if(intent!=null&&intent.getAction().equals(ALARM_ACTION)){ Log.d("信息:","收到一条闹钟广播"); } } public void sendAlarm(){ Intent intent = new Intent(ALARM_ACTION); PendingIntent pendingIntent=PendingIntent.getBroadcast(mcontext,0,intent,PendingIntent.FLAG_IMMUTABLE); //从系统服务中获取闹钟管理器 if(Build.VERSION.SDK_INT>Build.VERSION_CODES.M) { // 允许在空闲时发送广播，Android6.0之后新增的方法 AlarmManager alarmManager = (AlarmManager) mcontext.getSystemService(Context.ALARM_SERVICE); alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, 1000, pendingIntent); } else{ AlarmManager alarmManager = (AlarmManager) mcontext.getSystemService(Context.ALARM_SERVICE); alarmManager.set(AlarmManager.RTC_WAKEUP, 1000, pendingIntent); } } } 横屏竖屏切换 横屏竖屏切换会导致Activity的生命周期被打乱，即会销毁先前的Activity，重新开始一个Activity。 例子： package com.example.broadcast; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.res.Configuration; import android.os.Bundle; import android.util.Log; import android.widget.TextView; public class ChangeDirectionActivity extends AppCompatActivity { private TextView tv_monitor; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_change_direction); tv_monitor = findViewById(R.id.tv_text); Log.d("信息:","create启动"); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); switch (newConfig.orientation){ case Configuration.ORIENTATION_PORTRAIT: tv_monitor.setText("当前屏幕为竖屏方向"); break; case Configuration.ORIENTATION_LANDSCAPE: tv_monitor.setText("当前屏幕为横屏方向"); break; default: break; } } } 在通过自定义配置后，切换横竖屏不会出现Activity重新启动的效果。 画中画 package com.example.broadcast; import androidx.appcompat.app.AppCompatActivity; import android.app.PictureInPictureParams; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import android.util.Rational; public class ReturnDesktopActivity extends AppCompatActivity { private DesktopReceiver desktopReceiver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_return_desktop); desktopReceiver = new DesktopReceiver(); IntentFilter filter=new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); registerReceiver(desktopReceiver,filter); } @Override protected void onDestroy() { super.onDestroy(); unregisterReceiver(desktopReceiver); } //在进入画中画模式和退出画中画模式触发 @Override public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) { super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig); if(isInPictureInPictureMode){ Log.d("消息","进入画中画模式"); } else{ Log.d("消息","退出画中画模式"); } } private class DesktopReceiver extends BroadcastReceiver{ @Override public void onReceive(Context context, Intent intent) { if(intent!=null&&intent.getAction().equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)){ String reason=intent.getStringExtra("reason"); if(!TextUtils.isEmpty(reason)&&(reason.equals("homekey"))||reason.equals("recentapps")){ //Android8.0后可开启画中画模式 if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O &&!isInPictureInPictureMode()){ PictureInPictureParams.Builder builder=new PictureInPictureParams.Builder(); //下面10/5=2,表示画中画窗口宽度是高度两倍 Rational rational=new Rational(10,5); //进入画中画模式 builder.setAspectRatio(rational); enterPictureInPictureMode(builder.build()); } } } } } }
前言 最近在有在持续学习Java的安卓开发，不断的把知识记录下。 工具 Android Studio安装 Studio安装 SDK下载 SDK即软件开发工具包，可将App源码编译成可执行的App应用 创建项目 创建项目 这里Minimum SDK后面指的是兼容到安卓几，表示你的应用支持的最低版本，看百分比选，一般目前的都是9.0-10.0。 内置模拟器 初始化Gradle要下载半小时，内置模拟器也有十多G，默认在C盘很不爽，可以移动， 注册环境变量，然后修改下面的配置，直接把目录移动到想存的位置即可。 观察App运行日志 Android采用Log日志打印日志，各类日志划分： Log.e 表示错误信息，可能导致程序崩溃 Log.w：表示警告信息 Log.i：表示一般消息 Log.d：表示调试信息，可把程序运行变量值打印，方便跟踪调试 Log.v：表示冗余信息 真机调试 1、使用数据库连接到电脑 2、电脑上会自动安装USB存储设备驱动 3、打开手机的开发者选项并开启USB调试 4、将连接的手机设备文件传输模式，并允许计算机进行USB调试 关于安卓开发语言 App开发分为两大技术路线，分别为原生开发和混合开发，官方的编程语言包括Java和Kotlin。 混合开发渲染性能没有原生开发好，但是混合开发可以跨平台，版本更新不需要重新下载API文件。 关于C和C++，Java是解释型语言，在处理图像和音视频时性能显然就有瓶颈，而C和C++是编译型语言，会 先翻译成机器语言，在图像和音视频处理时可以调用java的JNI接口调用C和C++程序进行处理，也称NDK。 目录结构 mainfests：App运行配置文件 java子目录：存放java源代码，后面两个包存放测试用的代码 res目录下： drawable：存放图形描述和图片文件 Layout：App页面的布局文件 mipmap：App的启动图标 values：存放常量的文件，如样式风格等 Gradle Scripts目录下： build.gradle：描述项目级和模块级的App工程编译规则 proguard-rules.pro：java代码的混淆规则，保证安全性 gradle.properties：用于编译工程的命令行参数 local.properties：本地配置文件，描述开发者电脑的环境配置 settings.gradle：配置了需要编译的模块 build.gradle的文件概述解释 plugins { //使用的插件 id 'com.android.application' } android { namespace 'com.example.myapplication' compileSdk 33 //编译使用的SDK版本号,33表示使用Android13 defaultConfig { // App的包名 applicationId "com.example.myapplication" //指定App适合运行的最小的SDK版本号,28表示最小在Android9上运行 minSdk 28 //目标设备的版本号,表示App最希望在那个版本的Android上运行 targetSdk 33 //App的应用版本号 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } //跟单元测试相关 buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } //指定App编译的依赖包信息 dependencies { //指定编译的Android高版本支持库 implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.3' //指定单元测试编译用的junit版本号 testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } 清单文件 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.MyApplication" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> android:icon：App手机屏幕显示的图标 android:label：App手机屏幕显示的名称 android:allowBackup：是否允许数据备份，数据丢失时恢复应用 android:theme：指定App显示的风格 android:supportsRtl：是否支持波斯语等从右向左的文字排列顺序语言 activity应用程序组件，提供屏幕，用来交互完成某项任务 android:exported：当前Activity是否可以被另一个Application的组件启动 android:name：启动应用首先启动那个Applicaiton 简单控件即UI 一、界面显示与逻辑处理 创建新的activity页面 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/text2" /> </LinearLayout> 创建新的MainActivity类，并将类加入Mainifest package com.example.myapplication; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; public class MainActivity2 extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); } } <activity android:name=".MainActivity2"/> 设置跳转按钮 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!"/> </LinearLayout> 写跳转方法 package com.example.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tv=findViewById(R.id.tv); tv.setText("你好,世界!"); Button button=findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent=new Intent(); intent.setClass(MainActivity.this,MainActivity2.class); startActivity(intent); } }); } } 二、文本 文本大小 px ：图像元素，跟随屏幕大小和像素数量关系变化。 resolution：屏幕垂直和水平方向的像素数量。 dpi：像素密度，指屏幕上每英寸距离中有多少个像素点。 Density：指定每平方英寸含有的像素点数量。 dip/dp：长度单位，同一个单位在不同设备中有不同显示效果。 sp：专门用于设置字体，大小会根据系统的字体大小改变而改变。 dip是开发中的长度单位，最后也要转换为px，px=dip*dpi/160 例：如 320*480分辨率 3.6寸的手机: dpi=(320^2+480^2)^(1/2)/3.6=160 所以 1dp=1px 对于相同尺寸的手机，即使分辨率不同，占用屏幕比例也相同 设置文本颜色 设置文本的颜色一般可直接设置，也可以通过十六进制设置，如0xFFFFEEDD, 其中先 后十六进制顺序为透明度，红色，绿色，蓝色。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <TextView android:id="@+id/text_color" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="30sp" android:text="直接设置文本颜色" /> <TextView android:id="@+id/text_hex_color" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="30sp" android:textColor="@color/purple_200" android:text="十六进制设置文本颜色"/> <TextView android:id="@+id/text_code_color" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="30sp" android:text="代码中设置文本颜色"/> <TextView android:id="@+id/text_background" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="30sp" android:text="设置文本背景颜色" android:background="#ff00ff00"/> </LinearLayout> public class MainTextActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main_text); TextView text_color=findViewById(R.id.text_color); text_color.setTextColor(Color.GREEN); TextView text_code_color=findViewById(R.id.text_code_color); text_code_color.setTextColor(0xff00ff00); } } 视图 wrap_content：自适应大小，刚好能包含内容 match_parent：当前控件的大小和父布局的大小一样 dp：自行设置dp大小 在代码中设置视图宽高。 调用控件对象getLayoutParams方法获取控件的布局参数。 调用控件对象的setLayoutParams方法，填入修改后布局参数使之生效。 public class ViewBorderActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_view_border); TextView text_borer=findViewById(R.id.text_view_border); ViewGroup.LayoutParams params=text_borer.getLayoutParams(); params.width= Utils.dip2px(this,300); text_borer.setLayoutParams(params); } } public class Utils { //根据分辨率dp转化为像素px public static int dip2px(Context context, float dpValue){ //获取当前像素密度,1个dp对应多少个像素 float scale=context.getResources().getDisplayMetrics().density; return (int)(dpValue*scale+0.5f); } } 三、布局 线性布局LinearLayout orientation为horizontal，内部视图水平方向排列 oritentation为vertical，内部视图竖着排列 相对布局RelativeLayout 相对布局的下级视图位置由其他视图决定，确定位置参照物分两种： （1）与该视图自身评级的视图 （2）该视图的上级视图 不设定，默认在左上角 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/center" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:background="#ffffff" android:text="我在中间" android:textColor="#000000" android:textSize="10sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:background="#ffffff" android:text="我在上级左边中间" android:textColor="#000000" android:textSize="10sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#ffffff" android:layout_alignParentBottom="true" android:text="我在上级底部对齐" android:textSize="10sp" android:textColor="#000000"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toLeftOf="@id/center" android:layout_alignTop="@id/center" android:background="#ffffff" android:text="我在中间左边" android:textColor="#000000" android:textSize="10sp" /> </RelativeLayout> 网格布局GridLayout，支持多行多列的表格排列 网格布局默认从左往右，从上到下，属性如下 columnCount，指定网格列数，即每行能放多少视图 rowCount，指定网格行数，每列能放多少个视图 <?xml version="1.0" encoding="utf-8"?> <GridLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:columnCount="2" android:rowCount="2"> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:layout_height="60dp" android:text="红色" android:background="#ff0000" android:textSize="20sp" android:gravity="center" android:textColor="#000000"/> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:layout_height="60dp" android:text="橙色" android:background="#ffaa00" android:textSize="20sp" android:gravity="center" android:textColor="#000000"/> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:layout_height="60dp" android:text="绿色" android:background="#00ff00" android:textSize="20sp" android:gravity="center" android:textColor="#000000"/> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:layout_height="60dp" android:text="紫色" android:background="#660066" android:textSize="20sp" android:gravity="center" android:textColor="#000000"/> </GridLayout> 滚动视图ScrollView ScrollView，垂直方向滚动，垂直方向滚动时，layout_width属性值设置为math_parent，layout_height属性设置为wrap_content HorizontalScrollView，水平方向滚动，layout_width属性值设置为wrap_content，layout_height属性设置为match_parent <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <HorizontalScrollView android:layout_width="wrap_content" android:layout_height="200dp"> <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="horizontal"> <View android:layout_width="300dp" android:layout_height="match_parent" android:background="#00ff00"> </View> <View android:layout_width="300dp" android:layout_height="match_parent" android:background="#00aaff"> </View> </LinearLayout> </HorizontalScrollView> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="vertical"> <View android:layout_width="match_parent" android:layout_height="400dp" android:background="#ff0000"> </View> <View android:layout_width="match_parent" android:layout_height="400dp" android:background="#aa00ff"> </View> </LinearLayout> </ScrollView> </LinearLayout> 四、按钮 Button由TextView派生，区别在于： Button拥有默认按钮背景(紫色)，而TextView无背景。 Button的内部文本默认居中对齐。 Button会默认将英文字母转为大写，testAllCaps可以设置是否转换。 ImageButton显示图片的图像按钮，继承自ImageView ImageButton只能显示图片，不能显示文本 ImageButton的图像可按比例缩放,Button背景设置的图像会拉伸变形 Button只能靠背景显示一张图片，ImageButton可在前景和背景显示图片，从而实现两张图片叠加效果 例子：设置三个按钮，分别启动和禁用第三个按钮，禁用按钮需要长按才能禁用，第三个按钮点击则显示时间。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> </LinearLayout> <TextView android:id="@+id/button_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="查看测试禁用按钮的结果" android:textSize="17sp" android:textColor="#000000"/> </LinearLayout> package com.example.myapplication.utils; import java.text.SimpleDateFormat; import java.util.Date; public class DateTime { public static String getNowTime(){ SimpleDateFormat simpleDateFormat=new SimpleDateFormat("HH:mm:ss"); return simpleDateFormat.format(new Date()); } } package com.example.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; import com.example.myapplication.utils.DateTime; public class ButtonTextActivity extends AppCompatActivity { private Button button_test_click; private TextView button_view; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_button_text); Button button_begin_click=findViewById(R.id.button_begin_click); Button button_disable_click= findViewById(R.id.button_disable_click); button_test_click = findViewById(R.id.button_test_click); button_view = findViewById(R.id.button_text_view); button_begin_click.setOnClickListener(new MyOnclickListener()); button_test_click.setOnClickListener(new MyOnclickListener()); button_disable_click.setOnLongClickListener(new MyOnclickLongListener()); } class MyOnclickListener implements View.OnClickListener{ @Override public void onClick(View view) { switch (view.getId()){ case R.id.button_begin_click: button_test_click.setEnabled(true); break; case R.id.button_test_click: String time=String.format("当前时间:%s", DateTime.getNowTime()); button_view.setText(time); } } } class MyOnclickLongListener implements View.OnLongClickListener{ @Override public boolean onLongClick(View view) { button_test_click.setEnabled(false); return true; } } } 五、控件综合训练(简易计算器) Strings.xml <resources> <string name="app_name">My Application</string> <string name="text2">Activity Main</string> <string name="simple_calculator">简易计算器</string> <string name="cancel">CE</string> <string name="divide">÷</string> <string name="clear">C</string> <string name="multiply">+</string> <string name="ride">*</string> <string name="minus">-</string> <string name="nine">9</string> <string name="eight">8</string> <string name="seven">7</string> <string name="six">6</string> <string name="five">5</string> <string name="four">4</string> <string name="three">3</string> <string name="two">2</string> <string name="one">1</string> <string name="zero">0</string> <string name="reciprocal">1/X</string> <string name="doc">.</string> <string name="equal">=</string> <string name="root">√</string> </resources> dimens.xml <?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="button_font_size">30sp</dimen> <dimen name="button_height">90dp</dimen> </resources> layout.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#EEEEEE" android:orientation="vertical" android:padding="5dp"> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/simple_calculator" android:textColor="@color/black" android:gravity="center" android:textSize="30sp"/> <TextView android:id="@+id/tv_result" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/white" android:text="0" android:textColor="@color/black" android:lines="8" android:textSize="25sp" android:gravity="right|bottom"/> <GridLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:columnCount="4" android:rowCount="5"> </GridLayout> </LinearLayout> </ScrollView> </LinearLayout> 主类 package com.example.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class CalculatorActivity extends AppCompatActivity implements View.OnClickListener { private TextView tv_result; //第一个操作数 private String firstNum=""; //操作符号 private String operator=""; //第二个操作数 private String secodeNum=""; //计算结果 private String result=""; //显示的文本 private String showText=""; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_calculator); tv_result = findViewById(R.id.tv_result); findViewById(R.id.btn_cancel).setOnClickListener(this); findViewById(R.id.btn_divide).setOnClickListener(this); findViewById(R.id.btn_ride).setOnClickListener(this); findViewById(R.id.btn_clear).setOnClickListener(this); findViewById(R.id.btn_seven).setOnClickListener(this); findViewById(R.id.btn_eight).setOnClickListener(this); findViewById(R.id.btn_nine).setOnClickListener(this); findViewById(R.id.btn_multi).setOnClickListener(this); findViewById(R.id.btn_four).setOnClickListener(this); findViewById(R.id.btn_five).setOnClickListener(this); findViewById(R.id.btn_six).setOnClickListener(this); findViewById(R.id.btn_minus).setOnClickListener(this); findViewById(R.id.btn_one).setOnClickListener(this); findViewById(R.id.btn_two).setOnClickListener(this); findViewById(R.id.btn_three).setOnClickListener(this); findViewById(R.id.btn_root).setOnClickListener(this); findViewById(R.id.btn_reciprocal).setOnClickListener(this); findViewById(R.id.btn_zero).setOnClickListener(this); findViewById(R.id.btn_doc).setOnClickListener(this); findViewById(R.id.btn_equal).setOnClickListener(this); } @Override public void onClick(View view) { String inputText; inputText=((TextView) view).getText().toString(); switch (view.getId()){ case R.id.btn_cancel: if(operator.equals("")) { showText = showText.substring(0, showText.length() - 1); refreshOperate(showText); refreshText(showText); break; } if(secodeNum.equals("")&&!operator.equals("")){ showText = showText.substring(0, showText.length() - 1); CE(); refreshText(showText); break; } else{ showText = showText.substring(0, showText.length() - 1); refreshOperate(showText); refreshText(result); break; } case R.id.btn_clear: clear(); break; case R.id.btn_multi: case R.id.btn_divide: case R.id.btn_ride: case R.id.btn_minus: operator=inputText; refreshText(showText+operator); break; case R.id.btn_equal: double calculate_result=calculateFour(); refreshOperate(String.valueOf(calculate_result)); refreshText(showText+"="+result); break; //开根号 case R.id.btn_root: double sqrt_result=Math.sqrt(Double.parseDouble(firstNum)); refreshOperate(String.valueOf(sqrt_result)); refreshText(showText+"√="+result); break; //求导数 case R.id.btn_reciprocal: double reciprocal_result=1.0/Double.parseDouble(firstNum); refreshOperate(String.valueOf(reciprocal_result)); refreshText(showText+"/="+result); break; default: if (result.length()>0&&operator.equals("")){ clear(); } if(operator.equals("")){ firstNum=firstNum+inputText; } else{ secodeNum=secodeNum+inputText; } if(showText.equals("0")&&!inputText.equals(".")){ refreshText(inputText); } else { refreshText(showText + inputText); } break; } } private void cancel(){ } private void clear(){ refreshOperate(""); refreshText(""); } private double calculateFour(){ switch (operator){ case "+": return Double.parseDouble(firstNum)+Double.parseDouble(secodeNum); case "-": return Double.parseDouble(firstNum)-Double.parseDouble(secodeNum); case "*": return Double.parseDouble(firstNum)*Double.parseDouble(secodeNum); default: return Double.parseDouble(firstNum)/Double.parseDouble(secodeNum); } } private void CE(){ //7+ operator=""; } private void refreshOperate(String new_result){ result=new_result; firstNum=result; secodeNum=""; operator=""; } private void refreshText(String text){ showText=text; tv_result.setText(showText); } } 并非是什么完美计算器，仅是复习以上知识操作使用。 六、Activity 从当前页面跳转到新页面： startActivity(new intent(源页面.this,目标页面.class)) 当前页面返回上一个页面： finish(); activity的生命周期： onCreate 创建活动，把页面加载进内存，进入初始状态 onStart 开始活动，把活动页面显示到屏幕，就绪态 onResume 恢复活动，活动页面进入活跃状态 onPause 暂停活动，页面不能正常交互 onStop 停止活动，页面不在屏幕 onDestroy 销毁活动，回收系统资源，从内存清除 onRestart 重启活动，重新加载内存的页面数据 onNewIntent 重用已有的实例 Activity启动模式(栈) 默认启动模式standard，启动的Activity依照顺序被依次压入Task栈。 栈顶复用模式singleTop，栈顶Activity为我们新建的Activity，不会重新创建新的Activity。 栈内复用模式singleTask，栈内存在目标Activity实例，则将task内对应Activity实例之上的Activity出栈，并 将对应Activity置于栈顶。 全局唯一模式singleInstance，为目标Activity创建新的Task栈，新的Task栈只有这一个Activity，如 已经创建目标Activity，则不创建新的Task，将以前的Activity唤醒。 启动模式的设置：intent.setFlags()代码中启动，Mainfest.xml中动态设置launch。 例子：登录页面进行登录成功后，即使后退，也不能返回登录页面，而是直接退出程序。 package com.example.activity; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; public class LoginInputActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login_input); Button btn_login=findViewById(R.id.btn_login); btn_login.setOnClickListener(new MyOnclickListen()); } class MyOnclickListen implements View.OnClickListener{ @Override public void onClick(View view) { Intent intent=new Intent(LoginInputActivity.this,LoginSuccessActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } } } 通过设置启动模式来完成，一旦登录则会清空当前活动栈中的所有实例，然后再同时启动开辟新任务的活动栈，这样登录后，再点击返回则会直接退出程序。 Intent Intent各个组件之间信息沟通桥梁，用于组件之间通信，主要完成工作是本次通信请求从哪来，到哪去，要怎么走 发起方携带本次通信需要的数据，接收方从收到的意图中解析数据。 发起方若判断接收方处理结果，意图就要负责让接收方回应答数据。 显示Intent，直接指定来源活动与目标活动，精确匹配，三种方式 直接Intent函数中指定：new Intent() 调用意图对象：setClass() 调用意图对象setComponent() 隐式Intent，没有明确指定要跳转的目标活动，只给出一个动作字符串匹配，属于模糊匹配 通过setAction，url，setData()指定动作和数据 常见的隐式动作有： 例子1：使用隐式Intent直接进行拨号，发送短信，或跳转到其它APP package com.example.activity; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; import android.widget.Button; public class IntentActivity extends AppCompatActivity implements View.OnClickListener{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_intent); Button btn_dail=findViewById(R.id.btn_dail); Button btn_cms=findViewById(R.id.btn_cms); Button btn_my=findViewById(R.id.btn_mypage); btn_dail.setOnClickListener(this); btn_cms.setOnClickListener(this); btn_my.setOnClickListener(this); } @Override public void onClick(View view) { String phone="123456"; Intent intent=new Intent(); switch (view.getId()){ case R.id.btn_dail: intent.setAction(Intent.ACTION_DIAL); Uri uri=Uri.parse("tel:"+phone); intent.setData(uri); startActivity(intent); break; case R.id.btn_cms: intent.setAction(Intent.ACTION_SENDTO); Uri uri2=Uri.parse("smsto:"+phone); intent.setData(uri2); startActivity(intent); break; case R.id.btn_mypage: intent.setAction("android.intent.action.Aiwin"); intent.addCategory(Intent.CATEGORY_DEFAULT); startActivity(intent); break; } } } <intent-filter> <action android:name="android.intent.action.Aiwin" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> 最后跳转自己的APP，需要在mainfestxml中添加intent-filter 例子2：A页面发送信息给B页面，B页面再返回回应信息给A页面，需要通过registerForActivityResult() package com.example.activity; import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContract; 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; public class ActRequestActivity extends AppCompatActivity implements View.OnClickListener{ private ActivityResultLauncher<Intent> register; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_act_request); Button btn_send=findViewById(R.id.btn_request); TextView btn_receive=findViewById(R.id.btn_receive); btn_send.setOnClickListener(this); register = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() { @Override public void onActivityResult(ActivityResult result) { if(result!=null){ Intent intent=result.getData(); if(intent!=null&result.getResultCode()== Activity.RESULT_OK){ Bundle bundle=intent.getExtras(); String message=bundle.getString("response"); btn_receive.setText(message); } } } }); } @Override public void onClick(View view) { Intent intent=new Intent(this,ActResponseActivity.class); Bundle bundle=new Bundle(); bundle.putString("message","你睡了吗?"); intent.putExtras(bundle); register.launch(intent); } } package com.example.activity; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class ActResponseActivity extends AppCompatActivity implements View.OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_act_response); TextView request=findViewById(R.id.btn_request); Button btn_response=findViewById(R.id.btn_response); Bundle bundle=getIntent().getExtras(); String request_content=bundle.getString("message"); request.setText(request_content); btn_response.setOnClickListener(this); } @Override public void onClick(View view) { Intent intent=new Intent(); Bundle bundle=new Bundle(); bundle.putString("response","我没睡,我爸妈不在家"); intent.putExtras(bundle); setResult(ActRequestActivity.RESULT_OK,intent); finish(); } } 应用页面注册快捷方式 在安卓7以后，可以长按页面图标，弹出一些快捷方式，比如长按微信会弹出扫一扫，付款码等 点击即可直接跳转到该页面。 例子：在xml目录创建shortcuts.xml，先修改Mainfest.xml文件 <activity android:name=".ActStartActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" /> </activity> 配置shortcuts.xml文件 <shortcuts xmlns:android="http://schemas.android.com/apk/res/android"> <shortcut android:enabled="true" android:icon="@mipmap/ic_launcher" android:shortcutId="first" android:shortcutLongLabel="@string/shortcutLongLabel_first" android:shortcutShortLabel="@string/shortcutShortLabel_first"> <intent android:action="android.intent.action.VIEW" android:targetClass="com.example.activity.ActStartActivity" android:targetPackage="com.example.activity" /> <categories android:name="android.shortcut.conversation" /> </shortcut> <shortcut android:enabled="true" android:icon="@mipmap/ic_launcher" android:shortcutId="second" android:shortcutLongLabel="@string/shortcutLongLabel_second" android:shortcutShortLabel="@string/shortcutShortLabel_second"> <intent android:action="android.intent.action.VIEW" android:targetClass="com.example.activity.ActStartActivity" android:targetPackage="com.example.activity" /> <categories android:name="android.shortcut.conversation" /> </shortcut> </shortcuts> 效果： 七. 中级控件 Shape图 形状图形 Shape，形状图形的定义文件是以shape标签为根节点的XML描述文件，一般有四种类型的形状： rectangle 矩形，默认值 oval 椭圆，corners节点失效 line 直线，必须设置stroke节点 ring 圆环 其中XML中能设置的有：size尺寸，stroker描边，corners圆角，solid填充，padding间隔，gradient渐变 如设置一个椭圆形： <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="#ff66aa"/> <stroke android:width="1dp" android:color="#aaaaaa"/> </shape> 九宫格图片 在Android Stdio中，可以直接把PNG图片转成九宫格图片，利用九宫格图片，能够使图片被拉伸但是不变形， 能够用于适应不同的手机屏幕。 状态列表图形 简单来说，就是按钮被按下和没被按下显示不同的颜色或者形状等，以便于用户区分按了哪一个按钮，可通过StateListDrawable完成。状态的取值常用的有pressed、checked、focused、selected <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:drawable="@android:drawable/button_onoff_indicator_on"/> <item android:drawable="@android:drawable/button_onoff_indicator_off"/> </selector> <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> </LinearLayout> checkbox <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="5dp"> <CheckBox android:id="@+id/ck_system" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="5dp" android:text="系统的CheckBox" /> <CheckBox android:id="@+id/ck_custom" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:button="@drawable/checkbox_selector" android:padding="5dp" android:text="CheckBox换图标"/> </LinearLayout> package com.example.middlecontrolactivity; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.CheckBox; import android.widget.CompoundButton; public class CheckBoxAcivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_check_box_acivity); 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 compoundButton, boolean b) { String desc=String.format("您%s了这个CheckBox",b ? "勾选":"没勾选"); compoundButton.setText(desc); } } Switch 与checkbox使用是一致的，都是从Compuound类继承而来 textOn:设置右侧开启时文本 textOff:设置左侧关闭时文本 track:设置开关轨道的背景 thumb:设置开关标识图标 RadioButton 一般单选按钮只允许选择一个，需要放在RadioGroup里面，对RadioGroup进行监听 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="请选择你的性别" android:textColor="@color/black" android:textSize="17sp"/> <RadioGroup android:id="@+id/rg_rb" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <RadioButton android:id="@+id/rb_male" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="男"/> <RadioButton android:id="@+id/rb_female" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="女"/> </RadioGroup> <TextView android:id="@+id/tv_result" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="17sp" android:textColor="@color/black"/> </LinearLayout> package com.example.middlecontrolactivity; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.CompoundButton; import android.widget.RadioGroup; import android.widget.TextView; public class RadioButtonActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener { private TextView tv_result; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_radio_button); RadioGroup rg_rb=findViewById(R.id.rg_rb); tv_result=findViewById(R.id.tv_result); rg_rb.setOnCheckedChangeListener(this); } @Override public void onCheckedChanged(RadioGroup radioGroup, int i) { switch (i){ case R.id.rb_male: tv_result.setText("你选择的是男性"); break; case R.id.rb_female: tv_result.setText("你选择的女性"); break; } } } EditText、焦点变更事件、文本变化监听器 焦点变更事件可以对鼠标的焦点变更进行监听，比如登录要输入手机号+密码，可以对密码进行焦点监听，当输入密码时，检测手机号是否为11位，若不为，则不能输入密码。 文本变化监听器则可通过对EditText进行监听做出相应的动作，比如当手机号输够十一位后自动关闭键盘。 hint:指定提示文本的内容 maxLength:指定文件允许输入的最大长度 inputType:指定输入的文本类型 输入类型 说明 text 文本 testPassword 文本密码 number 整型数 numberSigned 带符号的数字 numberDecimal 带小数点的数字 numberPassword 数字密码 datetime 时间日期格式，数字，横线，斜杆，空格，冒号 date 日期格式，数字，横线，斜杠 time 时间格式 例子：简单的登录判断程序 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="5dp" android:orientation="vertical"> <EditText android:id="@+id/phone" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入11位手机号" android:maxLength="11" android:inputType="phone" android:textColorHint="@color/black" /> <EditText android:id="@+id/password" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入密码" android:inputType="textPassword" android:textColorHint="@color/black"/> <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="17sp"/> </LinearLayout> package com.example.middlecontrolactivity; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import com.example.middlecontrolactivity.util.ViewUtil; public class EditTextActivity extends AppCompatActivity implements View.OnClickListener, View.OnFocusChangeListener { private EditText phone; private EditText password; private TextView v; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_edit_text); v=findViewById(R.id.text); Button button = findViewById(R.id.button); phone=findViewById(R.id.phone); password=findViewById(R.id.password); button.setOnClickListener(this); password.setOnFocusChangeListener(this);//焦点变更事件 phone.addTextChangedListener(new HideTextWatcher(phone,11)); //输够位数则自动关闭键盘(文本监听) password.addTextChangedListener(new HideTextWatcher(phone,6));//输够位数则自动关闭键盘 } @Override public void onClick(View view) { String p=phone.getText().toString(); String pass=password.getText().toString(); if(p.equals("123456") && pass.equals("123456")){ v.setText("登录成功"); } else{ v.setText("登录失败"); } } @Override public void onFocusChange(View view, boolean b) { if(b){ String sure_phone=phone.getText().toString(); if(TextUtils.isEmpty(sure_phone)||sure_phone.length()<11){ phone.requestFocus(); Toast.makeText(this,"请输入11位号码",Toast.LENGTH_SHORT).show(); } } } private class HideTextWatcher implements TextWatcher{ private EditText mView; private int mMaxLength; public HideTextWatcher(EditText v, int maxLength) { mView=v; mMaxLength=maxLength; } @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void afterTextChanged(Editable editable) { String str=editable.toString(); if(str.length()==mMaxLength){ ViewUtil.hideOneInputMethod(EditTextActivity.this,mView); } } } } package com.example.middlecontrolactivity.util; import android.app.Activity; import android.content.Context; import android.view.View; import android.view.inputmethod.InputMethodManager; public class ViewUtil { public static void hideOneInputMethod(Activity act, View v){ //从系统服务中获取到输入法管理器 InputMethodManager imm= (InputMethodManager) act.getSystemService(Context.INPUT_METHOD_SERVICE); //通过token关闭屏幕上的输入法键盘 imm.hideSoftInputFromWindow(v.getWindowToken(),0); } } 提醒对话框 就是类似于卸载手机应用时的弹出的框，选择确定或者取消，分别对不同的选择进行不同的监听。 package com.example.middlecontrolactivity; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class AlertDialogActivity extends AppCompatActivity implements View.OnClickListener { private Button button; private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_alert_dialog); button=findViewById(R.id.alert_btn); textView=findViewById(R.id.text); button.setOnClickListener(this); } @Override public void onClick(View view) { AlertDialog.Builder builder=new AlertDialog.Builder(this); builder.setTitle("亲爱的用户"); builder.setMessage("你真的要卸载吗?"); builder.setPositiveButton("确定",(dialog,which)->{ textView.setText("虽然依依不舍,但是终究离开"); }); builder.setNegativeButton("再想想",(dialog,which)->{ textView.setText("我依旧会陪伴"); }); AlertDialog alertDialog=builder.create(); alertDialog.show(); } } 日期、时间对话框 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="5dp"> <DatePicker android:id="@+id/dp_date" android:layout_width="match_parent" android:layout_height="wrap_content" android:datePickerMode="spinner" android:calendarViewShown="false"/> <TextView android:id="@+id/date_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="17sp"/> </LinearLayout> package com.example.middlecontrolactivity; import androidx.appcompat.app.AppCompatActivity; import android.app.Dialog; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.DatePicker; import android.widget.TextView; import org.w3c.dom.Text; import java.util.Calendar; public class DatePickerDialog extends AppCompatActivity implements View.OnClickListener, android.app.DatePickerDialog.OnDateSetListener { private Button btn_date; private Button btn_sure; private DatePicker dp_date; private TextView date_text; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_date_picker_dialog); btn_date=findViewById(R.id.btn_date); btn_sure=findViewById(R.id.btn_sure); dp_date=findViewById(R.id.dp_date); date_text=findViewById(R.id.date_text); btn_date.setOnClickListener(this); btn_sure.setOnClickListener(this); } @Override public void onClick(View view) { switch (view.getId()){ case R.id.btn_sure: String desc=String.format("你选择的日期是%s年%s月%s日",dp_date.getYear(),dp_date.getMonth()+1,dp_date.getDayOfMonth()); date_text.setText(desc); break; case R.id.btn_date: Calendar calendar=Calendar.getInstance(); int year= calendar.get(Calendar.YEAR); int month= calendar.get(Calendar.MONTH); int day= calendar.get(Calendar.DAY_OF_MONTH); android.app.DatePickerDialog dialog=new android.app.DatePickerDialog(this,this,year,month,day); dialog.show(); break; } } @Override public void onDateSet(DatePicker datePicker, int year, int month, int day) { String desc=String.format("你选择的日期是%s年%s月%s日",year,month+1,day); date_text.setText(desc); } } 日期和时间所用的方式是一致的，只需将DatePicker改为TimePicker即可，同时DatePickerDialog改为TimePickerDialog即可。 八、综合案列(登录与找回密码) 设置两个页面，通过两种方式登录，手机号与验证码登录，并且可以跳转到另外的页面找回密码，通过Intent包裹数据进行不同页面的传输和传回， 并对手机号和密码，验证码框进行监听，达到长度则撤销键盘，达不到长度或不正确弹出提醒对话框。 UI界面涉及： login.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <RadioGroup android:id="@+id/rg_login" android:layout_width="match_parent" android:layout_height="@dimen/item_layout_height" android:orientation="horizontal"> <RadioButton android:id="@+id/rb_password" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="@string/login_by_password" android:checked="true"/> <RadioButton android:id="@+id/rb_verifycode" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="@string/login_by_verifycode"/> </RadioGroup> <LinearLayout android:layout_width="match_parent" android:layout_height="@dimen/item_layout_height" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:text="@string/phone_number" android:gravity="center" android:textColor="@color/black" android:textSize="@dimen/common_font_size"/> <EditText android:id="@+id/et_phone" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:hint="@string/input_phone_number" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" android:maxLength="11" android:textColor="@color/black" android:inputType="phone" android:textSize="@dimen/common_font_size"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="@dimen/item_layout_height" android:orientation="horizontal"> <TextView android:id="@+id/tv_password" android:layout_width="wrap_content" android:layout_height="match_parent" android:text="@string/login_password" android:gravity="center" android:textColor="@color/black" android:textSize="@dimen/common_font_size"/> <RelativeLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1"> <EditText android:id="@+id/et_password" android:layout_width="match_parent" android:layout_height="match_parent" android:hint="@string/input_password" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" android:maxLength="6" android:textColor="@color/black" android:inputType="numberPassword" android:textSize="@dimen/common_font_size"/> </RelativeLayout> </LinearLayout> <CheckBox android:id="@+id/ck_remember" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/remember_password" android:textColor="@color/black" android:textSize="@dimen/common_font_size"/> </LinearLayout> forgetpassword.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="5dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="@dimen/item_layout_height" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:text="@string/input_new_pasword" android:gravity="center" android:textColor="@color/black" android:textSize="@dimen/common_font_size"/> <EditText android:id="@+id/et_password" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:hint="@string/input_new_password" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" android:maxLength="11" android:textColor="@color/black" android:inputType="phone" android:textSize="@dimen/common_font_size"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="@dimen/item_layout_height" android:orientation="horizontal"> <TextView android:id="@+id/new_password" android:layout_width="wrap_content" android:layout_height="match_parent" android:text="@string/sure_new_password" android:gravity="center" android:textColor="@color/black" android:textSize="@dimen/common_font_size"/> <EditText android:id="@+id/sure_new_password" android:layout_width="match_parent" android:layout_height="match_parent" android:hint="@string/input_new_password_again" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" android:maxLength="6" android:textColor="@color/black" android:inputType="numberPassword" android:textSize="@dimen/common_font_size"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="@dimen/item_layout_height" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:text="@string/verifycode" android:gravity="center" android:textColor="@color/black" android:textSize="@dimen/common_font_size"/> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <EditText android:id="@+id/et_verifycode" android:layout_width="match_parent" android:layout_height="match_parent" android:hint="@string/input_verifycode" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" android:maxLength="6" android:textColor="@color/black" android:inputType="numberPassword" android:textSize="@dimen/common_font_size"/> </RelativeLayout> </LinearLayout> </LinearLayout> strings.xml: <resources> <string name="app_name">MiddleControlActivity</string> <string name="login_by_password">密码登录</string> <string name="login_by_verifycode">验证码登录</string> <string name="phone_number">手机号码:</string> <string name="input_phone_number">请输入手机号码</string> <string name="login_password">登录密码:</string> <string name="input_password">请输入密码</string> <string name="forget_password">忘记密码</string> <string name="remember_password">记住密码</string> <string name="login">登 录</string> <string name="input_new_pasword">输入新密码</string> <string name="input_new_password">请输入新密码</string> <string name="sure_new_password">确认新密码</string> <string name="input_new_password_again">请再次输入新密码</string> <string name="verifycode"> 验证码:</string> <string name="input_verifycode">请输入验证码</string> <string name="get_verifycode">获取验证码</string> <string name="done">确 定</string> </resources> dimens.xml: <?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="common_font_size">17sp</dimen> <dimen name="button_font_size">20sp</dimen> <dimen name="item_layout_height">50dp</dimen> </resources> 登录逻辑： package com.example.middlecontrolactivity; 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.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.TextView; import android.widget.Toast; import com.example.middlecontrolactivity.util.ViewUtil; import java.util.Random; public class LoginMainActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener, View.OnClickListener { private TextView tv_password; private EditText et_password; private Button btn_forget; private CheckBox ck_remember; private EditText et_phone; private RadioButton rb_password; private RadioButton rb_verifycode; private ActivityResultLauncher<Intent> register; private Button btn_login; private String mPassword="123456"; private String mVerifyCode; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login_main); RadioGroup rb_login=findViewById(R.id.rg_login); et_phone = findViewById(R.id.et_phone); tv_password=findViewById(R.id.tv_password); et_password=findViewById(R.id.et_password); btn_forget=findViewById(R.id.btn_forget); ck_remember=findViewById(R.id.ck_remember); rb_password = findViewById(R.id.rb_password); rb_verifycode=findViewById(R.id.rb_verifycode); btn_login=findViewById(R.id.btn_login); btn_login.setOnClickListener(this); et_phone.addTextChangedListener(new HideTextWatcher(et_phone,11)); et_password.addTextChangedListener(new HideTextWatcher(et_password,6)); rb_login.setOnCheckedChangeListener(this); btn_forget.setOnClickListener(this); register = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() { @Override public void onActivityResult(ActivityResult result) { Intent intent=result.getData(); if(intent!=null&&result.getResultCode()== Activity.RESULT_OK) { mPassword=intent.getStringExtra("new_password"); } } }); } @SuppressLint("NonConstantResourceId") @Override public void onCheckedChanged(RadioGroup radioGroup, int checkId) { switch (checkId){ case R.id.rb_password: tv_password.setText(R.string.login_password); et_password.setHint(R.string.input_password); btn_forget.setText(R.string.forget_password); ck_remember.setVisibility(View.VISIBLE); break; case R.id.rb_verifycode: tv_password.setText(R.string.verifycode); et_password.setHint(R.string.input_verifycode); btn_forget.setText(R.string.get_verifycode); ck_remember.setVisibility(View.GONE); break; } } @SuppressLint({"DefaultLocale", "NonConstantResourceId"}) @Override public void onClick(View view) { String phone=et_phone.getText().toString(); switch (view.getId()){ case R.id.btn_forget: if(phone.length()<11){ Toast.makeText(this,"请输入正确的手机号",Toast.LENGTH_SHORT).show(); return ; } if(rb_password.isChecked()){ //通过Intent包裹数据传递到忘记密码界面 Intent intent=new Intent(this,ForgetPasswordActivity.class); intent.putExtra("phone",phone); register.launch(intent); } else if(rb_verifycode.isChecked()){ //弹出对话框,提醒用户记住 mVerifyCode=String.format("%06d",new Random().nextInt(999999)); AlertDialog.Builder builder=new AlertDialog.Builder(this); builder.setTitle("请记住验证码"); builder.setMessage("手机号"+phone+",本次验证码是"+mVerifyCode+",请输入验证码"); builder.setPositiveButton("好的",null); AlertDialog dialog=builder.create(); dialog.show(); } break; case R.id.btn_login: if(rb_password.isChecked()){ if(!mPassword.equals(et_password.getText().toString())){ Toast.makeText(this,"请输入正确的密码",Toast.LENGTH_SHORT) .show(); return ; } loginSuccess(); } else if(rb_verifycode.isChecked()){ if(!mVerifyCode.equals(et_password.getText().toString())){ Toast.makeText(this,"请输入正确的验证码",Toast.LENGTH_SHORT); return; } loginSuccess() ; } break; } } //EditText监听器,到达位数则自动关闭键盘 private class HideTextWatcher implements TextWatcher { private EditText mView; private int mLength; public HideTextWatcher(EditText e, int MaxLength) { this.mView=e; this.mLength=MaxLength ; } @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void afterTextChanged(Editable editable) { if(editable.length()==mLength){ ViewUtil.hideOneInputMethod(LoginMainActivity.this,mView); } } } private void loginSuccess(){ String desc=String.format("您的手机号码是%s,恭喜你通过登录验证,点击确定返回",et_phone.getText().toString()); AlertDialog.Builder builder=new AlertDialog.Builder(this) ; builder.setMessage(desc); builder.setPositiveButton("确定返回", (dialogInterface, i) -> { finish(); }); builder.setNegativeButton("我再看看",null); Dialog dialog=builder.create(); dialog.show(); } } 找回密码逻辑： package com.example.middlecontrolactivity; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.Toast; import java.util.Random; public class ForgetPasswordActivity extends AppCompatActivity implements View.OnClickListener { private String mPhone; private String mVerifyCode; private EditText et_password; private EditText sure_password; private EditText et_verifycode; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_forget_password); mPhone=getIntent().getStringExtra("phone"); et_password=findViewById(R.id.et_password); sure_password=findViewById(R.id.sure_new_password); findViewById(R.id.get_verifycode).setOnClickListener(this); findViewById(R.id.confirm).setOnClickListener(this); et_verifycode=findViewById(R.id.et_verifycode); } @SuppressLint({"NonConstantResourceId", "DefaultLocale"}) @Override public void onClick(View view) { switch (view.getId()){ case R.id.get_verifycode: mVerifyCode=String.format("%06d",new Random().nextInt(999999)); AlertDialog.Builder builder=new AlertDialog.Builder(this); builder.setTitle("请记住验证码"); builder.setMessage("手机号"+mPhone+",本次验证码是"+mVerifyCode+",请输入验证码"); builder.setPositiveButton("好的",null); AlertDialog dialog=builder.create(); dialog.show(); break; case R.id.confirm: String password=et_password.getText().toString(); String password_second=sure_password.getText().toString(); String verifyCode=et_verifycode.getText().toString(); if(password.length()<6){ Toast.makeText(this,"请输入6位的密码",Toast.LENGTH_SHORT).show(); return; } if(!password.equals(password_second)){ Toast.makeText(this,"两次密码不一致",Toast.LENGTH_SHORT).show(); return; } if(!verifyCode.equals(mVerifyCode)){ Toast.makeText(this,"验证码不正确",Toast.LENGTH_SHORT).show(); return ; } Toast.makeText(this,"密码修改成功",Toast.LENGTH_SHORT).show(); Intent intent=new Intent(); intent.putExtra("new_password",password); setResult(Activity.RESULT_OK,intent); finish(); break; } } } 数据存储 一、SharedPreferences SharePreferences是Android的轻量级存储工具，采用存储结构是Key-Value的键值对方式，重量级存储的是SQLite数据库 共享参数的存储介质是符合XML规范的配置文件，保存路径是：/data/data/应用包名/shared_prefs/文件名.xml。 使用场景： 简单且孤立的数据，文本形式的数据，需持久化存储的数据(在App退出后再次启动，保存数据依旧有效)，实际开发中，经常存储用户的个性化配置信息，行为信息，临时需要保存的片段信息等。 例子：一个页面用于提交个人的姓名，年龄，身高，体重，是否结婚的信息，提交后，关闭程序再次打开信息依旧能够显示。 可以使用sharedPreferences用于存储提交过的信息，存储在xml中，再应用程序再次打开时，通过getSharedPreferences从中获取，并设置edittext的内容即可。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="5dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="姓名:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/name" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入你的姓名" android:textSize="17sp" android:textColor="@color/black"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="年龄:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/age" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入你的年龄" android:textSize="17sp" android:inputType="number" android:textColor="@color/black"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="身高:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/height" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入你的身高" android:textSize="17sp" android:inputType="number" android:textColor="@color/black"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="体重:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/weight" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入您的体重" android:inputType="number" android:textSize="17sp" android:textColor="@color/black"/> </LinearLayout> <CheckBox android:id="@+id/married" android:layout_width="match_parent" android:layout_height="wrap_content" android:checked="false" android:textColor="@color/black" android:text="已婚" android:textSize="17sp" /> </LinearLayout> package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.SharedPreferences; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.Toast; public class ShareWriteActivity extends AppCompatActivity implements View.OnClickListener { private EditText name; private EditText age; private EditText weight; private EditText height; private CheckBox married; private Button save; private SharedPreferences sharedPreferences; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_share_write); name=findViewById(R.id.name); age=findViewById(R.id.age); weight=findViewById(R.id.weight); height=findViewById(R.id.height); married=findViewById(R.id.married); save=findViewById(R.id.save); sharedPreferences = getSharedPreferences("personalInformation",MODE_PRIVATE); save.setOnClickListener(this); reload(); } public void reload(){ String re_name=sharedPreferences.getString("name",null); String re_age= String.valueOf(sharedPreferences.getInt("age",0)); String re_weight= String.valueOf(sharedPreferences.getFloat("weight",0f)); String re_height= String.valueOf(sharedPreferences.getFloat("height",0f)); boolean re_married=sharedPreferences.getBoolean("married",false); name.setText(re_name); age.setText(re_age); weight.setText(re_weight); height.setText(re_height); married.setChecked(re_married); } @Override public void onClick(View view) { String et_name=name.getText().toString(); String et_age=age.getText().toString(); String et_weight=weight.getText().toString(); String et_height=height.getText().toString(); boolean et_married=married.isChecked(); @SuppressLint("CommitPrefEdits") SharedPreferences.Editor editor=sharedPreferences.edit(); editor.putString("name",et_name); editor.putInt("age",Integer.parseInt(et_age)); editor.putFloat("weight",Float.parseFloat(et_weight)); editor.putFloat("height",Float.parseFloat(et_height)); editor.putBoolean("married",et_married); editor.apply(); Toast toast=Toast.makeText(this,"提交成功",Toast.LENGTH_SHORT); toast.show(); } } 可以看到在提交后，会在/data/data/应用程序名中生成一个xml文件。 二、SQLite SQLite与mysql几乎一致，SQLite不支持boolean类型，当输出true和false时会自动转换成0和1. 数据库的打开和删除 package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class CreateSqliteDatabase extends AppCompatActivity implements View.OnClickListener { private Button create; private Button delete; private TextView textView; private String DatabaseName; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_create_sqlite_database); create=findViewById(R.id.bt_create); delete=findViewById(R.id.bt_delete); textView=findViewById(R.id.text); create.setOnClickListener(this); delete.setOnClickListener(this); DatabaseName=getFilesDir()+"/test.db"; } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { String desc; switch (view.getId()){ case R.id.bt_create: SQLiteDatabase db=openOrCreateDatabase(DatabaseName,MODE_PRIVATE,null); desc=String.format("数据库%s创建%s",db.getPath(),(db!=null)?"成功":"失败"); textView.setText(desc); break; case R.id.bt_delete: boolean result=deleteDatabase(DatabaseName); desc=String.format("数据库%s删除%s",DatabaseName,result?"成功":"失败"); textView.setText(desc); break; } } } 数据库的增删改查 DatabaseUserHelper.java package com.example.datastorage.database; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import com.example.datastorage.entity.User; import java.util.ArrayList; import java.util.List; public class DatabaseUserHelper extends SQLiteOpenHelper { private static final String DB_NAME="user.db"; private static final String TABLE_NAME="user_info"; private static final int DB_VERSION=1; private static DatabaseUserHelper databaseUserHelper=null; public DatabaseUserHelper(Context context) { super(context,DB_NAME,null,DB_VERSION); } //单例模式获取数据库实例 public static DatabaseUserHelper getInstance(Context context){ if(databaseUserHelper==null){ databaseUserHelper=new DatabaseUserHelper(context); } return databaseUserHelper; } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { String sql= "CREATE TABLE IF NOT EXISTS "+TABLE_NAME+"(_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,name VARCHAR NOT NULL,age INTEGER NOT NULL,height LONG NOT NULL,weight float NOT NULL,married INTEGER NOT NULL);"; sqLiteDatabase.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { } public Long insert(User user){ SQLiteDatabase db=databaseUserHelper.getWritableDatabase(); ContentValues values=new ContentValues(); values.put("name",user.name); values.put("age",user.age); values.put("weight",user.weight); values.put("height",user.height); values.put("married",user.married); //如果插入的数据是空的,nullColumnHack可以给予一列列名进行插入。 return db.insert(TABLE_NAME,null,values); } public long deleteByName(String name){ SQLiteDatabase db=databaseUserHelper.getWritableDatabase(); return db.delete(TABLE_NAME, "name=?", new String[]{name}); } public long update(User user) { ContentValues values=new ContentValues(); values.put("name",user.name); values.put("age",user.age); values.put("weight",user.weight); values.put("height",user.height); values.put("married",user.married); SQLiteDatabase db = databaseUserHelper.getWritableDatabase(); return db.update(TABLE_NAME,values,"name=?",new String[]{user.name}); } public List<User> queryAll(){ List<User> list=new ArrayList<>(); SQLiteDatabase db=databaseUserHelper.getWritableDatabase(); //返回一个游标 Cursor cursor=db.query(TABLE_NAME,null,null,null,null,null,null); while(cursor.moveToNext()){ User user=new User(); user.id=cursor.getInt(0); user.name=cursor.getString(1); user.age=cursor.getInt(2); user.height=cursor.getLong(3); user.weight=cursor.getFloat(4); user.married= cursor.getInt(5) != 0; list.add(user); } return list; } } User.java类 package com.example.datastorage.entity; import androidx.annotation.NonNull; public class User { public int id; public String name; public int age; public long height; public float weight; public boolean married; public User(String name, int age, long height, float weight, boolean married) { this.name = name; this.age = age; this.height = height; this.weight = weight; this.married = married; } public User(){ } @NonNull @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", height=" + height + ", weight=" + weight + ", married=" + married + '}'; } } xml类: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="5dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="姓名:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/name" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入你的姓名" android:textSize="17sp" android:textColor="@color/black"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="年龄:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/age" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入你的年龄" android:textSize="17sp" android:inputType="number" android:textColor="@color/black"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="身高:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/height" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入你的身高" android:textSize="17sp" android:inputType="number" android:textColor="@color/black"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="体重:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/weight" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入您的体重" android:inputType="number" android:textSize="17sp" android:textColor="@color/black"/> </LinearLayout> <CheckBox android:id="@+id/married" android:layout_width="match_parent" android:layout_height="wrap_content" android:checked="false" android:textColor="@color/black" android:text="已婚" android:textSize="17sp" /> </LinearLayout> Activity类: package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import com.example.datastorage.database.DatabaseUserHelper; import com.example.datastorage.entity.User; import com.example.datastorage.util.ToastUtil; import java.util.List; public class SQLiteHelperActivity extends AppCompatActivity implements View.OnClickListener { private EditText name; private EditText age; private EditText weight; private EditText height; private CheckBox married; private Button save; private Button delete; private Button update; private Button query; private DatabaseUserHelper databaseUserHelper; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sqlite_helper); name=findViewById(R.id.name); age=findViewById(R.id.age); weight=findViewById(R.id.weight); height=findViewById(R.id.height); married=findViewById(R.id.married); save=findViewById(R.id.save); delete=findViewById(R.id.delete); update=findViewById(R.id.update); query=findViewById(R.id.query); save.setOnClickListener(this); delete.setOnClickListener(this); update.setOnClickListener(this); query.setOnClickListener(this); } @Override protected void onStart() { super.onStart(); databaseUserHelper=DatabaseUserHelper.getInstance(this); } @Override protected void onStop() { super.onStop(); //关闭数据库连接 databaseUserHelper.close(); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { String et_name=name.getText().toString(); String et_age=age.getText().toString(); String et_weight=weight.getText().toString(); String et_height=height.getText().toString(); User user=null; switch (view.getId()){ case R.id.save: user=new User(et_name, Integer.parseInt(et_age), Long.parseLong(et_height), Long.parseLong(et_weight), married.isChecked()); if(databaseUserHelper.insert(user)>0){ ToastUtil.show(this,"添加成功"); } break; case R.id.delete: if(databaseUserHelper.deleteByName(et_name)>0){ ToastUtil.show(this,"删除成功"); } case R.id.update: user=new User(et_name, Integer.parseInt(et_age), Long.parseLong(et_height), Long.parseLong(et_weight), married.isChecked()); if(databaseUserHelper.update(user)>0){ ToastUtil.show(this,"更新成功"); } case R.id.query: List<User> list=databaseUserHelper.queryAll(); for (User u :list){ Log.d("query",u.toString()); } break; } } } 事务管理 事务类 beginTransaction：开始事务 setTransactionSuccessful：设置事务成功标志 endTransaction：结束事务 事务的作用就是保证数据的一致性和完整性(避免异常和错误等导致的数据信息异常)，能让多个并发访问数据库的时候彼此不被干扰。 Onupgrade方法 onupgrade方法在数据库版本更新的时候会被执行，即上文的DB_VERSION不为1的时候，数据库在运行的时候会自动比对，不一致则会被执行。 如将版本号变为2，在upgrade中为数据库新添加字段 @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { String sql="ALTER TABLE "+TABLE_NAME+" ADD COLUMN phone VARCHAR"; sqLiteDatabase.execSQL(sql); } 九、外部存储空间 Android把外部存储分成了两块区域，一块是所有应用均可访问的公共空间，另一块是只有应用自己才可以访问的存储空间，私有空间只要应用卸载了 就消失了，但是公共空间即使应用被卸载了，数据依旧会放在里面。 例子：将姓名，身高，年龄，体重，已婚分别保存到不同的外部存储空间，然后将其读取出来，卸载应用后会发现数据依旧存在外部存储空间。 package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; import com.example.datastorage.util.FileUtil; import com.example.datastorage.util.ToastUtil; import java.io.File; import java.io.IOException; public class FileWriteActivity extends AppCompatActivity implements View.OnClickListener { private EditText name; private EditText age; private EditText weight; private EditText height; private CheckBox married; private TextView tv_text; private String path; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_file_write); name=findViewById(R.id.name); age=findViewById(R.id.age); weight=findViewById(R.id.weight); height=findViewById(R.id.height); married=findViewById(R.id.married); tv_text=findViewById(R.id.tv_text); Button save = findViewById(R.id.save); Button bt_read= findViewById(R.id.bt_read); save.setOnClickListener(this); bt_read.setOnClickListener(this); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.save: String et_name=name.getText().toString(); String et_age=age.getText().toString(); String et_weight=weight.getText().toString(); String et_height=height.getText().toString(); boolean et_married=married.isChecked(); String stringBuilder = "姓名:" + et_name + "年龄:" + et_age + "身高:" + et_height + "体重:" + et_weight + "婚否:" + (et_married ? "是" : "否"); //外部私有存储空间 String directory=null; String fileName=System.currentTimeMillis()+".txt"; directory=getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).toString(); path=directory+ File.separatorChar+fileName; //外部公共存储空间 // directory=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).toString(); // path=directory+ File.separatorChar+fileName; FileUtil.saveText(path, stringBuilder); ToastUtil.show(this,"保存成功"); Log.d("路径:",path); break; case R.id.bt_read: tv_text.setText(FileUtil.queryText(path)); break; } } } package com.example.datastorage.util; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; public class FileUtil { public static void saveText(String path,String txt) { BufferedWriter os=null; try { os=new BufferedWriter(new FileWriter(path)); os.write(txt); } catch (Exception e) { e.printStackTrace(); } finally { if(os!=null){ try{ os.close(); } catch (Exception e){ e.printStackTrace(); } } } } public static String queryText(String path){ BufferedReader is=null; StringBuilder stringBuilder=new StringBuilder(); try{ is=new BufferedReader(new FileReader(path)); String line=null; while((line=is.readLine())!=null){ stringBuilder.append(line); } } catch (IOException e) { e.printStackTrace(); } return stringBuilder.toString(); } } 注意存到外部公共空间，需要在Mainifest.xml中加入权限。 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission-sdk-23 android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission-sdk-23 android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:requestLegacyExternalStorage="true" android:theme="@style/Theme.MyApplication"> <activity android:name=".FileWriteActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> 存储卡中读取图片 Andorid的位图工具是Bitmap，App读写Bitmap可以使用性能更好的OuputStream和InputStream Android还提供BitmapFactory工具用于读取各种来源图片，方法如下： decodeResource：从资源文件中读取图片信息 decodeFile：该方法可将指定路径图片读取到Bitmap对象 decodeStream：从输入流中读取位图数据 例子：通过bitmap读取图片存储到存储卡中，然后读取到ImageView中显示。 public static void saveImage(String path, Bitmap bitmap) { FileOutputStream outputStream=null; try { outputStream=new FileOutputStream(path); //通过bitmap压缩文件输出流 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); } catch (Exception e){ e.printStackTrace(); } finally { if(outputStream!=null){ try{ outputStream.close(); } catch (Exception e){ e.printStackTrace(); } } } } public static Bitmap openImage(String path){ FileInputStream inputStream=null; Bitmap bitmap=null; try{ inputStream=new FileInputStream(path); bitmap= BitmapFactory.decodeStream(inputStream); }catch (Exception e){ e.printStackTrace(); } finally { if(inputStream!=null){ try{ inputStream.close(); } catch (Exception e){ e.printStackTrace(); } } } return bitmap; } } package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ImageView; import com.example.datastorage.util.FileUtil; import com.example.datastorage.util.ToastUtil; import java.io.File; public class ImageWriteActivity extends AppCompatActivity implements View.OnClickListener{ private ImageView imageView; private String path; @SuppressLint("MissingInflatedId") protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_image_write); Button bt_read=findViewById(R.id.bt_read); Button bt_save=findViewById(R.id.bt_save); imageView=findViewById(R.id.bt_image); bt_read.setOnClickListener(this); bt_save.setOnClickListener(this); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.bt_save: String filename=System.currentTimeMillis()+".jpeg"; path=getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + File.separatorChar +filename; Log.d("图片保存路径",path); Bitmap bitmap= BitmapFactory.decodeResource(getResources(),R.drawable.image); FileUtil.saveImage(path,bitmap); ToastUtil.show(this,"保存成功"); break; case R.id.bt_read: // imageView.setImageBitmap(FileUtil.openImage(path)); Bitmap bitmap1=BitmapFactory.decodeFile(path); imageView.setImageBitmap(bitmap1); break; } } } Application Application是Android的一大组件，在App的运行过程中有且只有一个Application对象，贯穿整个生命周期，创建在Acitivity之前。 Application有如下的三种方法： onCreate()，启动程序时调用。 onTerminate()，App退出时调用，用于模拟环境测试，不会在真实产品中回调。 OnConfigurationChange()，配置改变时调用，如竖屏变横屏。 一般用于读取一些要频繁读取的小型数据，因为存放的位置是内存，因此不能放太多太大的数据。 例子：将一个人的信息存放到Application变量中。 MyApplication.java类: package com.example.datastorage; import android.app.Application; import android.util.Log; import java.util.HashMap; public class MyApplication extends Application { private static MyApplication application; public HashMap<String,String> stringHashMap=new HashMap<>(); public static MyApplication getInstance(){ return application; } @Override public void onCreate() { super.onCreate(); application=this; Log.d("App状态:","App启动"); } } package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import java.util.Objects; public class AppWriteActivity extends AppCompatActivity implements View.OnClickListener { private EditText name; private EditText age; private EditText weight; private EditText height; private CheckBox married; private Button save; private MyApplication myApplication; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_app_write); name=findViewById(R.id.name); age=findViewById(R.id.age); weight=findViewById(R.id.weight); height=findViewById(R.id.height); married=findViewById(R.id.married); save=findViewById(R.id.save); save.setOnClickListener(this); myApplication=MyApplication.getInstance(); reload(); } private void reload() { String app_name=myApplication.stringHashMap.get("name"); if(app_name==null){ return ; } String app_age= myApplication.stringHashMap.get("age"); String app_weight=myApplication.stringHashMap.get("weight"); String app_height=myApplication.stringHashMap.get("height"); String app_married=myApplication.stringHashMap.get("married"); name.setText(app_name); age.setText(app_age); weight.setText(app_weight); height.setText(app_height); if(Objects.equals(app_married, "是")){ married.setChecked(true); } else{ married.setChecked(false); } } @Override public void onClick(View view) { String et_name=name.getText().toString(); String et_age=age.getText().toString(); String et_weight=weight.getText().toString(); String et_height=height.getText().toString(); boolean et_married=married.isChecked(); myApplication.stringHashMap.put("name",et_name); myApplication.stringHashMap.put("age",et_age); myApplication.stringHashMap.put("weight",et_weight); myApplication.stringHashMap.put("height",et_height); myApplication.stringHashMap.put("married",et_married ?"是":"否"); } } 只要程序没有被关闭，Application存放的东西就依旧存在，程序关闭后，数据被回收就不存在了。 十、Jetpack Room Room框架基于SQLite，通过注解技术极大的简化了数据库操作，减少了原来相当一部分的代码量，跟Java后端开发的Mybatis大致相同，就是可以直接使用注解完成数据库的操作。 使用Room之前要修改build.grandle模块文件，在dependencies中添加两行： implementation 'androidx.room:room-runtime:2.5.1' annotationProcessor 'androidx.room:room-compiler:2.5.1' 例子：写一个书籍数据库的增删改查 Room框架编码过程一般分为以下五步： @Entity：编写书籍信息表对应的实体类 @Dao：编写书籍信息表对应的持久化类 @Database：编写书籍信息表对应的数据库类，由RoomDatabase派生 在自定义的Application类中声明数据库唯一实例 在操作书籍信息表的地方获取数据表的持久化对象 创建数据库实体类 package com.example.datastorage.entity; import androidx.room.Entity; import androidx.room.PrimaryKey; @Entity public class Book { @PrimaryKey(autoGenerate = true) public Integer id; public String name; public String author; public String press; public String price; public Integer getId() { return id; } public void setId(Integer 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 String getPrice() { return price; } public void setPrice(String price) { this.price = price; } @Override public String toString() { return "Book{" + "id=" + id + ", name='" + name + '\'' + ", author='" + author + '\'' + ", press='" + press + '\'' + ", price='" + price + '\'' + '}'; } } 创建Dao接口 package com.example.datastorage.Dao; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.Query; import androidx.room.Update; import com.example.datastorage.entity.Book; import java.util.List; @Dao public interface BookDao { @Insert void insert(Book... books); @Delete void delete(Book...books); @Query("DELETE FROM Book") void deleteAll(); @Update int update(Book...books); @Query("SELECT *FROM Book") List<Book> queryAll(); @Query("SELECT *FROM Book where name=:name order by id desc limit 1") Book queryByName(String name); } 持久化类 package com.example.datastorage.database; import androidx.room.Database; import androidx.room.RoomDatabase; import com.example.datastorage.Dao.BookDao; import com.example.datastorage.entity.Book; //exportSchema表示是否导出数据库信息的json串(便于检查调试)，设为true需要gradle指定json的生成路径。 @Database(entities = {Book.class},version = 1,exportSchema = false) public abstract class BookDatabase extends RoomDatabase { //获得数据库某张表的持久化对象 public abstract BookDao bookDao(); } Application启动类： package com.example.datastorage; import android.app.Application; import android.util.Log; import androidx.room.Room; import com.example.datastorage.database.BookDatabase; import java.util.HashMap; public class MyApplication extends Application { private static MyApplication application; public HashMap<String,String> stringHashMap=new HashMap<>(); public static MyApplication getInstance(){ return application; } private BookDatabase bookDatabase; @Override public void onCreate() { super.onCreate(); application=this; Log.d("App状态:","App启动"); //构建书籍数据库实例,允许数据库迁移和主线程运行 bookDatabase= Room.databaseBuilder(this,BookDatabase.class, "book").addMigrations() .allowMainThreadQueries() .build(); } public BookDatabase getBookDB(){ return bookDatabase; } } 使用类： package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; import com.example.datastorage.Dao.BookDao; import com.example.datastorage.entity.Book; import com.example.datastorage.util.ToastUtil; import java.util.List; public class RoomWriteActivity extends AppCompatActivity implements View.OnClickListener { private EditText name; private EditText Author; private EditText press; private EditText price; private BookDao bookDao; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_room_write); name=findViewById(R.id.bookName); Author=findViewById(R.id.author); press=findViewById(R.id.press); price=findViewById(R.id.price); Button bt_save=findViewById(R.id.save); Button bt_query=findViewById(R.id.query); Button bt_delete=findViewById(R.id.delete); Button bt_update=findViewById(R.id.update); bt_save.setOnClickListener(this); bt_query.setOnClickListener(this); bt_delete.setOnClickListener(this); bt_update.setOnClickListener(this); bookDao=MyApplication.getInstance().getBookDB().bookDao(); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { String put_name=name.getText().toString(); String put_author=Author.getText().toString(); String put_press=press.getText().toString(); String put_price=price.getText().toString(); switch (view.getId()){ case R.id.save: Book book=new Book(); book.setName(put_name); book.setAuthor(put_author); book.setPress(put_press); book.setPrice(put_price); bookDao.insert(book); ToastUtil.show(this,"保存成功"); break; case R.id.delete: Book b2=new Book(); b2.setId(2); bookDao.delete(b2); ToastUtil.show(this,"删除成功"); break; case R.id.query: List<Book> list=bookDao.queryAll(); for(Book b : list){ Log.d("查询书籍：", b.toString()); } break; case R.id.update: Book b4=bookDao.queryByName(put_name); Book b3=new Book(); b3.setId(b4.getId()); b3.setName(put_name); b3.setAuthor(put_author); b3.setPress(put_press); b3.setPrice(put_price); bookDao.update(b3); ToastUtil.show(this,"修改成功"); break; } } } 十一、正文字数太多，开启下一篇
Sringboot2整合shiro实现登录认证、权限控制和记住我功能
前言 很久没写过文章了，最近课程设计硬性要求要使用Spring Secure或shiro其中一种安全框架，因为shiro的轻便更被广泛使用，所以学了一下shiro框架的基本使用，也来了点兴致，所以记录一下。 提示：以下是本篇文章正文内容，下面案例可供参考 一、关于Shiro Apache Shiro 是一个功能强大且易于使用的Java安全框架，为开发人员提供了一种直观而全面的身份验证，授权，加密和会话管理解决方案，提供了身份验证、授权、密码学、会话管理四大基本安全功能、还提供一些缓存、记住我、并发等多种额外功能解决一些问题。 Shiro的一些核心概念有：Realms、SecutiryManager、Subjuect。 关于Subject可直接理解为对象，用于和应用进行交互，通过Subject传入到安全管理器。 关于SecurityManger就是安全管理器，所有的安全性操作都与它进行交互，是Shiro管理的核心。 关于Realms可以理解为存储域，存储Shiro的一些安全数据等，安全管理器需要从Realms获取数据，验证是否合法。 大概就这些，学的比较粗浅，底层源码也没咋了解，直接就学了如何使用。 二、功能实现 1.创建User表的数据库 实现Shiro与数据库交互，能够通过手机号+密码或邮箱+密码进行登录验证。 这里的数据库字段设置如上、Id号（主键）、name（姓名）、phone（手机号）、username（用户名）、password（密码）、email（邮箱）、state（状态）。 2.引入依赖 pom.xml文件如下： <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.4</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>shiroDemo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>shiroDemo</name> <description>shiroDemo</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.17</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.10.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.10.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project> 3.配置application.yaml文件 当然也可以配置application.properties，个人习惯使用yaml，这里配置mybatis和连接mysql数据库，如下： spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/teamwork?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root type: com.alibaba.druid.pool.DruidDataSource mybatis: #config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置 mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件位置 configuration: map-underscore-to-camel-case: true 4.完善Mybatis 创建User类如下： package com.example.shirodemo.Bean; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @AllArgsConstructor @NoArgsConstructor public class User implements Serializable { private Integer id; private String name; private String phone; private String username; private String password; private String email; private Integer state; } 创建Mapper接口和UserMapper.xml文件 <?xml version="1.0" encoding="UTF-8" ?> <mapper namespace="com.example.shirodemo.mapper.UserMapper"> </mapper> package com.example.shirodemo.mapper; import com.example.shirodemo.Bean.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; @Mapper public interface UserMapper { @Select("select *from user where password=#{password} limit 1") User ShiroAuthority(String password); @Select("select password from user where phone=#{phone} ") User Login(String phone); @Select("select password from user where email=#{email}") User LoginByEmail(String email); } 这里用两个Select语句，当前端传入数据第一种方式查询不到user用户时，就进行第二种，以此来实现两种登录方式，当然也可以一个方法直接写入到XML文件中，比如下面这样。 <?xml version="1.0" encoding="UTF-8" ?> <mapper namespace="com.example.homework1.mapper.UserMapper"> select *from User <where> <if test="phone!=null and phone!=''"> and phone=#{phone} </if> <if test="email!=null and email!=''"> and email=#{email} </if> and password= #{password} </where> 5.重写Realm和Shiro配置类 因为数据库中存储的密码是用的base64+md5加密存储，所以这里要先写一个MD5的加密类，如下： package com.example.shirodemo.config; import org.springframework.util.DigestUtils; import java.util.Base64; import static com.mysql.cj.util.StringUtils.getBytes; public class MD5 { /** * @param text明文 * @param key密钥 * @return 密文 * 对数据库内的密码存储进行md5的加密 */ public static String md5(String password,String key) throws Exception { String base64encodedString = Base64.getEncoder().encodeToString((password + key).getBytes("utf-8")); // 加密后的字符串 return DigestUtils.md5DigestAsHex(getBytes(base64encodedString)); } } 重写Realm方法 package com.example.shirodemo.config; import com.example.shirodemo.Bean.User; import com.example.shirodemo.mapper.UserMapper; import com.example.shirodemo.service.UserService; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import java.util.Collections; public class MyRealm extends AuthorizingRealm { @Autowired UserService userService; @Autowired UserMapper userMapper; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { User user = (User) principals.getPrimaryPrincipal(); if (user != null) { User users = (User) userMapper.ShiroAuthority(user.getPassword()); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();//权限信息 if (users.getUsername().equals("admin")) { System.out.println(users.getName()); simpleAuthorizationInfo.addRole("admin"); } else { simpleAuthorizationInfo.addRole("user"); } return simpleAuthorizationInfo; } return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) { String username= (String) authcToken.getPrincipal(); // 获取用户密码 User user; user = userService.Login(username); if (user == null) { // 没找到帐号 user =userService.LoginByEmail(username); if(user==null) throw new UnknownAccountException(); } // 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配 return new SimpleAuthenticationInfo(user, user.getPassword(),getName()); } } 这里主要重写两个方法，第一个方法用于进行权限验证，这里用不到，下面一个方法用于进行登录验证，主要从Token中取得用户名，然后是否存在此用户，不存在则报错，存在则交给CredentialsMatcher进行密码进行密码匹配，principals用于控制角色，通过Autoken中的对象查询数据库的用户名，判断是否是admin，添加角色用于判断。 ShiroConfiguration类 package com.example.shirodemo.config; import org.apache.shiro.codec.Base64; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.CookieRememberMeManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import java.util.LinkedHashMap; import java.util.Map; @Configuration public class ShiroConfiguration { @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * Shiro生命周期处理器 * @return */ @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){ return new LifecycleBeanPostProcessor(); } /** * 自动创建代理 * @return */ @Bean @DependsOn({"lifecycleBeanPostProcessor"}) public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } /** 解决MyRealm中UserMapper一直是null的问题 **/ @Bean MyRealm myRealm() { return new MyRealm(); } /** *安全管理器 * **/ @Bean SecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myRealm()); manager.setRememberMeManager(rememberMeManager()); manager.setSessionManager(sessionManager()); return manager; } @Bean ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); //指定 SecurityManager bean.setSecurityManager(securityManager()); //登录页面 bean.setLoginUrl("/login"); //登录成功页面 bean.setSuccessUrl("/"); //访问未获授权路径时跳转的页面 bean.setUnauthorizedUrl("/login"); //配置路径拦截规则，注意，要有序 Map<String, String> map = new LinkedHashMap<>(); map.put("/doLogin", "anon"); map.put("/logout","logout"); map.put("/**", "user"); bean.setFilterChainDefinitionMap(map); return bean; } /** 但是再shiro:1.6开始，新增了一个InvalidRequestFilter的过滤器，用于拦截存在安全问题的uri，不进行配置在首次访问/时，URL中会出现jsessionid，并返回400错误 * **/ @Bean public DefaultWebSessionManager sessionManager(){ DefaultWebSessionManager sessionManager=new DefaultWebSessionManager(); sessionManager.setSessionIdUrlRewritingEnabled(false); return sessionManager; } /**设置Cookie**/ @Bean public SimpleCookie rememberMeCookie() { SimpleCookie cookie = new SimpleCookie("rememberMe"); cookie.setMaxAge(86400); return cookie; } /** * 设置cookie管理对象和Cookie的加密密钥，默认是AES加密 * @return */ @Bean public CookieRememberMeManager rememberMeManager() { CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberMeCookie()); cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag==")); return cookieRememberMeManager; } } 将logout写入filterChainDefinitionMap链中就可以自动实现注销功能，注意UserMapper类和跳转被拦截的问题即可，CookieRememberMeManager和 SimpleCookie类主要用于记住我功能后生成返回Cookie，前面三个类使@RequireRole()注解的角色控制生效。 6.前端和Controller层 Controller层——IndexController package com.example.shirodemo.Controller; import com.example.shirodemo.Bean.User; import com.example.shirodemo.config.MD5; import com.example.shirodemo.mapper.UserMapper; import com.example.shirodemo.service.UserService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @Controller public class IndexController { @GetMapping("/login") public String login(){ return "login"; } @GetMapping("/") public String index(Model model) { model.addAttribute("message", "登录成功"); return "index"; } @PostMapping("/doLogin") public String doLogin(String username, String password, boolean rememberMe,Model model){ Subject subject= SecurityUtils.getSubject(); try { String surepassword= MD5.md5(password,"Aiwin"); UsernamePasswordToken token=new UsernamePasswordToken(username, surepassword); token.setRememberMe(rememberMe); subject.login(token); model.addAttribute("message","登录成功"); return "index"; } catch (AuthenticationException e) { e.printStackTrace(); model.addAttribute("message","登录失败"); return "login"; } catch (Exception e) { e.printStackTrace(); return "login"; } } } 主要是Suject类，将接受到的用户和正确的密码提交到Token，通过与Subject类进行交互进行login，MyRealm中取得的用户名和密码可以理解为就是从这里Token交上去的。 HomeController类: package com.example.shirodemo.Controller; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.annotation.RequiresRoles; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class HomeController { @GetMapping("/home") public String home(){ return "hello"; } @RequiresRoles("admin") @GetMapping("/third") public String third(){ return "third"; } } 前端页面 Index.html: Title 注销 login.html Title 注销 跳转 跳转管理员页面 hello.html 第二个页面 这是第二个页面 third.html 第三个页面 只能管理员访问哦 至此，功能全部实现，实现效果图如下 直接关闭浏览器，然后再访问/，可以直接跳转到登录成功的页面，点击注销再访问则会跳转到登录页面，应该是通过Cookie中的rememberMe中取得user的信息进行验证判断是否通过。
数据库大作业-Python Django开发购物网站完整流程
前言 一、需求分析 1，功能设计 二、数据库设计 1，概念设计 2，逻辑设计 3，物理设计： 四，系统功能设计 1，功能层次图： 2，系统功能模块详细设计 五，技术环境 六、代码实现 目录结构图： 实现浏览: 前言 数据库项目开发，要经历需求饭呢西，概念数据，逻辑设计，物理设计，功能设计等几个阶段，学习数据库系统理论时，结合了项目的开发，可能做的很菜，但是也写篇博客总结一下。 提示：以下是本篇文章正文内容，下面案例可供参考 一、需求分析 1，功能设计 B2B的商业平台功能：注册，商品上架，需求上架，下单，购物车，用户个人中心这些增删改查的功能。 数据流图： 功能流程图： 商品上架流程图 下单商品流程图 购物车业务图 需求发布流程图 个人中心流程图 二、数据库设计 1，概念设计 E-R图： 2，逻辑设计 客服（ _ _客服ID ， _ _用户ID，商品ID ，用户姓名，商品名称,...） 用户（ _ _用户ID ，用户姓名， _ _需求ID，订单ID ,...） 需求（ _ _需求ID ，发布人， _ _订单ID ，商品名，...） 商品（ _ _商品ID ，商品名， _ _类别ID ，类别名，...） 类别（ _ _类别ID ，类别名， _ _订单ID，商品ID ，...） 订单（ _ _订单ID， _ _订单细节ID ，收货人， _ _物流ID__ ，...） 订单细节（ _ _订单细节ID ， _ _订单ID，商品ID ，商品名，...） 物流状态（ _ _物流ID ， _ _订单ID ，...） 3，物理设计： user表:存储用户的各种信息 Product:存储商品的各种信息 Request:存储需求的各种信息 Product_category:存储商品的各种类别信息 Orders:存储订单信息 Orders_detail:存储订单细节信息 Logistic:存储订单的物流状态 User表: 存储方式:通过用户的ID进行索引递增形式的存储 属性名 | 存储名 | 变量类型 | 约束条件 | 说明 ---|---|---|---|--- 用户号 | ID | Int(10) | 主键 | 姓名 | Name | Char(10) | 非空 | 账号 | Username | Char(20) | 非空 | 密码 | Password | Char(20) | 非空 | 密码干扰 | Salt | Char(5) | 非空 | 联系电话 | Phone | Char(11) | 非空 | 行业 | Business | Char(20) | 无 | 权限 | Jurisdiction | Int(1) | 非空 | 0普通用户,1客服 创建时间 | Create_time | Datetime | 非空 | | 格式:2022-02-22 12:01:01 | ---|--- 修改时间 | Upadate_time | Datetime | 非空 | 格式:2022-02-22 12:01:01 状态 | status | Int(1) | 非空 | Product表: 存储方式：聚簇存储，通过商品的类别ID进行一类一类的存储 属性名 | 存储名 | 变量类型 | 约束条件 | 说明 ---|---|---|---|--- 商品号 | id | Int(10) | 主键 | 姓名 | user_name | Char(10) | 非空 | 联系电话 | phone | Char(11) | 非空 | 商品名称 | Name | Char(20) | 非空 | 类别 | category_id | Char(10) | 非空 | 图片 | picture | Varchar(50) | 非空 | 商品描述 | description | Varchar(50) | 非空 | 发货地址 | address | Varchar(50) | 非空 | 单价 | price | Double(10,2) | 非空 | 状态 | state | Int(1) | 非空 | 0审核中,1已上架,2已下架 创建时间 | create_time | Datetime | 非空 | 格式:2022-02-22 12:01:01 修改时间 | update_time | Datetime | 非空 | 格式:2022-02-22 12:01:01 Request表: 存储方式：根据商品的类别进行聚簇存储 属性名 | 存储名 | 变量类型 | 约束条件 | 说明 ---|---|---|---|--- 需求号 | Id | Int(10) | 主键 | 发布人 | name | Char(10) | 非空 | 发布日期 | create_time | Datetime | 非空 | 格式:2022-02-22 12:01:01 地址 | address | Varchar(50) | 非空 | 商品类别 | category_id | Char(10) | 外键 | 商品名 | product_name | Char(20) | 非空 | 商品描述 | description | Varchar(50) | 无 | 联系方式 | phone | Int(11) | 非空 | 状态 | state | Int(1) | 非空 | 0取消1审核,2上架 修改时间 | update_time | datetime | 非空 | 格式:2022-02-22 12:01:01 Product_category表: 存储方式：根据商品的主键ID进行索引存储 属性名 | 存储名 | 变量类型 | 约束条件 | 说明 ---|---|---|---|--- 类别号 | id | Char(10) | 主键 | 类别名 | name | Varchar(50) | 非空 | 类别描述 | description | Varchar(50) | 无 | 创建时间 | create_time | datetime | 非空 | 格式:2022-02-22 12:01:01 orders表(索引存储): 存储方式:根据主键订单ID递增存储 属性名 | 存储名 | 变量类型 | 约束条件 | 说明 ---|---|---|---|--- 订单号 | id | Char(10) | 主键 | 收货人 | name | Char(10) | 非空 | 联系方式 | phone | Char(11) | 非空 | 订单金额 | money | Double(10,2) | 非空 | 发货地址 | saddress | Varchar(50) | 非空 | 收获地址 | daddress | Varchar(50) | 非空 | 状态 | state | Int(1) | 非空 | 0未发货,1运送中,2已完成 创建时间 | create_time | datetime | 非空 | 格式:2022-02-22 12:01:01 修改时间 | update_time | datetime | 非空 | 格式:2022-02-22 12:01:01 Orders_detail表(聚簇存储): 存储方式：订单号进行细节号递增方式和订单号进行一类存储 属性名 | 存储名 | 变量类型 | 约束条件 | 说明 ---|---|---|---|--- 细节号 | id | Char(10) | 主键 | 订单号 | orders_id | Char(10) | 外键 | 商品号 | Product_id | Int(10) | 外键 | 商品名称 | name | char(20) | 非空 | 单价 | price | Double(10,2) | 非空 | 数量 | quantity | Int(10) | 非空 | logistics表(索引存储): 存储方式:根据订单号进行聚簇一类的存储 属性名 | 存储名 | 变量类型 | 约束条件 | 说明 ---|---|---|---|--- 物流号 | Id | Char(10) | 主键 | 订单号 | orders_id | Char(10) | 外键 | 已到地 | address | Varchar(50) | 非空 | 状态 | state | Int(1) | 非空 | 0运输中,1已到达 创建时间 | Create_time | datetime | 非空 | 格式:2022-02-22 12:01:01 修改时间 | Update_time | datetime | 非空 | 格式:2022-02-22 12:01:01 Django_session表(自动生成): 属性名 | 存储名 | 变量类型 | 约束条件 | 说明 ---|---|---|---|--- 密钥 | Session_key | Varchar(40) | 非空 | 数据 | Session_data | longtext | 非空 | 时间 | Expire_date | Datetime(6) | 非空 | 四，系统功能设计 1，功能层次图： **2，系统功能模块详细设计** 功能:注册，登录,商品模块(商品上架,商品下架,商品修改,商品显示),需求模块(需求上架,需求取消,需求修改,需求显示),订单模块(购买商品,购物车,订单修改,订单显示)。 PIO图: 模块: | 用户输入注册信息 ---|--- 模块编号: | 001 被调用模块: | 用户登录 调用模块: | 用户注册 输入: | 用户的注册信息 输出: | 用户信息记录 处理描述: | 将用户的注册信息封装记录保存 数据元素: | 用户注册的各项信息 模块: | 用户输入登录信息 ---|--- 模块编号: | 002 被调用模块: | 商品,需求,订单模块 调用模块: | 用户登录 输入: | 用户的登录信息 输出: | 用户的登录会话记录 处理描述: | 用户登录后将信息以会话封装记录 数据元素: | 用户注册保存的各项信息 模块: | 用户填写上架商品信息 ---|--- 模块编号: | 003 被调用模块: | 商品显示 调用模块: | 商品上架 输入: | 用户填写的商品信息 输出: | 用户要上架的商品单 处理描述: | 将用户填写的商品信息封装记录以待审核 数据元素: | 用户填写的各项商品信息 模块: | 用户修改商品信息 ---|--- 模块编号: | 004 被调用模块: | 商品发布 调用模块: | 商品修改 输入: | 用户填写要修改的商品信息 输出: | 修改的商品信息更新到商品单 处理描述: | 原封装记录上更新信息待审核 数据元素: | 用户的各项商品信息 模块: | 下架商品 ---|--- 模块编号: | 005 被调用模块: | 商品显示 调用模块: | 下架商品 输入: | 下架的程序指令 输出: | 将封装记录的商品信息变为不可显示 处理描述: | 有关商品信息不再在页面中显示 数据元素: | 商品的各项信息 模块: | 需求上架 ---|--- 模块编号: | 006 被调用模块: | 需求显示 调用模块: | 需求上架 输入: | 用户填写的各项需求信息 输出: | 用户需求信息记录单 处理描述: | 将用户需求信息封装记录显示 数据元素: | 用户各项需求信息 模块: | 需求修改 ---|--- 模块编号: | 007 被调用模块: | 需求显示 调用模块: | 需求修改 输入: | 用户在原需求信息基础上填写修改的信息 输出: | 用户需求信息更新的记录单 处理描述: | 更新的用户需求信息封装记录显示 数据元素: | 用户各项需求信息 模块: | 需求取消 ---|--- 模块编号: | 008 被调用模块: | 需求显示 调用模块: | 需求取消 输入: | 下架需求的指令 输出: | 将封装记录的需求信息变为不可显示 处理描述: | 不再在主页面中显示用户的需求信息 数据元素: | 各项需求信息的内容 模块: | 购物车 ---|--- 模块编号: | 009 被调用模块: | 购买商品 调用模块: | 购物车 输入: | 用户选择的各项商品和商品信息 输出: | 用户选择的商品封装会话记录 处理描述: | 将用户选择的商品记录显示 数据元素: | 用户选择的商品 模块: | 购买商品 ---|--- 模块编号: | 010 被调用模块: | 订单显示 调用模块: | 购买商品 输入: | 用户购物车中或直接下单的商品 输出: | 下单的商品封装记录于表 处理描述: | 将下单的商品记录后显示在个人页面中 数据元素: | 用户下单的各项商品信息 模块: | 订单修改 ---|--- 模块编号: | 011 被调用模块: | 订单显示 调用模块: | 订单修改 输入: | 用户修改订单中的个人信息 输出: | 修改的原订单信息封装记录 处理描述: | 将修改后订单信息显示在个人页面中 数据元素: | 用户的订单信息 模块: | 物流显示 ---|--- 模块编号: | 012 被调用模块: | 订单 调用模块: | 物流显示 输入: | 用户填写的订单地址信息和更新的物流信息 输出: | 订单信息中的物流信息 处理描述: | 将物流信息显示到页面中 数据元素: | 用户的订单信息中的物流地址信息 五，技术环境 前端:HTML, CSS, JavaScript, Ajax 后端:Python3.8,mysql，djnago3.2 六、代码实现 目录结构图： fish -initpy -asgi.py -settings.py -urls.py -wsgi.py myadmin（管理员页面） -migrations -init.py -view.py -init.py -category.py(类别） -index.py（主页） -orders.py（订单） -product.py（商品） -request.py（需求） -user.py（用户) -init.py -admin.py -apps.py -models.py -shopmiddleware.py(中间件） -urls.py -test.py web -migrations.py -init.py -views.py -init.py -car.py -index.py -chat.py -orders.py -user_center_index.py -user_center_order.py -user_center_product.py -user_center_request.py -init.py -admin.py -apps.py -models.pu -urls.py -test.py static: myadmin -category: add.html，edit.html，index.html，info.html -index:index.html，login.html -orders:detail.html，edit.html，index.html，info.html，logistic.html -product:add.html，edit.html，index.html，info.html -request:add.html，edit.html，index.html，info.html -user:add.html，edit.html，index.html，info.html base.html，info.html web -chat: admin_index.html，index.html -index:index.html，info.html，login.html，orders.html，register.html user_center -index:edit.html，index.html，info.html -orders:edit.html，index.html，info.html，datail.html，logistic.html -product:add.html，edit.html，index.html，info.html -requests:add.html，edit.html，index.html，info.html base.html manage.py 实现浏览: 源码打包: 链接：https://pan.baidu.com/s/1IqtkTZ6pt_Pc_kD8HY6hLg?pwd=4396 提取码：4396
记几道BFS，中心扩展法，动态规划，状态压缩的例题
目录 前言 一、中心扩展法 二、BFS 三、状态压缩 四、动态规划 总结 前言 在学习数据结构的时候学到了不少算法思想，但是那时候真的很难，直到现在发现有些算法思想不懂不行，所以去刷了不少题，在此记录下印象比较深的题。 提示：以下是本篇文章正文内容，下面案例可供参考 一、中心扩展法 1、个人理解：中心扩展法的思想就是在解决某个事情的时，以某点为中心，向着左右两边扩散，直至某个条件不满足或满足时再返回结果，或者以下一个点为中心继续进行相同的扩展运算。 2、例题：最长回文字符串 题目简介：在一个英文字符串L中，找出最长的回文字符串，如L="caayyhheehhbbbhhjhhyyaac"，那么它最长的回文字串是"hhbbbhh"，L的长度可能接近10的4次方，要考虑时间和空间复杂度的问题。 自己想的时候：能不能使用暴力的做法，直接遍历所有的子字符串，从所有的子字符串中判断回文的字符串，然后从回文字符串中返回长度最大的。再仔细想想，L的长度可能接近10的4次方，那么子字符串就有2^(10^4)-1种，还要判断这么多种字符串是否回文，时间消耗必然很大。 最优解法：中心扩展法 分析题目：题目包含的情况其实有以下几种 第一种：当字符串只有1个的极端情况，则直接返回本身即可 第二种：以某个字符串为中心，在不超出索引的情况下向两边扩展，假若中心点的字符与左边一个或右边一个字符相等，则中心字符串往后移一位，回文字符串长度+1。比如"abcmm"，以m为中心点时，右边字符与它相等，则mm就是一个回文字符串，中心点往后移，长度+1。 第三种：第二种情况不符合的情况下，比如"abcdca"，当从a开始为中心扩散，中心到c时，判断左边一个字符是否和右边一个字符相等，相等则回文长度+2，继续往左边和右边的第二个遍历，直到不满足情况就返回。 最终：在不超过索引的情况下，遍历字符串，以每个字符会中心，进行扩散，记录最大的回文字符串长度即可，这样即使L的长度可能接近10的4次方，也只需要遍历10^4次。 代码： def max_Palindrome_string(s): length = len(s) # 判断字符串的极端情况 if length == 1: return s elif length == 2 and s[0] != s[1]: return s[0] elif length == 2 and s[0] == s[1]: return s max_len = 1 max_str = s[0] for i in range(length): # 以每个字符为中心进行左右扩散 cur_len = 1 # 记录当前回文字符串的长度 right = i + 1 # 右边字符 left = i - 1 # 左边字符 while left >= 0 and s[i] == s[left]: # 第二种情况,当前字符和左边一个相等或者右边一个相等,则往当前字符往后移一个,回文字符长度+1 cur_len += 1 left -= 1 while right < length and s[i] == s[right]: cur_len += 1 right += 1 while right < length and left >= 0 and s[right] == s[left]: # 第三种情况,当第二种情况不相等,左边字符等于右边字符,回文字符长度+2 cur_len = cur_len + 2 right = right + 1 left = left - 1 left += 1 # 不满足循环后left+1,记录回文字符字串最左边的标记 if cur_len > max_len: # 当前回文字符串长度与记录到的最大回文长度比较 max_len = cur_len max_str = s[left:left + max_len] # 返回最大的回文字符串 return max_str if __name__ == '__main__': string = 'caayyhheehhbbbhhjhhyyaac' print(max_Palindrome_string(string)) 二、BFS 1、个人理解：以图的路径搜索来讲，BFS就以节点的方法搜索遍历图的所有路径，返回需要的结果。 2、例题： 一马当先：马只能走'日'字形（包括旋转90°的日），现在想象一下，给你一个n行m列网格棋盘， 棋盘的左下角有一匹马，请你计算至少需要几步可以将它移动到棋盘的右上角。 题目分析：马走日，一种有多少种情况呢，左上，左下，右上，右下，日也可以是躺着的日，一共有8种情况，以二维坐标轴表示，假如马在中心(0,0)点，那么马就只能跳到(2，-1)，（1，-2），（-1，-2），（-2，-1），（-2，1），（-1，2），（1，2），（2，1）八种情况。那么就可以使用BFS思想，让马从（0，0）起跳，跳八种情况，然后将跳后的八个点作为起跳，继续每个点都跳八种情况，将所有情况搜寻出来，直至马到达右上角结束返回。 代码如下: def BFS_all_step(m, n): x = [+2, +1, -1, -2, -2, -1, +1, +2] y = [-1, -2, -2, -1, +1, +2, +2, +1] temp = [set(), {(0, 0)}, set()] # 分别代表上一跳,当前跳,下一跳 t = 1 # 记录移动的步数 f = True while f: f = False for loc in temp[1]: # 从当前跳为起跳点,遍历跳上述的八种情况 for i in range(8): # 跳八种可能的情况 x = loc[0] + x[i] y = loc[1] + y[i] if (x, y) == (m, n): # 如果刚好跳到右上角,则结束返回 return t elif 0 <= x <= m and 0 <= y <= n and not ( (x, y) in temp[0] or (x, y) in temp[1] or (x, y) in temp[2]): # 还在棋盘内并且确保跳的路径不重复 temp[2].add((x, y)) # 记录下一跳的所有结果 f = True t += 1 # 还没跳到右上角,步数+1 del temp[0] # 将下一跳的结果变成当前跳的位置,当前跳变成上一跳的结果 temp.append(set()) # 将下一跳设置为空 return -1 # 所有情况都没有达到右上角，返回-1 if __name__ == '__main__': m = 5 n = 3 print(BFS_all_step(m, n)) 三、状态压缩 1、个人理解：状态压缩的思想指的是按照意思先定义一个初始满足条件的值，然后对值进行压缩，全部压缩结束后，判断被压 2、例题： 正方形拼接：现在有一堆木棒，告诉你它们的长度，判断能否用这些木棒拼接成正方形。 注意：所有的木棒都要用上，且不能截断。 给你一个正整数list L, 如 L=[1,1,1,1], L中的每个数字代表一个木棒的长度，如果这些 木棒能够拼成一个正方形， 输出Yes，否则输出No。 如L=[1,1,1,1]，则输出Yes;L=[1,1,1],则输出No。 题目分析： 首先，要符合木棍能够拼接成正方形，木棍的总长度必定是4的倍数，每条边的长度为总木棍长度/4，也就是每条边的长度是固定的，最长的木棍必定要拼接到当前最短的边中才有可能拼接成四条边相等，因此可以将四条边初始状态定为总长度/4，通过用最大的长度减去最长的木棍对初始状态进行压缩，若有边长度相同则随意选一条，直至木棍减完，所有边被压缩为0，则代表可以组成正方形。 代码如下： L = [4, 5, 3, 2, 9, 1, 3, 8, 1, 6, 5, 1] L.sort() sum_L = sum(L) if sum_L % 4 != 0 or not sum_L: # 判断木棍的是否为4个倍数，不是则不可能拼接成正方形 print("No") else: square_list = [sum_L // 4] * 4 # 建立4个数的列表，每个数长度为总长度/4 while L: square_list[3] -= L.pop() # 将最长的木棍拼接到最短的边中 square_list.sort() # 再对列表排序,便于上面求剩余最多的边 if not min(square_list): print('Yes') else: print('No') 四、动态规划 1、个人理解：动态规划大多用于求在许多解中找到最优解的一种方法，将一个问题分解成多个子问题，通过利用子问题的结果继续运算叠加，计算出最终的结果。 2、题目： 最大非连续子序列：给你一个整数list L, 如 L=[2,-3,3,50], 求L的一个非连续子序列，使其和最大，输出最大子序列的和。这里非连续子序列的定义是，子序列中任意相邻的两个数在原序列里都不相邻。例如，对于L=[2,-3,3,50]， 输出52（分析：很明显，该列表最大非连续子序列为[2,50]) 题目分析：非连续子序列相加的结果有很多，要在众多结果中求出最大的，可以使用动态规划。 也就说，要确认L[i+1]个位置的最大非连续子序列和，与L[i-1]位置前求到的最大子序列的结果与L[i+1]相加即可。 代码如下: L = [2, 3, -3, 50] L = [0] + L print(L) for i, value in enumerate(L): if i > 1: # 索引为1前的最大非连续子序列为本身,从索引为2开始算起 L[i] = L[i] + max(L[:i - 1]) # 求第L[i]个非连续子序列和,将L[i]与L[i-1]前的最大非连续子序列和相加即可 print(max(L)) 总结 许多算法听起来貌似非常简单，但当遇到具体的题目要解决，真的不容易，甚至乎看着别人的代码看半天都不懂，只能一步一步debug，弄清楚算法的每一个步骤的意思，才能很好的理解，还得努力。