記一次使用windbg排查內存泄漏的過程_網頁設計公司

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

透過選單樣式的調整、圖片的縮放比例、文字的放大及段落的排版對應來給使用者最佳的瀏覽體驗,所以不用擔心有手機版網站兩個後台的問題,而視覺效果也是透過我們前端設計師優秀的空間比例設計,不會因為畫面變大變小而影響到整體視覺的美感。

一、背景

  近期有一個項目在運行當中出現一些問題,程序順利啟動,但是觀察一陣子后發現內存使用總量在很緩慢地升高,

雖然偶爾還會往下降一些,但是總體還是不斷上升;內存運行6個小時候從33M上升到80M;

  程序存在內存泄漏是確定無疑的了,大概出問題的方向也知道,就是程序新加入一個採集協議(BACnet協議,MSTP_DLL),

但是怎麼把具體泄漏位置找出來卻非常麻煩,因為這個協議是封裝在一個C語言寫的動態庫中,想要單步調試好像不太可能,

況且源碼也不再我這裏;

  如果到此為止,推脫給其他同事找問題,那聯合調試費時不說。其他同事也身兼數職,不大可能有時間調試,

那項目推進肯定停滯;那沒辦法了,只能硬着頭皮上;網上了解一番,對於這種內存泄漏問題,比較好的處理方式就是

抓取內存快照,然後分析數據提交記錄,使用查看使用堆棧等信息;所以基於以上原因,選擇了windbg內核調試工具;

先分析一下看看,說不定可以發現問題;

 

二、windbg注意事項

1、首先要安裝對版本,即你的程序是32位還是64位,對於的windbg版本也要一致,否則會報錯;詳情了解:點擊這裏

2、需要用64位的任務管理器抓32位的dump文件,那不能直接在任務管理器右鍵“創建轉儲文件“,需要運行(C:\Windows\SysWOW64\taskmgr.exe)

3、或者直接在windbg上使用命令存儲,先附加到進程,然後使用命令:(.dump /ma c:\xxx.dmp),這樣就將快照保存在C盤了;

4、最重要的,要確保你的機器能連接外網;由於windbg的使用需要在線更新符號文件,但是這個地址剛好被國家防火牆屏蔽;

 

三、windbg必要設置

1、首先我先抓取2個內存快照文件(中間相隔一段時間),如下

 

 

2、打開windbg,設置符號下載路徑

將33.dmp直接拖進工作區即可,然後打開菜單File -> Symbol File Path

輸入地址:SRV*c:\symbols*http://msdl.microsoft.com/download/symbols

 

四、分析文件

1、分別打開兩個dmp文件,輸入命令!dumpheap -stat查看各種類型的內存分配情況

33.dmp
>.load C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll >!dumpheap -stat ..... 61f87928 2292 34012 System.RuntimeType[] 5d2dbe74 267 34176 System.Data.DataColumn 61fd75e0 668 37408 System.Reflection.RuntimePropertyInfo 61f8426c 702 48976 System.Int32[] 5d2dcc24 70 72520 System.Data.RBTree`1+Node[[System.Data.DataRow, System.Data]][] 61f883e4 1242 84456 System.Reflection.RuntimeParameterInfo 61f8839c 2045 89980 System.Signature 0a7566bc 596 92976 HG.MacamUnit.Entity.TSubSysNodes 61f82788 723 117736 System.Object[] 61f89850 8 131696 System.Int64[] 61fd8938 2792 167520 System.Reflection.RuntimeMethodInfo 007988d0 220 434392 Free 61f824e4 12187 738904 System.String 61f85c40 2138 743067 System.Byte[] 61f82c60 294 6629796 System.Char[] Total 55014 objects
80.dmp
>.load C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll
>!dumpheap -stat
.....
61f83698      876        24528 System.RuntimeType
61f84ec0      159        26472 System.Collections.Hashtable+bucket[]
61fc9020      631        27764 System.Reflection.RtFieldInfo
61f95be8       46        28392 System.Reflection.Emit.__FixupData[]
61f87928     2292        34012 System.RuntimeType[]
61fd75e0      668        37408 System.Reflection.RuntimePropertyInfo
5d2dcc24       42        43512 System.Data.RBTree`1+Node[[System.Data.DataRow, System.Data]][]
61f8426c      595        45868 System.Int32[]
61f883e4     1242        84456 System.Reflection.RuntimeParameterInfo
61f8839c     2045        89980 System.Signature
61f82788 622 113684 System.Object[] 61f89850 8 131696 System.Int64[] 61fd8938 2769 166140 System.Reflection.RuntimeMethodInfo 61f824e4 9800 676596 System.String 61f85c40 2064 705655 System.Byte[] 61f82c60 195 2369402 System.Char[] 007988d0 114 3338792 Free Total 47306 objects  

着重分析(紅色部分)這兩個文件的內存分配情況,似乎差別不大,完全看不出來80-33=近50M的內存消耗在哪裡;

但認真思考一下,這樣好像也沒有問題,因為System.***這種類型是C#環境獨有的,已知C#沒有內存泄漏,所以這裏沒有體現應該是正常的;

那C語言接口文件裡邊的問題該如何找出來呢?

 

2、再來試試!heap -s,查看各種堆的內存提交數據量

33.dmp

0:047> !heap -s
LFH Key                   : 0x343fce0b
Termination on corruption : ENABLED
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
00780000 00000002    8192   4636   8192    209  2484     4    0      e   LFH
002e0000 00001002     256      4    256      2     1     1    0      0      
00280000 00001002    1088     72   1088      5     2     2    0      0      
00c70000 00041002     256      4    256      2     1     1    0      0      
002d0000 00001002    1088    132   1088      8    23     2    0      0      
00450000 00001002     256      4    256      0     1     1    0      0      
07230000 00041002     256      4    256      2     1     1    0      0      
00c10000 00001002     256    216    256      3    39     1    0      0   LFH
09b50000 00001002     256     80    256     39    28     1    0      0      
09d00000 00001002      64      4     64      2     1     1    0      0      
09ef0000 00001002    1088     72   1088      6     2     2    0      0      
004c0000 00001002    1088    192   1088     15   140     2    0      0      
09760000 00041002     256     28    256      4     4     1    0      0      
09ed0000 00001002      64     12     64      1     1     1    0      0      
0b210000 00001002    3136   1456   3136     52    84     3    0      0   LFH
0a700000 00001002     256    212    256      2     1     1    0      0      
0e1e0000 00011002     256      4    256      0     1     1    0      0      
0d030000 00001002     256     16    256      3     1     1    0      0      
11b30000 00001002    1088    388   1088      0     1     2    0      0      
-----------------------------------------------------------------------------

 

80.dmp

0:051> !heap -s LFH Key : 0x343fce0b Termination on corruption : ENABLED Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast (k) (k) (k) (k) length blocks cont. heap ----------------------------------------------------------------------------- 00780000 00000002 8192 4808 8192 225 2505 4 0 f1 LFH 002e0000 00001002 256 4 256 2 1 1 0 0 00280000 00001002 1088 132 1088 4 6 2 0 0 00c70000 00041002 256 4 256 2 1 1 0 0 002d0000 00001002 1088 168 1088 12 26 2 0 0 00450000 00001002 256 4 256 0 1 1 0 0 07230000 00041002 256 4 256 2 1 1 0 0 00c10000 00001002 256 228 256 26 69 1 0 0 LFH 09b50000 00001002 256 80 256 39 25 1 0 0 09d00000 00001002 64 4 64 2 1 1 0 0 09ef0000 00001002 1088 132 1088 6 5 2 0 0 004c0000 00001002 1088 220 1088 26 173 2 0 0 09760000 00041002 256 28 256 4 8 1 0 0 09ed0000 00001002 64 12 64 1 1 1 0 0 0b210000 00001002 3136 1456 3136 74 71 3 0 0 LFH 0a700000 00001002 256 212 256 2 1 1 0 0 0e1e0000 00011002 256 4 256 0 1 1 0 0 0d030000 00001002 256 16 256 1 1 1 0 0 11b30000 00001002 47808 46068 47808 396 6836 7 0 0 -----------------------------------------------------------------------------

這次有異常了,可以看到11b30000這一行內存提交變化很大 47808 – 1088 = 46720;

這次可以肯定問題就在這個堆裡邊;

3、進去看看11b30000,使用命令:!heap -stat -h 11b30000

80.dmp


0:051> !heap -stat -h 11b30000 
 heap @ 11b30000
group-by: TOTSIZE max-display: 20
    size     #blocks     total     ( %) (percent of total busy bytes)
 1f0 102d9 - 1f58470 (92.48) 18 102b0 - 184080 (4.47) 10 102ae - 102ae0 (2.98)
    214 13 - 277c  (0.03)
    1000 2 - 2000  (0.02)
    800 2 - 1000  (0.01)
    220 1 - 220  (0.00)
    1d7 1 - 1d7  (0.00)
    80 3 - 180  (0.00)
    a4 1 - a4  (0.00)
    24 4 - 90  (0.00)
    14 4 - 50  (0.00)
    4a 1 - 4a  (0.00)
    25 2 - 4a  (0.00)
    48 1 - 48  (0.00)
    46 1 - 46  (0.00)
    41 1 - 41  (0.00)
    3e 1 - 3e  (0.00)
    3c 1 - 3c  (0.00)
    37 1 - 37  (0.00)

 

可以看到前面3項幾乎佔據99%的內存提交記錄;尤其以內存塊大小為1f0的數據塊使用最多內存;

到目前為止,我們知道了幾項有效信息,有大小分別為1f0、18、10的三種數據塊,不斷申請出新空間;

但是這樣還不夠,根據一個內存塊的大小並不能準確定位是哪裡出了問題,這是一個結構體?還是字符串?還是數組?

都不知道,所以有必要進去看看,有哪些地方使用到了這些數據塊

 

4、查看使用了1f0數據塊大小的位置列表,使用命令:!heap -flt s [size]

80.dmp

0:051> !heap -flt s 1f0
    _DPH_HEAP_ROOT @ 5a1000
    Freed and decommitted blocks
      DPH_HEAP_BLOCK : VirtAddr VirtSize
    Busy allocations
      DPH_HEAP_BLOCK : UserAddr  UserSize - VirtAddr VirtSize
    _HEAP @ 780000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        0078e5b8 0045 0000  [00]   0078e5e0    001f0 - (busy)
    _DPH_HEAP_ROOT @ 9d11000
    Freed and decommitted blocks
      DPH_HEAP_BLOCK : VirtAddr VirtSize
    Busy allocations
      DPH_HEAP_BLOCK : UserAddr  UserSize - VirtAddr VirtSize
    _HEAP @ 4c0000
    _DPH_HEAP_ROOT @ af41000
    Freed and decommitted blocks
      DPH_HEAP_BLOCK : VirtAddr VirtSize
    Busy allocations
      DPH_HEAP_BLOCK : UserAddr  UserSize - VirtAddr VirtSize
    _HEAP @ b210000
        0cf61680 0045 0045  [00]   0cf616a8    001f0 - (busy)
    _DPH_HEAP_ROOT @ d871000
    Freed and decommitted blocks
      DPH_HEAP_BLOCK : VirtAddr VirtSize
    Busy allocations
      DPH_HEAP_BLOCK : UserAddr  UserSize - VirtAddr VirtSize
    _HEAP @ d030000
    _DPH_HEAP_ROOT @ 11631000
    Freed and decommitted blocks
      DPH_HEAP_BLOCK : VirtAddr VirtSize
    Busy allocations
      DPH_HEAP_BLOCK : UserAddr  UserSize - VirtAddr VirtSize
    _HEAP @ 11b30000
        11b312e8 0045 0045  [00]   11b31310    001f0 - (busy)
        11b315a8 0045 0045  [00]   11b315d0    001f0 - (busy)
        11b356f8 0045 0045  [00]   11b35720    001f0 - (busy)
        11b35920 0045 0045  [00]   11b35948    001f0 - (busy)
        11b36f30 0045 0045  [00]   11b36f58    001f0 - (busy)
        11b37b58 0045 0045  [00]   11b37b80    001f0 - (busy)
        11b37e18 0045 0045  [00]   11b37e40    001f0 - (busy)
        11b3e4f0 0045 0045  [00]   11b3e518    001f0 - (busy)
        11b3f570 0045 0045  [00]   11b3f598    001f0 - (busy)
        11b3f830 0045 0045  [00]   11b3f858    001f0 - (busy)
        11b3faf0 0045 0045  [00]   11b3fb18    001f0 - (busy)
        11b3fdb0 0046 0045  [00]   11b3fdd8    001f0 - (busy)
        12890578 0045 0046  [00]   128905a0    001f0 - (busy)
        ......  

可以看到有很多堆都有使用到1f0大小的內存塊,但是只有最後一個堆 _DPH_HEAP_ROOT @ 11631000

是記錄最多的,滿屏都是,這裏只能截斷,選取一部分看看  

5、查看調用堆棧,使用命令:!heap -p -a [address]

80.dmp

0:051> !heap -p -a 11b3fdd8
    address 11b3fdd8 found in
    _HEAP @ 11b30000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        11b3fdb0 0046 0000  [00]   11b3fdd8    001f0 - (busy)
        Trace: 083a
        7405a6a7 verifier!AVrfpDphNormalHeapAllocate+0x000000d7
        74058f6e verifier!AVrfDebugPageHeapAllocate+0x0000030e
        77d10fe6 ntdll!RtlDebugAllocateHeap+0x00000030
        77ccab8e ntdll!RtlpAllocateHeap+0x000000c4
        77c73461 ntdll!RtlAllocateHeap+0x0000023a
        664668e5 msvcr90!_calloc_impl+0x00000125
        66463c5a msvcr90!calloc+0x0000001a

 
0:051> !heap -p -a 11b3fdd8
    address 11b3fdd8 found in
    _HEAP @ 11b30000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        11b3fdb0 0046 0000  [00]   11b3fdd8    001f0 - (busy)
        Trace: 083a
        7405a6a7 verifier!AVrfpDphNormalHeapAllocate+0x000000d7
        74058f6e verifier!AVrfDebugPageHeapAllocate+0x0000030e
        77d10fe6 ntdll!RtlDebugAllocateHeap+0x00000030
        77ccab8e ntdll!RtlpAllocateHeap+0x000000c4
        77c73461 ntdll!RtlAllocateHeap+0x0000023a
        664668e5 msvcr90!_calloc_impl+0x00000125
        66463c5a msvcr90!calloc+0x0000001a

 
0:051> !heap -p -a 11b3fb18
    address 11b3fb18 found in
    _HEAP @ 11b30000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        11b3faf0 0045 0000  [00]   11b3fb18    001f0 - (busy)
        Trace: 083a
        7405a6a7 verifier!AVrfpDphNormalHeapAllocate+0x000000d7
        74058f6e verifier!AVrfDebugPageHeapAllocate+0x0000030e
        77d10fe6 ntdll!RtlDebugAllocateHeap+0x00000030
        77ccab8e ntdll!RtlpAllocateHeap+0x000000c4
        77c73461 ntdll!RtlAllocateHeap+0x0000023a
        664668e5 msvcr90!_calloc_impl+0x00000125
        66463c5a msvcr90!calloc+0x0000001a  

  隨意挑選幾個查看調用堆棧,似乎沒有有用的特徵信息,verifier、ntdll、msvcr90這些都是操作系統內核級別的函數;

並不能暴露出使用1f0大小的數據塊大概位置,這就有點難辦了,難道此路不通?如果不找到有效堆棧信息,想定位

內心泄漏點,靠單步調試會相當麻煩。。。

  不急,先看看,這些地方內存塊內容是什麼,說不定能找到一些有效特徵信息;

使用命令:db [UserPtr]

80.dmp

0:051> db 11b3fb18
11b3fb18  00 00 04 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fb28  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fb38  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fb48  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fb58  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fb68  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fb78  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fb88  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0:051> db 11b3fdd8
11b3fdd8  00 00 04 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fde8  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fdf8  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fe08  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fe18  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fe28  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fe38  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fe48  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0:051> db 11b3fdd8
11b3fdd8  00 00 04 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fde8  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fdf8  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fe08  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fe18  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fe28  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fe38  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
11b3fe48  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................  

結果是令人失望的;

显示這些基本都是空白內存,裡邊已經沒有任何有效信息,,

陷入死衚衕里了,難道到此為止?

還不死心,我們再看看這些地址有沒有引用跟,如果有引用跟,也可以打印堆棧信息

使用命令:!gcroot [UserPtr]

80.dmp

0:051> !gcroot 11b3fb18
Found 0 unique roots (run '!GCRoot -all' to see all roots).
0:051> !gcroot 11b3fdd8
Found 0 unique roots (run '!GCRoot -all' to see all roots).
0:051> !gcroot 11b3fdd8
Found 0 unique roots (run '!GCRoot -all' to see all roots).

 願望是美好的,這個大小位1f0的數據塊被申請了0x102d9次,使用!gcroot命令查看得到貌似都是無引用的野數據

我們再來看看,這個 _DPH_HEAP_ROOT @ 11631000堆的創建堆棧

80.dmp

0:051> dt ntdll!_DPH_HEAP_ROOT CreateStackTrace 11631000
   +0x0b8 CreateStackTrace : 0x04d54f8c _RTL_TRACE_BLOCK
0:051> dds 0x04d54f8c 
04d54f8c  04d1b714
04d54f90  0000f801
04d54f94  000f0000
04d54f98  74058969 verifier!AVrfDebugPageHeapCreate+0x439
04d54f9c  77cbcea2 ntdll!RtlCreateHeap+0x41
04d54fa0  757356bc KERNELBASE!HeapCreate+0x50
04d54fa4  66463a4a msvcr90!_heap_init+0x1b
04d54fa8  66422bb4 msvcr90!__p__tzname+0x2a
04d54fac  66422d5e msvcr90!_CRTDLL_INIT+0x1e
04d54fb0  77c79264 ntdll!LdrpCallInitRoutine+0x14
04d54fb4  77c7fe97 ntdll!LdrpRunInitializeRoutines+0x26f
04d54fb8  77c7ea4e ntdll!LdrpLoadDll+0x472
04d54fbc  77cbd3df ntdll!LdrLoadDll+0xc7
04d54fc0  75732e6a KERNELBASE!LoadLibraryExW+0x233
04d54fc4  7562483c kernel32!LoadLibraryW+0x11
04d54fc8  6d3d18de*** WARNING: Unable to verify checksum for Win32Project1.dll
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for Win32Project1.dll - 
 Win32Project1+0x18de
04d54fcc  6d3d28fc Win32Project1!BACNet::Init+0x5c
04d54fd0  6d3d5925 Win32Project1!Init+0x25
04d54fd4  66639972*** WARNING: Unable to verify checksum for SMDB.dll
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for SMDB.dll - 
 SMDB!LogPop+0x12
04d54fd8  66639452 SMDB!CreateSharedMemory+0x12
04d54fdc  6d8e47bd clrjit!Compiler::impImportBlockCode+0x2aac [f:\dd\ndp\clr\src\jit32\importer.cpp @ 10258]
04d54fe0  6d8c2e6b clrjit!Compiler::impImportBlock+0x5f [f:\dd\ndp\clr\src\jit32\importer.cpp @ 13246]
04d54fe4  6d8c306a clrjit!Compiler::impImport+0x235 [f:\dd\ndp\clr\src\jit32\importer.cpp @ 14195]
04d54fe8  6d8c364f clrjit!Compiler::compCompile+0x62 [f:\dd\ndp\clr\src\jit32\compiler.cpp @ 2491]
04d54fec  6d8c4276 clrjit!Compiler::compCompileHelper+0x32f [f:\dd\ndp\clr\src\jit32\compiler.cpp @ 3615]
04d54ff0  6d8c43fc clrjit!Compiler::compCompile+0x2ab [f:\dd\ndp\clr\src\jit32\compiler.cpp @ 3086]
04d54ff4  6d8c45c8 clrjit!jitNativeCode+0x1f6 [f:\dd\ndp\clr\src\jit32\compiler.cpp @ 4057]
04d54ff8  6d8c377d clrjit!CILJit::compileMethod+0x7d [f:\dd\ndp\clr\src\jit32\ee_il_dll.cpp @ 180]
04d54ffc  633b39b3 clr!invokeCompileMethodHelper+0x10b
04d55000  633b3a8b clr!invokeCompileMethod+0x3d
04d55004  633b3ae8 clr!CallCompileMethodWithSEHWrapper+0x39
04d55008  633b3d97 clr!UnsafeJitFunction+0x431

動態庫Win32Project1.dll是對MSTP_DLL動態庫的再次封裝可以確定不存在內存泄漏問題;

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

搬家費用:依消費者運送距離、搬運樓層、有無電梯、步行距離、特殊地形、超重物品等計價因素後,評估每車次單

看到這個堆是在於硬件設備通信的時候,初始化時CLR創建的線程;

不過知道這個好像也沒有什麼用,因為我們本來就知道是BACnet協議通信的動態庫有問題;

只能說明是初始化之後產生的內存泄漏;

 

但是為什麼這些無跟指針沒有被垃圾回收? 

但是仔細一想,好像也是正常,因為這些是可以明確的在C語言編寫的動態庫里申請的內存,屬於不受託管的內存;

C#垃圾回收也只能回收託管內存,所以這部分數據不主動釋放,那就會永遠在那裡;

但是現在,好像陷入死衚衕了,找不到思路,既然如此就先放放,先看看其他兩個數據塊的調用情況;

6、!heap -flt s 18

80.dmp

> !heap -flt s 18
...
        16f45098 000a 000a  [00]   16f450c0    00018 - (busy)
        16f45358 000a 000a  [00]   16f45380    00018 - (busy)
        16f45618 000a 000a  [00]   16f45640    00018 - (busy)
        16f458d8 000a 000a  [00]   16f45900    00018 - (busy)
        16f45b98 000a 000a  [00]   16f45bc0    00018 - (busy)
        16f46080 000a 000a  [00]   16f460a8    00018 - (busy)
        16f46118 000a 000a  [00]   16f46140    00018 - (busy)
        16f461b0 000a 000a  [00]   16f461d8    00018 - (busy)
        16f46248 000a 000a  [00]   16f46270    00018 - (busy)
        16f462e0 000a 000a  [00]   16f46308    00018 - (busy)
        16f46378 000a 000a  [00]   16f463a0    00018 - (busy)
        16f46410 000a 000a  [00]   16f46438    00018 - (busy)
        16f464a8 000b 000a  [00]   16f464d0    00018 - (busy)
        16f46548 000a 000b  [00]   16f46570    00018 - (busy)
        16f46808 000a 000a  [00]   16f46830    00018 - (busy)
        16f46ac8 000a 000a  [00]   16f46af0    00018 - (busy)
        16f46d88 000a 000a  [00]   16f46db0    00018 - (busy)
        16f47048 000a 000a  [00]   16f47070    00018 - (busy)
        16f47308 000a 000a  [00]   16f47330    00018 - (busy)
...

7、隨意挑幾個看看,命令:!heap -p -a [UserPtr]  

80.dmp

0:051> !heap -p -a 
invalid address  passed to `-p -a'0:051> !heap -p -a 16f460a8    
    address 16f460a8 found in
    _HEAP @ 11b30000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        16f46080 000a 0000  [00]   16f460a8    00018 - (busy)
        Trace: 074b
        7405a6a7 verifier!AVrfpDphNormalHeapAllocate+0x000000d7
        74058f6e verifier!AVrfDebugPageHeapAllocate+0x0000030e
        77d10fe6 ntdll!RtlDebugAllocateHeap+0x00000030
        77ccab8e ntdll!RtlpAllocateHeap+0x000000c4
        77c73461 ntdll!RtlAllocateHeap+0x0000023a
        664668e5 msvcr90!_calloc_impl+0x00000125
        66463c5a msvcr90!calloc+0x0000001a
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for MSTP_DLL.dll - 
        669baea1 MSTP_DLL!MSTP_Get_RPM_ACK_Data+0x00000091

 
0:051> !heap -p -a 16f46570    
    address 16f46570 found in
    _HEAP @ 11b30000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        16f46548 000a 0000  [00]   16f46570    00018 - (busy)
        7405a6a7 verifier!AVrfpDphNormalHeapAllocate+0x000000d7
        74058f6e verifier!AVrfDebugPageHeapAllocate+0x0000030e
        77d10fe6 ntdll!RtlDebugAllocateHeap+0x00000030
        77ccab8e ntdll!RtlpAllocateHeap+0x000000c4
        77c73461 ntdll!RtlAllocateHeap+0x0000023a
        664668e5 msvcr90!_calloc_impl+0x00000125
        66463c5a msvcr90!calloc+0x0000001a
        669baea1 MSTP_DLL!MSTP_Get_RPM_ACK_Data+0x00000091

 
0:051> !heap -p -a 16f46308
    address 16f46308 found in
    _HEAP @ 11b30000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        16f462e0 000a 0000  [00]   16f46308    00018 - (busy)
        Trace: 074b
        7405a6a7 verifier!AVrfpDphNormalHeapAllocate+0x000000d7
        74058f6e verifier!AVrfDebugPageHeapAllocate+0x0000030e
        77d10fe6 ntdll!RtlDebugAllocateHeap+0x00000030
        77ccab8e ntdll!RtlpAllocateHeap+0x000000c4
        77c73461 ntdll!RtlAllocateHeap+0x0000023a
        664668e5 msvcr90!_calloc_impl+0x00000125
        66463c5a msvcr90!calloc+0x0000001a
        669baea1 MSTP_DLL!MSTP_Get_RPM_ACK_Data+0x00000091  

這次很順利,這個內存使用的地方實在MSTP_DLL的 MSTP_Get_RPM_ACK_Data裡邊;這個就是我們要找的最終的內存泄漏點信息;

同樣操作堆10大小的數據塊操作一遍

80.dmp

> !heap -flt s 10
...
        15359fa0 0009 0009  [00]   15359fc8    00010 - (busy)
        1535a2a0 0009 0009  [00]   1535a2c8    00010 - (busy)
        1535a560 0009 0009  [00]   1535a588    00010 - (busy)
        1535aee8 0009 0009  [00]   1535af10    00010 - (busy)
        1535af80 0009 0009  [00]   1535afa8    00010 - (busy)
        1535b018 0009 0009  [00]   1535b040    00010 - (busy)
        1535b360 0009 0009  [00]   1535b388    00010 - (busy)
        1535b620 0009 0009  [00]   1535b648    00010 - (busy)
        1535c420 0009 0009  [00]   1535c448    00010 - (busy)
        1535d220 0009 0009  [00]   1535d248    00010 - (busy)
        1535d4e0 0009 0009  [00]   1535d508    00010 - (busy)
        1535d7a0 0009 0009  [00]   1535d7c8    00010 - (busy)
        1535da60 0009 0009  [00]   1535da88    00010 - (busy)
        1535dd20 0009 0009  [00]   1535dd48    00010 - (busy)
        1535dfe0 0009 0009  [00]   1535e008    00010 - (busy)
        1535e2a0 0009 0009  [00]   1535e2c8    00010 - (busy)
        1535e560 0009 0009  [00]   1535e588    00010 - (busy)
        1535e820 0009 0009  [00]   1535e848    00010 - (busy)
        1535eae0 0009 0009  [00]   1535eb08    00010 - (busy)
        1535eda0 0009 0009  [00]   1535edc8    00010 - (busy)
        1535f060 0009 0009  [00]   1535f088    00010 - (busy)
        1535f320 0009 0009  [00]   1535f348    00010 - (busy)
        1535f5e0 0009 0009  [00]   1535f608    00010 - (busy)
...
80.dmp

0:051> !heap -p -a 1535eb08    
    address 1535eb08 found in
    _HEAP @ 11b30000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        1535eae0 0009 0000  [00]   1535eb08    00010 - (busy)
        Trace: 0817
        7405a6a7 verifier!AVrfpDphNormalHeapAllocate+0x000000d7
        74058f6e verifier!AVrfDebugPageHeapAllocate+0x0000030e
        77d10fe6 ntdll!RtlDebugAllocateHeap+0x00000030
        77ccab8e ntdll!RtlpAllocateHeap+0x000000c4
        77c73461 ntdll!RtlAllocateHeap+0x0000023a
        664668e5 msvcr90!_calloc_impl+0x00000125
        66463c5a msvcr90!calloc+0x0000001a
        669bb07b MSTP_DLL!MSTP_Get_RP_ACK_Data+0x0000003b

 
0:051> !heap -p -a 1535f088    
    address 1535f088 found in
    _HEAP @ 11b30000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        1535f060 0009 0000  [00]   1535f088    00010 - (busy)
        Trace: 0817
        7405a6a7 verifier!AVrfpDphNormalHeapAllocate+0x000000d7
        74058f6e verifier!AVrfDebugPageHeapAllocate+0x0000030e
        77d10fe6 ntdll!RtlDebugAllocateHeap+0x00000030
        77ccab8e ntdll!RtlpAllocateHeap+0x000000c4
        77c73461 ntdll!RtlAllocateHeap+0x0000023a
        664668e5 msvcr90!_calloc_impl+0x00000125
        66463c5a msvcr90!calloc+0x0000001a
        669bb07b MSTP_DLL!MSTP_Get_RP_ACK_Data+0x0000003b

 
0:051> !heap -p -a 1535f348    
    address 1535f348 found in
    _HEAP @ 11b30000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        1535f320 0009 0000  [00]   1535f348    00010 - (busy)
        Trace: 0817
        7405a6a7 verifier!AVrfpDphNormalHeapAllocate+0x000000d7
        74058f6e verifier!AVrfDebugPageHeapAllocate+0x0000030e
        77d10fe6 ntdll!RtlDebugAllocateHeap+0x00000030
        77ccab8e ntdll!RtlpAllocateHeap+0x000000c4
        77c73461 ntdll!RtlAllocateHeap+0x0000023a
        664668e5 msvcr90!_calloc_impl+0x00000125
        66463c5a msvcr90!calloc+0x0000001a
        669bb07b MSTP_DLL!MSTP_Get_RP_ACK_Data+0x0000003b  

這次也順利拿到另一個內存泄漏的位置信息在MSTP_DLL的 MSTP_Get_RP_ACK_Data裡邊;  

MSTP_Get_RP_ACK_Data

MSTP_Get_RPM_ACK_Data

這兩個方法其實是讀取模塊點數值或者收集模塊信息的時候返回的一個數據指針;

現在很明顯這兩個方法返回的指針可能是有問題的,裡邊非常大的可能存在內存泄漏;

7、驗證

跟同事找到原來的MSTP_DLL的源碼,找到以上兩個方法體

 

 可以看到當初那位同事設計這個方法的時候,很明顯有2個錯誤;

1)返回的指針只見聲明內存空間,不見釋放;

2)返回數據的指針不應該在方法體中的返回值中傳出來,應該寫在方法參數中,外部聲明,傳進去賦值,然後外部使用,再外部釋放

3)兩個方法體都一樣的問題

 

五、整理

1)我們知道有三處內存泄漏,分別大小是1f0、18、10 

2)三者佔據99%的新增不釋放的內存消耗

3)我們已經找到其中兩個泄漏位置,還剩下一個

4)1f0是重中之重,佔據內存消耗92%,不解決這個BUG,問題基本就相當於沒解決

5)無法找到1f0的調用堆棧信息,無明顯特徵信息,無引用跟;

5)emmmmm? (第二聲)

 

好像被我們錯過了一個信息, 

是否還記得最開始那一段?

80.dmp

0:051> !heap -stat -h 11b30000 
 heap @ 11b30000
group-by: TOTSIZE max-display: 20
    size     #blocks     total     ( %) (percent of total busy bytes)
    1f0 102d9 - 1f58470  (92.48)
    18 102b0 - 184080  (4.47)
    10 102ae - 102ae0  (2.98)  

 這幾個數據很接近,都是申請次數大小,也就是說著三個數據塊被申請的次數差不多。。

鑒於此,我們再去看看33M內存的時候這幾個次數的值是多少

33.dmp

0:047> !heap -s
LFH Key                   : 0x343fce0b
Termination on corruption : ENABLED
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
00780000 00000002    8192   4636   8192    209  2484     4    0      e   LFH
002e0000 00001002     256      4    256      2     1     1    0      0      
00280000 00001002    1088     72   1088      5     2     2    0      0      
00c70000 00041002     256      4    256      2     1     1    0      0      
002d0000 00001002    1088    132   1088      8    23     2    0      0      
00450000 00001002     256      4    256      0     1     1    0      0      
07230000 00041002     256      4    256      2     1     1    0      0      
00c10000 00001002     256    216    256      3    39     1    0      0   LFH
09b50000 00001002     256     80    256     39    28     1    0      0      
09d00000 00001002      64      4     64      2     1     1    0      0      
09ef0000 00001002    1088     72   1088      6     2     2    0      0      
004c0000 00001002    1088    192   1088     15   140     2    0      0      
09760000 00041002     256     28    256      4     4     1    0      0      
09ed0000 00001002      64     12     64      1     1     1    0      0      
0b210000 00001002    3136   1456   3136     52    84     3    0      0   LFH
0a700000 00001002     256    212    256      2     1     1    0      0      
0e1e0000 00011002     256      4    256      0     1     1    0      0      
0d030000 00001002     256     16    256      3     1     1    0      0      
11b30000 00001002    1088    388   1088      0     1     2    0      0      
-----------------------------------------------------------------------------
0:047> !heap -stat -h 11b30000 
 heap @ 11b30000
group-by: TOTSIZE max-display: 20
    size     #blocks     total     ( %) (percent of total busy bytes)
    1f0 1f2 - 3c4e0  (86.13)
    18 1c9 - 2ad8  (3.82)
    1000 2 - 2000  (2.86)
    10 1c7 - 1c70  (2.54)
    214 c - 18f0  (2.23)
    800 2 - 1000  (1.43)
    220 1 - 220  (0.19)
    1d7 1 - 1d7  (0.16)
    80 3 - 180  (0.13)
    a4 1 - a4  (0.06)
    24 4 - 90  (0.05)
    14 4 - 50  (0.03)
    4a 1 - 4a  (0.03)
    25 2 - 4a  (0.03)
    48 1 - 48  (0.03)
    46 1 - 46  (0.02)
    41 1 - 41  (0.02)
    3e 1 - 3e  (0.02)
    3c 1 - 3c  (0.02)
    37 1 - 37  (0.02)  

 分別是1f2、1c9、1c7;

1f0:102d9 – 1f2 = 65767

18:102b0 – 1c9 = 65767

10:102ae – 1c7 = 65767

居然申請的次數一模一樣!

穩了!這個1f0可以斷定與其他兩個緊密相關;首先懷疑的就是

MSTP_Get_RP_ACK_Data

MSTP_Get_RPM_ACK_Data

1)這兩個方法體中使用到的所有子方法體有沒有申請空間的語句;

2)申請的空間大小是不是就是1f0;

 

依據上面的推測,再次閱讀那2個方法體;

 

 

 

 

經過分析BACNET_APPLICATION_DATA_VALUE結構體大小剛好就是1f0 

好了,搞定

 

如果對你有幫助,請點贊、評論;  

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

節能減碳愛地球是景泰電動車的理念,是創立景泰電動車行的初衷,滿意態度更是服務客戶的最高品質,我們的成長來自於你的推薦。

當老傳統成為新時尚_包裝設計

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

北宋年間,家家戶戶貼年畫已是一種風尚,也由此,木版年畫在歷史的長河中始終留存着時尚的記憶。當代,手工木版年畫被以非物質文化遺產的形態加以保護,同時,傳承和創新也成為木版年畫傳承人肩負的兩個重任。有這樣一群人,他們視木版年畫為中國民間藝術寶庫中的一顆璀璨明珠,一直走在挖掘、創新、變革的路上。又是新年將至,今年的木版年畫可以為人們帶來哪些新意?這些心心念念的新時代“年畫家”都在忙些什麼?

 設計師王全傑 :年畫新可能

楊柳青年畫周曆簽

新年將至,北京順義區的一所小院里,設計師王全傑剛剛忙完楊柳青年畫周曆簽——2020年“世象新語”周曆的預售,5000套周曆幾乎都已被單位預定出去。初次嘗試與楊柳青木版年畫合作,並能夠得到市場的認可,王全傑不斷回味。

2019年7月,王全傑在清華美院校友召喚下加入了年畫日新創作營,本着對於年畫的關注與喜愛,帶着輕鬆自在的心情,王全傑輕裝上陣了。

剛入營,兩件事讓王全傑心情無法平靜。首先創作營的主辦方將組織結構安排得相當縝密,全國11個木版年畫產地會自發組成一個個創作小組(注:最終實際組成了9個創作小組),年畫傳承人+插畫師+設計師+導師(清華美院和中央美院的教授和副教授)作為一個小組組合,王全傑成了楊柳青組組長。主辦方的安排是,經過一個階段的年畫知識學習,再經歷1個月的創作,然後做出創新作品和產品進行展覽,最後對社會公開創作成果。

在學習了年畫知識、深入年畫創作地區了解了年畫歷史后,王全傑原先以為會憑藉著豐富設計經驗輕鬆完成這份任務,但卻被厚重的傳統文化攪動了內心。“真是無處下手,年畫傳承人深陷其中很難改變,我們作為外來者,出於對老祖宗留下的這些寶物的敬畏,似乎也不敢隨便說、隨便做了。”王全傑說。

一個月的迷茫期過後,王全傑總結出年畫轉化的兩個難點:第一,作為傳承人,雖然擁有老祖宗留下的年畫模板、掌握着傳統的刻畫技藝,但是缺少產品轉化能力;第二,作為外來者——設計人員,雖有很強的設計能力,但究竟轉化成什麼是最大的難點。

王全傑在腦海中翻轉:做什麼樣的產品,怎麼能體現楊柳青風格,怎麼能有新意,怎麼能增加使用的體驗感、情景感,還得製造點小驚喜。

經歷了無數個靈感的沉浮,一次次被自己或小組成員否定,王全傑關於周曆的創意橫空出世。機緣巧合的是,這樣一個創意很快得到北京一家企業的認可,並有意出資支持該創意。王全傑信心倍增。

“創意有了,落實到產品時,第一版就被出資方否定了。”王全傑再次回到原點。利用楊柳青年畫元素設計的第一版周曆美觀,但缺少生活黏性、很難達到與使用者的互動。

年畫日新創作營楊柳青組設計師們再次前往楊柳青,找到楊柳青木版年畫國家級非遺傳承人霍慶順,聽霍慶順細數年畫民俗,並對其所擁有的與老百姓生活相關的藏品做了仔細研究。霍慶順老人一句話點亮了王全傑內心。

“你看踩高蹺,大多數人都只看到表面上的熱鬧,但很少有人知道這其中最主要的用意是祭拜藥王、祈福安康。進入年關,每一天都有特殊的意義……”這句話就像一把開啟創意之門的鑰匙,為迷途中的王全傑和設計師們打開了門:“周曆的基礎上加入日曆,在臘月的最後一周開始日曆倒計時。”

創意有了,楊柳青創作小組成員連續两天兩夜的頭腦風暴,將創意再次細化落實在產品上:在周曆基礎之上特別策劃的“過年日曆”,篇篇辣詞趣語,以當下語境直入現代生活,內置如意轉盤每天跟你靈犀互動。這些新意讓2020年“世象新語”周曆產品順利出版落地。

“我們設計師想給楊柳青木版年畫傳承者做出一個示範,讓他們看到傳統年畫與當代流行文化融合的更多可能性。為大眾提供一個日常使用載體,讓人們了解楊柳青木版年畫的豐富性和多樣性。”王全傑說。

故事記錄者恭弘=叶 恭弘萌 :回歸當代時尚

穀雨平台上的金華木板年畫

對傳統手工藝有着濃厚興趣的恭弘=叶 恭弘萌,探索傳統與當代文化融合的路徑用了5年時間,為了讓更多人了解傳統手工藝他組建了一個傳統手工藝視頻傳播平台——穀雨,這個平台免費為手工藝人們拍攝宣傳片。

※產品缺大量曝光嗎?你需要的是一流包裝設計!

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

“中國上下五千年,從來不缺故事,但卻沒有人會講這些中國故事,有太多我們為之自豪的傳統手工藝散落於四野。如果不能讓人看見,又何談傳承呢?”恭弘=叶 恭弘萌說。

恭弘=叶 恭弘萌畢業於中國美術學院視覺傳達系。他做過設計師,參与設計的項目有中國國際西湖博覽會、第21屆金雞百花電影節標誌等;在電視台工作了十幾年。為了追逐心中的夢想,放棄穩定的工作,恭弘=叶 恭弘萌創業成立了自己的文化公司。服務了幾百個商業客戶后,2015年他突然感覺工作生涯中所有的積累都應該為了達成他內心中一個夢想——用各種方式構築傳統手工藝從再現到再生的體系,讓傳統手工藝能夠回歸當代生活,成為新的時尚,至此,穀雨——傳統手工藝內容轉化平台宣告問世。

2019年,穀雨的木版年畫產品轉化項目“傳統節中國禮”主旨就是讓傳統節更中國、更時尚。“穀雨打造的內容轉化平台是要做傳統手工藝時尚品牌孵化池。”恭弘=叶 恭弘萌說。

穀雨的平台上金華木版年畫製作者黃菁菁就是一位不甘於讓傳統成為傳說的女性。

金華木版年畫,孕育於漢唐,形成於宋元,鼎盛於明清。浙江在五代吳越時期就是木版畫比較發達的地區,至宋代,金華已經是全國木版年畫的中心之一。

黃菁菁出身木版年畫世家,長大后並沒有從事年畫製作,而成為一名商人,在杭州開了一家文化公司,經營得紅紅火火。雖然她在商界收穫了成功,但內心深處無數次回憶起兒時父輩們製作木版年畫的場景、父親抱着她給她講《五子登科》年畫故事的場景……她對記憶中的年畫魂牽夢繞。

那段日子里,似乎總有一個聲音,在向她不停地召喚。她決定:回家,做木版年畫。

回鄉之時正是金華木版年畫最蕭條的時期,曾經有專家對金華木版年畫的萎縮倍感痛心,黃菁菁的回歸讓金華木板年畫再現生機。

說來容易,做起來才知道苦辣辛酸。首先需搜集老版與老畫,“尋回金華年畫的根”。剛開始的那幾年,黃菁菁一直奔波在路上,從南疆到北國。後來甚至還走出了國門,無論是老版還是老畫,黃菁菁的用語都是“請回來”。 就這樣,黃菁菁一點一滴地打造起金華木版年畫博物館和年畫製作體驗館。如今,她的年畫博物館中已收藏了60餘套老版、2000多幅老年畫,其中六成都是孤版。而她的年畫製作體驗基地成了中國第六個年畫製作基地,也是浙江唯一的年畫基地。

傳承人邰立平:讓年畫“火”起來

邰立平作品《方弼》

年關將至,鳳翔木版年畫代表性傳承人邰立平忙得不亦樂乎。

鳳翔年畫“始於唐宋,盛於明清”,早在600多年前的明初洪武年間,世代耕居於此的邰氏家族就已經開始從事年畫的生產了。邰立平是鳳翔木版年畫第20代傳人。他創辦了鳳怡年畫社,致力於對流散民間的古樣進行挖掘、整理、研究和複製,使這一古老民間傳統藝術得以傳承。

邰立平這一代傳承人經歷了年畫從興到衰整個過程。“隨着時代的轉變和人們生活方式的變化,之前人們張貼年畫的習俗正在慢慢地改變。雖然春節時張貼年畫的習俗還未消失,但也從張貼傳統手工印製的年畫轉變為張貼機器印刷品,這就對傳統的非遺手工年畫形成了很大衝擊。傳統的木版年畫就是反映人民生活的一部百科全書。近年來,隨着國家對傳統文化的重視,社會上對這項古老的傳承技藝越來越推崇和認可,出現了很多國潮風格的文創作品,這都是傳統非遺技藝的文化土壤。”邰立平說。

邰立平認為,當下是鳳翔木版年畫發展的最佳時機。“把木版年畫繼承下去,讓民間美術發揚光大”這是邰立平最大的心愿。為了能夠儘早實現自己的心愿,他分步驟推動自己的計劃。

首先需要找回失散的木刻版。從1978年改革開放到上世紀90年代的年畫,邰立平共復刻了400多套版,大概2000塊木版,把散落民間的老畫樣基本全部恢復。他還分別在1994年出版了手工裝訂的《鳳翔木版年畫選》第一卷、1997年出版了第二卷。目前第三卷的出版工作也在緊張進行中。

其次,在創作上,鳳翔年畫風格緊貼時代脈搏。1999年巴黎中國文化周,邰立平創作了活動吉祥物獅子滾繡球版畫,2008年奧運會時創作了福娃主題作品。鳳翔年畫的名氣越來越大。邰立平先後應邀在澳大利亞、德國、法國等多個國家以及國內各大美院和美術館參展,其作品也陸續被中國國家博物館和國內外200多家藝術學院與機構收藏。

再次,從經營的角度,邰立平也不斷進行着適應市場的改變。傳統年畫的尺寸和包裝都不適應現代都市人的需求,他們專門推出了適合張貼在城市單扇門上的小門神年畫,對年畫的包裝進行變革,更能使現代人喜歡和接受。

這些年邰立平一直在探索如何讓年畫走進千家萬戶,使老百姓能真正用起來。他參与非遺進校園活動;他和國內知名設計師合作,推出了雕刻時光日曆;和中國手藝網和雅昌合作,推出了鳳翔木版年畫年曆;和騰訊、京東合作,跨界進入手游領域,讓年輕人在玩遊戲時也能體驗國潮風。(鄭芋)

本站聲明:網站內容來http://www.societynews.cn/html/wh/fq/,如有侵權,請聯繫我們,我們將及時處理

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

上新台中搬家公司提供您一套專業有效率且人性化的辦公室搬遷、公司行號搬家及工廠遷廠的搬家服務

透過 NestedScrollView 源碼解析嵌套滑動原理,Android View 的事件分發原理解析_貨運

※智慧手機時代的來臨,RWD網頁設計為架站首選

網動結合了許多網際網路業界的菁英共同研發簡單易操作的架站工具,及時性的更新,為客戶創造出更多的網路商機。

NestedScrollView 是用於替代 ScrollView 來解決嵌套滑動過程中的滑動事件的衝突。作為開發者,你會發現很多地方會用到嵌套滑動的邏輯,比如下拉刷新頁面,京東或者淘寶的各種商品頁面。

那為什麼要去了解 NestedScrollView 的源碼呢?那是因為 NestedScrollView 是嵌套滑動實現的模板範例,通過研讀它的源碼,能夠讓你知道如何實現嵌套滑動,然後如果需求上 NestedScrollView 無法滿足的時候,你可以自定義。

嵌套滑動

說到嵌套滑動,就得說說這兩個類了:NestedScrollingParent3 和 NestedScrollingChild3 ,當然同時也存在後面不帶数字的類。之所以後面帶数字了,是為了解決之前的版本遺留的問題:fling 的時候涉及嵌套滑動,無法透傳到另一個View 上繼續 fling,導致滑動效果大打折扣 。

其實 NestedScrollingParent2 相比 NestedScrollingParent 在方法調用上多了一個參數 type,用於標記這個滑動是如何產生的。type 的取值如下:

    /**
     * Indicates that the input type for the gesture is from a user touching the screen. 觸摸產生的滑動
     */
    public static final int TYPE_TOUCH = 0;

    /**
     * Indicates that the input type for the gesture is caused by something which is not a user
     * touching a screen. This is usually from a fling which is settling.  簡單理解就是fling
     */
    public static final int TYPE_NON_TOUCH = 1;

嵌套滑動,說得通俗點就是子 view 和 父 view 在滑動過程中,互相通信決定某個滑動是子view 處理合適,還是 父view 來處理。所以, Parent 和 Child 之間存在相互調用,遵循下面的調用關係:

上圖可以這麼理解:

  • ACTION_DOWN 的時候子 view 就要調用 startNestedScroll( ) 方法來告訴父 view 自己要開始滑動了(實質上是尋找能夠配合 child 進行嵌套滾動的 parent),parent 也會繼續向上尋找能夠配合自己滑動的 parent,可以理解為在做一些準備工作 。
  • 父 view 會收到 onStartNestedScroll 回調從而決定是不是要配合子 view 做出響應。如果需要配合,此方法會返回 true。繼而 onStartNestedScroll()回調會被調用。
  • 在滑動事件產生但是子 view 還沒處理前可以調用 dispatchNestedPreScroll(0,dy,consumed,offsetInWindow) 這個方法把事件傳給父 view,這樣父 view 就能在onNestedPreScroll 方法裏面收到子 view 的滑動信息,然後做出相應的處理把處理完后的結果通過 consumed 傳給子 view。

  • dispatchNestedPreScroll()之後,child可以進行自己的滾動操作。

  • 如果父 view 需要在子 view 滑動后處理相關事件的話可以在子 view 的事件處理完成之後調用 dispatchNestedScroll 然後父 view 會在 onNestedScroll 收到回調。

  • 最後,滑動結束,調用 onStopNestedScroll() 表示本次處理結束。

  • 但是,如果滑動速度比較大,會觸發 fling, fling 也分為 preFling 和 fling 兩個階段,處理過程和 scroll 基本差不多。 

NestedScrollView

首先是看類的名字

 class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
 NestedScrollingChild3, ScrollingView {

可以發現它繼承了 FrameLayout,相當於它就是一個 ViewGroup,可以添加子 view , 但是需要注意的事,它只接受一個子 view,否則會報錯。

    @Override
    public void addView(View child) {
        if (getChildCount() > 0) {
            throw new IllegalStateException("ScrollView can host only one direct child");
        }

        super.addView(child);
    }

    @Override
    public void addView(View child, int index) {
        if (getChildCount() > 0) {
            throw new IllegalStateException("ScrollView can host only one direct child");
        }

        super.addView(child, index);
    }

    @Override
    public void addView(View child, ViewGroup.LayoutParams params) {
        if (getChildCount() > 0) {
            throw new IllegalStateException("ScrollView can host only one direct child");
        }

        super.addView(child, params);
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        if (getChildCount() > 0) {
            throw new IllegalStateException("ScrollView can host only one direct child");
        }

        super.addView(child, index, params);
    }

add view

對於 NestedScrollingParent3,NestedScrollingChild3 的作用,前文已經說了,如果還是不理解,後面再對源碼的分析過程中也會分析到。

其實這裏還可以提一下 RecyclerView:

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {

這裏沒有繼承 NestedScrollingParent3 是因為開發者覺得 RecyclerView 適合做一個子類。並且它的功能作為一個列表去展示,也就是不適合再 RecyclerView 內部去做一些複雜的嵌套滑動之類的。這樣 RecycylerView 外層就可以再嵌套一個 NestedScrollView 進行嵌套滑動了。後面再分析嵌套滑動的時候,也會把 RecycylerView 當作子類來進行分析,這樣能更好的理解源碼。

內部有個接口,使用者需要對滑動變化進行監聽的,可以添加這個回調:

    public interface OnScrollChangeListener {
        /**
         * Called when the scroll position of a view changes.
         *
         * @param v The view whose scroll position has changed.
         * @param scrollX Current horizontal scroll origin.
         * @param scrollY Current vertical scroll origin.
         * @param oldScrollX Previous horizontal scroll origin.
         * @param oldScrollY Previous vertical scroll origin.
         */
        void onScrollChange(NestedScrollView v, int scrollX, int scrollY,
                int oldScrollX, int oldScrollY);
    }

構造函數

下面來看下構造函數:

    public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
            int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initScrollView();

        final TypedArray a = context.obtainStyledAttributes(
                attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0);
        // 是否要鋪滿全屏
        setFillViewport(a.getBoolean(0, false));

        a.recycle();
        // 即是子類,又是父類
        mParentHelper = new NestedScrollingParentHelper(this);
        mChildHelper = new NestedScrollingChildHelper(this);

        // ...because why else would you be using this widget? 默認是滾動,不然你使用它就沒有意義了
        setNestedScrollingEnabled(true);

        ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE);
    }    

這裏我們用了兩個輔助類來幫忙處理嵌套滾動時候的一些邏輯處理,NestedScrollingParentHelper,NestedScrollingChildHelper。這個是和前面的你實現的接口 NestedScrollingParent3,NestedScrollingChild3 相對應的。

下面看下  initScrollView 方法里的具體邏輯:

    private void initScrollView() {
        mScroller = new OverScroller(getContext());
        setFocusable(true);
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
     // 會調用 ViewGroup 的 onDraw setWillNotDraw(
false); // 獲取 ViewConfiguration 中一些配置,包括滑動距離,最大最小速率等等 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = configuration.getScaledTouchSlop(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); }

setFillViewport

在構造函數中,有這麼一個設定:

setFillViewport(a.getBoolean(0, false));

與 setFillViewport 對應的屬性是 android:fillViewport=”true”。如果不設置這個屬性為 true,可能會出現如下圖一樣的問題:

xml 布局:

<?xml version="1.0" encoding="utf-8"?>
<NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:background="#fff000">
        <Button
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </LinearLayout>
</NestedScrollView>

效果:

可以發現這個沒有鋪滿全屏,可是 xml 明明已經設置了 match_parent 了。這是什麼原因呢?

那為啥設置 true 就可以了呢?下面來看下它的 onMeasure 方法:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // false 直接返回
        if (!mFillViewport) {
            return;
        }

        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            return;
        }

        if (getChildCount() > 0) {
            View child = getChildAt(0);
            final NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams();

            int childSize = child.getMeasuredHeight();
            int parentSpace = getMeasuredHeight()
                    - getPaddingTop()
                    - getPaddingBottom()
                    - lp.topMargin
                    - lp.bottomMargin;
            // 如果子 view 高度小於 父 view 高度,那麼需要重新設定高度
            if (childSize < parentSpace) {
                int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin,
                        lp.width);
                // 這裏生成 MeasureSpec 傳入的是 parentSpace,並且用的是 MeasureSpec.EXACTLY 
                int childHeightMeasureSpec =
                        MeasureSpec.makeMeasureSpec(parentSpace, MeasureSpec.EXACTLY);
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

當你將 mFillViewport 設置為 true 后,就會把父 View 高度給予子 view 。可是這個解釋了設置 mFillViewport 可以解決不能鋪滿屏幕的問題,可是沒有解決為啥 match_parent 無效的問題。

在回到類的繼承關係上,NestedScrollView 繼承的是 FrameLayout,也就是說,FrameLayout 應該和 NestedScrollView 擁有一樣的問題。可是當你把 xml 中的布局換成 FrameLayout 后,你發現竟然沒有問題。那麼這是為啥呢?

原因是 NestedScrollView 又重寫了 measureChildWithMargins 。子view 的 childHeightMeasureSpec 中的 mode 是 MeasureSpec.UNSPECIFIED 。當被設置為這個以後,子 view 的高度就完全是由自身的高度決定了。

    @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        // 在生成子 view 的 MeasureSpec 時候,傳入的是 MeasureSpec.UNSPECIFIED
        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

比如子 view 是 LinearLayout ,這時候,它的高度就是子 view 的高度之和。而且,這個 MeasureSpec.UNSPECIFIED 會一直影響着後面的子子孫孫 view 。

我猜這麼設計的目的是因為你既然使用了 NestedScrollView,就沒必要在把子 View  搞得跟屏幕一樣大了,它該多大就多大,不然你滑動的時候,看見一大片空白體驗也不好啊。

而 ViewGroup 中,measureChildWithMargins 的方法是這樣的:

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

由於一般使用 NestedScrollView 的時候,都是會超過屏幕高度的,所以不設置這個屬性為 true 也沒有關係。

※回頭車貨運收費標準

宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念

繪製

既然前面已經把 onMeasure 講完了,那索引把繪製這塊都講了把。下面是 draw 方法,這裏主要是繪製邊界的陰影:

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        if (mEdgeGlowTop != null) {
            final int scrollY = getScrollY();
       // 上邊界陰影繪製
if (!mEdgeGlowTop.isFinished()) { final int restoreCount = canvas.save(); int width = getWidth(); int height = getHeight(); int xTranslation = 0; int yTranslation = Math.min(0, scrollY); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) { width -= getPaddingLeft() + getPaddingRight(); xTranslation += getPaddingLeft(); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) { height -= getPaddingTop() + getPaddingBottom(); yTranslation += getPaddingTop(); } canvas.translate(xTranslation, yTranslation); mEdgeGlowTop.setSize(width, height); if (mEdgeGlowTop.draw(canvas)) { ViewCompat.postInvalidateOnAnimation(this); } canvas.restoreToCount(restoreCount); }
       // 底部邊界陰影繪製
if (!mEdgeGlowBottom.isFinished()) { final int restoreCount = canvas.save(); int width = getWidth(); int height = getHeight(); int xTranslation = 0; int yTranslation = Math.max(getScrollRange(), scrollY) + height; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) { width -= getPaddingLeft() + getPaddingRight(); xTranslation += getPaddingLeft(); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) { height -= getPaddingTop() + getPaddingBottom(); yTranslation -= getPaddingBottom(); } canvas.translate(xTranslation - width, yTranslation); canvas.rotate(180, width, 0); mEdgeGlowBottom.setSize(width, height); if (mEdgeGlowBottom.draw(canvas)) { ViewCompat.postInvalidateOnAnimation(this); } canvas.restoreToCount(restoreCount); } } }

onDraw 是直接用了父類的,這個沒啥好講的,下面看看 onLayout:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        mIsLayoutDirty = false;
        // Give a child focus if it needs it
        if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
            scrollToChild(mChildToScrollTo);
        }
        mChildToScrollTo = null;

        if (!mIsLaidOut) { // 是否是第一次調用onLayout // If there is a saved state, scroll to the position saved in that state.
            if (mSavedState != null) {
                scrollTo(getScrollX(), mSavedState.scrollPosition);
                mSavedState = null;
            } // mScrollY default value is "0"

            // Make sure current scrollY position falls into the scroll range.  If it doesn't,
            // scroll such that it does.
            int childSize = 0;
            if (getChildCount() > 0) {
                View child = getChildAt(0);
                NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams();
                childSize = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            }
            int parentSpace = b - t - getPaddingTop() - getPaddingBottom();
            int currentScrollY = getScrollY();
            int newScrollY = clamp(currentScrollY, parentSpace, childSize);
            if (newScrollY != currentScrollY) {
                scrollTo(getScrollX(), newScrollY);
            }
        }

        // Calling this with the present values causes it to re-claim them
        scrollTo(getScrollX(), getScrollY());
        mIsLaidOut = true;
    }

onLayout 方法也沒什麼說的,基本上是用了父類 FrameLayout 的布局方法,加入了一些 scrollTo 操作滑動到指定位置。

嵌套滑動分析

如果對滑動事件不是很清楚的小夥伴可以先看看這篇文章:Android View 的事件分發原理解析。

在分析之前,先做一個假設,比如 RecyclerView 就是 NestedScrollView 的子類,這樣去分析嵌套滑動更容易理解。這時候,用戶點擊 RecyclerView 觸發滑動。需要分析整個滑動過程的事件傳遞。

dispatchTouchEvent

這裏,NestedScrollView 用的是父類的處理,並沒有添加自己的邏輯。

onInterceptTouchEvent

當事件進行分發前,ViewGroup 首先會調用 onInterceptTouchEvent 詢問自己要不要進行攔截,不攔截,就會分發傳遞給子 view。一般來說,對於 ACTION_DOWN 都不會攔截,這樣子類有機會獲取事件,只有子類不處理,才會再次傳給父 View 來處理。下面來看看其具體代碼邏輯:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        /*
         * This method JUST determines whether we want to intercept the motion.
         * If we return true, onMotionEvent will be called and we do the actual
         * scrolling there.
         */

        /*
        * Shortcut the most recurring case: the user is in the dragging
        * state and he is moving his finger.  We want to intercept this
        * motion.
        */
        final int action = ev.getAction();
     // 如果已經在拖動了,說明已經在滑動了,直接返回 true
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { return true; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { /* * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check * whether the user has moved far enough from his original down touch. */ /* * Locally do absolute value. mLastMotionY is set to the y value * of the down event. */ final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. 不是一個有效的id break; } final int pointerIndex = ev.findPointerIndex(activePointerId); if (pointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + activePointerId + " in onInterceptTouchEvent"); break; } final int y = (int) ev.getY(pointerIndex);
          // 計算垂直方向上滑動的距離
final int yDiff = Math.abs(y - mLastMotionY);
          // 確定可以產生滾動了
if (yDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { mIsBeingDragged = true; mLastMotionY = y; initVelocityTrackerIfNotExists();
            // 可以獲取滑動速率 mVelocityTracker.addMovement(ev); mNestedYOffset
= 0; final ViewParent parent = getParent(); if (parent != null) {
               // 讓父 view 不要攔截,這裏應該是為了保險起見,因為既然已經走進來了,只要你返回 true,父 view 就不會攔截了。 parent.requestDisallowInterceptTouchEvent(
true); } } break; } case MotionEvent.ACTION_DOWN: { final int y = (int) ev.getY();
          // 如果點擊的範圍不在子 view 上,直接break,比如自己設置了很大的 margin,此時用戶點擊這裏,這個範圍理論上是不參与滑動的
if (!inChild((int) ev.getX(), y)) { mIsBeingDragged = false; recycleVelocityTracker(); break; } /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ mLastMotionY = y; mActivePointerId = ev.getPointerId(0);           // 在收到 DOWN 事件的時候,做一些初始化的工作 initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); /* * If being flinged and user touches the screen, initiate drag; * otherwise don't. mScroller.isFinished should be false when * being flinged. We need to call computeScrollOffset() first so that * isFinished() is correct. */ mScroller.computeScrollOffset();
          // 如果此時正在fling, isFinished 會返回 flase mIsBeingDragged
= !mScroller.isFinished();
          // 開始滑動 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: /* Release the drag */ mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; recycleVelocityTracker(); if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); }
          // 手抬起后,停止滑動 stopNestedScroll(ViewCompat.TYPE_TOUCH);
break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } /* * The only time we want to intercept motion events is if we are in the * drag mode. */ return mIsBeingDragged; }

onInterceptTouchEvent 事件就是做一件事,決定事件是不是要繼續交給自己的 onTouchEvent 處理。這裏需要注意的一點是,如果子 view 在 dispatchTouchEvent 中調用了:

parent.requestDisallowInterceptTouchEvent(true)

那麼,其實就不會再調用 onInterceptTouchEvent 方法。也就是說上面的邏輯就不會走了。但是可以發現,down 事件,一般是不會攔截的。但是如果正在 fling,此時就會返回 true,直接把事件全部攔截。

那看下 RecyclerView 的 dispatchTouchEvent 是父類的,沒啥好分析的。而且它的 onInterceptTouchEvent 也是做了一些初始化的一些工作,和 NestedScrollView 一樣沒啥可說的。

onTouchEvent

再說 NestedScrollView 的 onTouchEvent。

對於 onTouchEvent 得分兩類進行討論,如果其子 view 不是 ViewGroup ,且是不可點擊的,就會把事件直接交給 NestedScrollView 來處理。

但是如果點擊的子 view 是 RecyclerView 的 ViewGroup 。當 down 事件來的時候,ViewGroup 的子 view 沒有處理,那麼就會交給 ViewGroup 來處理,你會發現ViewGroup 的 onTouchEvent 是默認返回 true 的。也就是說事件都是由  RecyclerView 來處理的。

這時候來看下 NestedScrollView 的 onTouchEvent 代碼:

 public boolean onTouchEvent(MotionEvent ev) {
        initVelocityTrackerIfNotExists();

        MotionEvent vtev = MotionEvent.obtain(ev);

        final int actionMasked = ev.getActionMasked();

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0;
        }
        vtev.offsetLocation(0, mNestedYOffset);

        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
          // 需要有一個子類才可以進行滑動
if (getChildCount() == 0) { return false; }
          // 前面提到如果用戶在 fling 的時候,觸碰,此時是直接攔截返回 true,自己來處理事件。
if ((mIsBeingDragged = !mScroller.isFinished())) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } /* * If being flinged and user touches, stop the fling. isFinished * will be false if being flinged.處理結果就是停止 fling */ if (!mScroller.isFinished()) { mScroller.abortAnimation(); } // Remember where the motion event started mLastMotionY = (int) ev.getY(); mActivePointerId = ev.getPointerId(0);
         // 尋找嵌套父View,告訴它準備在垂直方向上進行 TOUCH 類型的滑動 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break; } case MotionEvent.ACTION_MOVE: final int activePointerIndex = ev.findPointerIndex(mActivePointerId); if (activePointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); break; } final int y = (int) ev.getY(activePointerIndex); int deltaY = mLastMotionY - y;
          // 滑動前先把移動距離告訴嵌套父View,看看它要不要消耗,返回 true 代表消耗了部分距離
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH)) { deltaY -= mScrollConsumed[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; }
          // 滑動距離大於最大最小觸發距離
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); }
            // 觸發滑動 mIsBeingDragged
= true; if (deltaY > 0) { deltaY -= mTouchSlop; } else { deltaY += mTouchSlop; } } if (mIsBeingDragged) { // Scroll to follow the motion event mLastMotionY = y - mScrollOffset[1]; final int oldY = getScrollY(); final int range = getScrollRange(); final int overscrollMode = getOverScrollMode(); boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); // Calling overScrollByCompat will call onOverScrolled, which // calls onScrollChanged if applicable.
            // 該方法會觸發自身內容的滾動
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, 0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) { // Break our velocity if we hit a scroll barrier. mVelocityTracker.clear(); } final int scrolledDeltaY = getScrollY() - oldY; final int unconsumedY = deltaY - scrolledDeltaY;
            // 通知嵌套的父 View 我已經處理完滾動了,該你來處理了
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset, ViewCompat.TYPE_TOUCH)) {
              // 如果嵌套父View 消耗了滑動,那麼需要更新 mLastMotionY
-= mScrollOffset[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } else if (canOverscroll) { ensureGlows(); final int pulledToY = oldY + deltaY;
               // 觸發邊緣的陰影效果
if (pulledToY < 0) { EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(), ev.getX(activePointerIndex) / getWidth()); if (!mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } } else if (pulledToY > range) { EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(), 1.f - ev.getX(activePointerIndex) / getWidth()); if (!mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onRelease(); } } if (mEdgeGlowTop != null && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { ViewCompat.postInvalidateOnAnimation(this); } } } break; case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
          // 計算滑動速率
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
          // 大於最小的設定的速率,觸發fling
if ((Math.abs(initialVelocity) > mMinimumVelocity)) { flingWithNestedDispatch(-initialVelocity); } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); } mActivePointerId = INVALID_POINTER; endDrag(); break; case MotionEvent.ACTION_CANCEL: if (mIsBeingDragged && getChildCount() > 0) { if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); } } mActivePointerId = INVALID_POINTER; endDrag(); break; case MotionEvent.ACTION_POINTER_DOWN: { final int index = ev.getActionIndex(); mLastMotionY = (int) ev.getY(index); mActivePointerId = ev.getPointerId(index); break; } case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); break; } if (mVelocityTracker != null) { mVelocityTracker.addMovement(vtev); } vtev.recycle(); return true; }

ACTION_DOWN

先看 down 事件,如果處於 fling 期間,那麼直接停止 fling, 接着會調用 startNestedScroll,會讓 NestedScrollView 作為子 view 去 通知嵌套父 view,那麼就需要找到有沒有可以嵌套滑動的父 view 。

    public boolean startNestedScroll(int axes, int type) {
        // 交給 mChildHelper 代理來處理相關邏輯
        return mChildHelper.startNestedScroll(axes, type);
    }


    public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        // 找到嵌套父 view 了,就直接返回
        if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
        // 是否支持嵌套滾動
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {  // while 循環,將支持嵌套滑動的父 View 找出來。
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    // 把父 view 設置進去
                    setNestedScrollingParentForType(type, p);
                    // 找到后,通過該方法可以做一些初始化操作
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }            

可以看到,這時候主要就是為了找到嵌套父 view。當 ViewParentCompat.onStartNestedScroll 返回 true,就表示已經找到嵌套滾動的父 View 了 。下面來看下這個方法的具體邏輯:

    // ViewParentCompat  
    public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes, int type) {
        if (parent instanceof NestedScrollingParent2) {
            // First try the NestedScrollingParent2 API
            return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                    nestedScrollAxes, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            // Else if the type is the default (touch), try the NestedScrollingParent API
            if (Build.VERSION.SDK_INT >= 21) {
                try {
                    return parent.onStartNestedScroll(child, target, nestedScrollAxes);
                } catch (AbstractMethodError e) {
                    Log.e(TAG, "ViewParent " + parent + " does not implement interface "
                            + "method onStartNestedScroll", e);
                }
            } else if (parent instanceof NestedScrollingParent) {
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
        }
        return false;
    }

這裏其實沒啥好分析,就是告訴父類當前是什麼類型的滾動,以及滾動方向。其實這裏可以直接看下 NestedScrollView 的 onStartNestedScroll 的邏輯。

//  NestedScrollView
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
            int type) {
     // 確保觸發的是垂直方向的滾動
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; }

當確定了嵌套父 View 以後,又會調用父 view 的  onNestedScrollAccepted 方法,在這裏可以做一些準備工作和配置。下面我們看到的 是 Ns 裏面的方法,注意不是父 view 的,只是當作參考。

public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
    mParentHelper.onNestedScrollAccepted(child, target, axes, type);
   // 這裏 Ns 作為子 view 調用 該方法去尋找嵌套父 view。注意這個方法會被調用是 NS 作為父 view 收到的。這樣就 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type); }

到這裏,down 的作用就講完了。

ACTION_MOVE 

首先是會調用 dispatchNestedPreScroll,講當前的滑動距離告訴嵌套父 View。

  public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
            int type) {
     // Ns 作為子 view 去通知父View
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); } 

下面看下 mChildHelper 的代碼邏輯:

    /**
     * Dispatch one step of a nested pre-scrolling operation to the current nested scrolling parent.
     *
     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
     * method/{@link androidx.core.view.NestedScrollingChild2} interface method with the same
     * signature to implement the standard policy.</p>
     *
     * @return true if the parent consumed any of the nested scroll
     */
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
       // 獲取之前找到的嵌套滾動的父 View
final ViewParent parent = getNestedScrollingParentForType(type); if (parent == null) { return false; }        // 滑動距離肯定不為0 才有意義 if (dx != 0 || dy != 0) { int startX = 0; int startY = 0; if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); startX = offsetInWindow[0]; startY = offsetInWindow[1]; } if (consumed == null) { if (mTempNestedScrollConsumed == null) { mTempNestedScrollConsumed = new int[2]; } consumed = mTempNestedScrollConsumed; } consumed[0] = 0; consumed[1] = 0;
          // 調用嵌套父 View 的對應的回調 ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); offsetInWindow[0] -= startX; offsetInWindow[1] -= startY; } return consumed[0] != 0 || consumed[1] != 0; } else if (offsetInWindow != null) { offsetInWindow[0] = 0; offsetInWindow[1] = 0; } } return false; }

這裏主要是將滑動距離告訴 父 view,有消耗就會返回 true 。

    // ViewParentCompat
    public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
            int[] consumed) {
        onNestedPreScroll(parent, target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
    }

其實下面的 onNestedPreScroll 跟前面的 onStartNestedScroll 邏輯很像,就是層層傳遞。

    public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
            int[] consumed, int type) {
        if (parent instanceof NestedScrollingParent2) {
            // First try the NestedScrollingParent2 API
            ((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            // Else if the type is the default (touch), try the NestedScrollingParent API
            if (Build.VERSION.SDK_INT >= 21) {
                try {
                    parent.onNestedPreScroll(target, dx, dy, consumed);
                } catch (AbstractMethodError e) {
                    Log.e(TAG, "ViewParent " + parent + " does not implement interface "
                            + "method onNestedPreScroll", e);
                }
            } else if (parent instanceof NestedScrollingParent) {
                ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
            }
        }
    }

下面為了方便,沒法查看 NS 的嵌套父 View 的邏輯。直接看 Ns 中對應的方法。

    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
            int type) {
     // 最終也是 Ns 再傳給其嵌套父 View dispatchNestedPreScroll(dx, dy, consumed,
null, type); }

傳遞完了之後,就會調用  overScrollByCompat 來實現滾動。

    boolean overScrollByCompat(int deltaX, int deltaY,
            int scrollX, int scrollY,
            int scrollRangeX, int scrollRangeY,
            int maxOverScrollX, int maxOverScrollY,
            boolean isTouchEvent) {
        final int overScrollMode = getOverScrollMode();
        final boolean canScrollHorizontal =
                computeHorizontalScrollRange() > computeHorizontalScrollExtent();
        final boolean canScrollVertical =
                computeVerticalScrollRange() > computeVerticalScrollExtent();
        final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS
                || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
        final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS
                || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);

        int newScrollX = scrollX + deltaX;
        if (!overScrollHorizontal) {
            maxOverScrollX = 0;
        }

        int newScrollY = scrollY + deltaY;
        if (!overScrollVertical) {
            maxOverScrollY = 0;
        }

        // Clamp values if at the limits and record
        final int left = -maxOverScrollX;
        final int right = maxOverScrollX + scrollRangeX;
        final int top = -maxOverScrollY;
        final int bottom = maxOverScrollY + scrollRangeY;

        boolean clampedX = false;
        if (newScrollX > right) {
            newScrollX = right;
            clampedX = true;
        } else if (newScrollX < left) {
            newScrollX = left;
            clampedX = true;
        }

        boolean clampedY = false;
        if (newScrollY > bottom) {
            newScrollY = bottom;
            clampedY = true;
        } else if (newScrollY < top) {
            newScrollY = top;
            clampedY = true;
        }

        if (clampedY && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
            mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange());
        }
     
        onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);

        return clampedX || clampedY;
    }

整塊邏輯其實沒啥好說的,然後主要是看 onOverScrolled 這個方法:

   protected void onOverScrolled(int scrollX, int scrollY,
            boolean clampedX, boolean clampedY) {
        super.scrollTo(scrollX, scrollY);
    }

最終是調用 scrollTo 方法來實現了滾動。

當滾動完了后,會調用 dispatchNestedScroll 告訴父 view 當前還剩多少沒消耗,如果是 0,那麼就不會上傳,如果沒消耗完,就會傳給父 View 。

如果是子 View 傳給 NS 的,是會通過 scrollBy 來進行消耗的,然後繼續向上層傳遞。

    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed, int type) {
        final int oldScrollY = getScrollY();
        scrollBy(0, dyUnconsumed);
        final int myConsumed = getScrollY() - oldScrollY;
        final int myUnconsumed = dyUnconsumed - myConsumed;
        dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null,
                type);
    }

假設當前已經滑動到頂部了,此時繼續滑動的話,就會觸發邊緣的陰影效果。

ACTION_UP

當用戶手指離開后,如果滑動速率超過最小的滑動速率,就會調用 flingWithNestedDispatch(-initialVelocity) ,下面來看看這個方法的具體邏輯:

    private void flingWithNestedDispatch(int velocityY) {
        final int scrollY = getScrollY();
        final boolean canFling = (scrollY > 0 || velocityY > 0)
                && (scrollY < getScrollRange() || velocityY < 0);
     // fling 前問問父View 要不要 fling, 一般是返回 false
if (!dispatchNestedPreFling(0, velocityY)) {
       // 這裏主要是告訴父類打算自己消耗了 dispatchNestedFling(
0, velocityY, canFling);
       // 自己處理 fling(velocityY); } }

下面繼續看 fling 的實現。

    public void fling(int velocityY) {
        if (getChildCount() > 0) {

            mScroller.fling(getScrollX(), getScrollY(), // start
                    0, velocityY, // velocities
                    0, 0, // x
                    Integer.MIN_VALUE, Integer.MAX_VALUE, // y
                    0, 0); // overscroll
            runAnimatedScroll(true);
        }
    }

    private void runAnimatedScroll(boolean participateInNestedScrolling) {
        if (participateInNestedScrolling) {
            // fling 其實也是一種滾動,只不過是非接觸的
            startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
        } else {
            stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
        }
        mLastScrollerY = getScrollY();
        ViewCompat.postInvalidateOnAnimation(this);
    }

最終會觸發重繪操作,重繪過程中會調用 computeScroll,下面看下其內部的代碼邏輯。

    @Override
    public void computeScroll() {

        if (mScroller.isFinished()) {
            return;
        }

        mScroller.computeScrollOffset();
        final int y = mScroller.getCurrY();
        int unconsumed = y - mLastScrollerY;
        mLastScrollerY = y;

        // Nested Scrolling Pre Pass
        mScrollConsumed[1] = 0;
     // 滾動的時候,依然會把當前的未消耗的滾動距離傳給嵌套父View dispatchNestedPreScroll(
0, unconsumed, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH); unconsumed -= mScrollConsumed[1]; final int range = getScrollRange(); if (unconsumed != 0) { // Internal Scroll final int oldScrollY = getScrollY();
       // 自己消耗 overScrollByCompat(
0, unconsumed, getScrollX(), oldScrollY, 0, range, 0, 0, false); final int scrolledByMe = getScrollY() - oldScrollY; unconsumed -= scrolledByMe; // Nested Scrolling Post Pass mScrollConsumed[1] = 0;
        // 繼續上傳給父View dispatchNestedScroll(
0, scrolledByMe, 0, unconsumed, mScrollOffset, ViewCompat.TYPE_NON_TOUCH, mScrollConsumed); unconsumed -= mScrollConsumed[1]; }      // 如果到這裡有未消耗的,說明已經滾動到邊緣了 if (unconsumed != 0) { final int mode = getOverScrollMode(); final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); if (canOverscroll) { ensureGlows(); if (unconsumed < 0) { if (mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); } } else { if (mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); } } }
       // 停止滾動   abortAnimatedScroll(); }      // 如果此時滾動還未結束,並且當前的滑動距離都被消耗了,那麼繼續刷新滾動,直到停止為止
if (!mScroller.isFinished()) { ViewCompat.postInvalidateOnAnimation(this); } }

到這裏,關於 Ns 的嵌套滑動就講完了。希望大家能夠對嵌套滑動有個理解。

閱讀 Ns 的源碼,可以讓你更好的理解嵌套滑動,以及事件分發的邏輯。

  本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※評比南投搬家公司費用收費行情懶人包大公開

搬家價格與搬家費用透明合理,不亂收費。本公司提供下列三種搬家計費方案,由資深專業組長到府估價,替客戶量身規劃選擇最經濟節省的計費方式

【Python】組合數據類型_網頁設計公司

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

搬家費用:依消費者運送距離、搬運樓層、有無電梯、步行距離、特殊地形、超重物品等計價因素後,評估每車次單

集合類型

集合類型定義

集合是多個元素的無序組合

  • 集合類型與數學中的集合概念一致
  • 集合元素之間無序,每個元素唯一,不存在相同元素
  • 集合元素不可更改,不能是可變數據類型

    理解:因為集合類型不重複,所以不能更改,否則有可能重複。

集合是多個元素的無序組合

  • 集合用大括號 {} 表示,元素間用逗號分隔
  • 建立集合類型用 {}set()
  • 建立空集合類型,必須使用set()

集合操作符

操作符及應用 描述
S | T 並,返回一個新集合,包括在集合S和T中的所有元素
S – T 差,返回一個新集合,包括在集合S但不在T中的元素
S & T 交,返回一個新集合,包括同時在集合S和T中的元素
S ^ T 補,返回一個新集合,包括集合S和T中的非相同元素
S <= T 返回True/False,判斷S和T的子集關係
S < T 返回True/False,判斷S和T的子集關係
S >= T 返回True/False,判斷S和T的包含關係
S > T 返回True/False,判斷S和T的包含關係
S |= T 並,更新集合S,包括在集合S和T中的所有元素
S -= T 差,更新集合S,包括在集合S但不在T中的元素
S &= T 交,更新集合S,包括同時在集合S和T中的元素
S ^= T 補,更新集合S,包括集合S和T中的非相同元素

集合處理方法

操作函數或方法 描述
S.add(x) 如果x不在集合S中,將x增加到S
S.discard(x) 移除S中元素x,如果x不在集合S中,不報錯
S.remove(x) 移除S中元素x,如果x不在集合S中,產生KeyError異常
S.clear() 移除S中所有元素
S.pop() 隨機返回S的一個元素,更新S,若S為空產生KeyError異常
S.copy() 返回集合S的一個副本
len(S) 返回集合S的元素個數
x in S 判斷S中元素x,x在集合S中,返回True,否則返回False
x not in S 判斷S中元素x,x不在集合S中,返回True,否則返回False
set(x) 將其他類型變量x轉變為集合類型

集合類型應用場景

數據去重:集合類型所有元素無重複

序列類型

序列類型定義

序列是具有先後關係的一組元素

  • 序列是一維元素向量,元素類型可以不同
  • 類似數學元素序列: s0, s1, … , sn-1
  • 元素間由序號引導,通過下標訪問序列的特定元素

序列處理函數及方法

操作符及應用 描述
x in s 如果x是序列s的元素,返回True,否則返回False
x not in s 如果x是序列s的元素,返回False,否則返回True
s + t 連接兩個序列s和t
s*n 或 n*s 將序列s複製n次
s[i] 索引,返回s中的第i個元素,i是序列的序號
s[i: j]
s[i: j: k]
切片,返回序列s中第i到j以k為步長的元素子序列
函數和方法 描述
len(s) 返回序列s的長度,即元素個數
min(s) 返回序列s的最小元素,s中元素需要可比較
max(s) 返回序列s的最大元素,s中元素需要可比較
s.index(x)
s.index(x, i, j)
返回序列s從i開始到j位置中第一次出現元素x的位置
s.count(x) 返回序列s中出現x的總次數

元組類型及操作

元組是序列類型的一種擴展

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

節能減碳愛地球是景泰電動車的理念,是創立景泰電動車行的初衷,滿意態度更是服務客戶的最高品質,我們的成長來自於你的推薦。

  • 元組是一種序列類型,一旦創建就不能被修改
  • 使用小括號 ()tuple() 創建,元素間用逗號 , 分隔
  • 可以使用或不使用小括號

元組繼承序列類型的全部通用操作

  • 元組繼承了序列類型的全部通用操作
  • 元組因為創建后不能修改,因此沒有特殊操作
  • 使用或不使用小括號

列表類型及操作

列表是序列類型的一種擴展,十分常用

  • 列表是一種序列類型,創建后可以隨意被修改
  • 使用方括號 [] 或list() 創建,元素間用逗號 , 分隔
  • 列表中各元素類型可以不同,無長度限制
函數或方法 描述
ls[i] = x 替換列表ls第i元素為x
ls[i: j: k] = lt 用列表lt替換ls切片后所對應元素子列表
del ls[i] 刪除列表ls中第i元素
del ls[i: j: k] 刪除列表ls中第i到第j以k為步長的元素
ls += lt 更新列表ls,將列表lt元素增加到列表ls中
ls *= n 更新列表ls,其元素重複n次
函數或方法 描述
ls.append(x) 在列表ls最後增加一個元素x
ls.clear() 刪除列表ls中所有元素
ls.copy() 生成一個新列表,賦值ls中所有元素
ls.insert(i,x) 在列表ls的第i位置增加元素x
ls.pop(i) 將列表ls中第i位置元素取出並刪除該元素
ls.remove(x) 將列表ls中出現的第一個元素x刪除
ls.reverse() 將列表ls中的元素反轉

序列類型應用場景

數據表示:元組 和 列表

  • 元組用於元素不改變的應用場景,更多用於固定搭配場景
  • 列表更加靈活,它是最常用的序列類型
  • 最主要作用:表示一組有序數據,進而操作它們

元素遍歷

數據保護

  • 如果不希望數據被程序所改變,轉換成元組類型

字典

字典類型定義

  • 映射是一種鍵(索引)和值(數據)的對應
  • 鍵值對:鍵是數據索引的擴展
  • 字典是鍵值對的集合,鍵值對之間無序
  • 採用大括號{}dict()創建,鍵值對用冒號: 表示

{<鍵1>:<值1>, <鍵2>:<值2>, … , <鍵n>:<值n>}

<字典變量> = {<鍵1>:<值1>, … , <鍵n>:<值n>}
<值> = <字典變量>[<鍵>]
<字典變量>[<鍵>] = <值>
[ ] 用來向字典變量中索引或增加元素

字典處理函數及方法

函數或方法 描述
del d[k] 刪除字典d中鍵k對應的數據值
k in d 判斷鍵k是否在字典d中,如果在返回True,否則False
d.keys() 返回字典d中所有的鍵信息
d.values() 返回字典d中所有的值信息
d.items() 返回字典d中所有的鍵值對信息
d.get(k, <default>) 鍵k存在,則返回相應值,不在則返回 值
d.pop(k, <default>) 鍵k存在,則取出相應值,不在則返回 值
d.popitem() 隨機從字典d中取出一個鍵值對,以元組形式返回
d.clear() 刪除所有的鍵值對
len(d) 返回字典d中元素的個數

字典類型應用場景

映射的表達

  • 映射無處不在,鍵值對無處不在
  • 例如:統計數據出現的次數,數據是鍵,次數是值
  • 最主要作用:表達鍵值對數據,進而操作它們

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

透過選單樣式的調整、圖片的縮放比例、文字的放大及段落的排版對應來給使用者最佳的瀏覽體驗,所以不用擔心有手機版網站兩個後台的問題,而視覺效果也是透過我們前端設計師優秀的空間比例設計,不會因為畫面變大變小而影響到整體視覺的美感。

文物里的唐都長安人生活_貨運

※回頭車貨運收費標準

宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念

  展出的文官俑。

※智慧手機時代的來臨,RWD網頁設計為架站首選

網動結合了許多網際網路業界的菁英共同研發簡單易操作的架站工具,及時性的更新,為客戶創造出更多的網路商機。

  参觀者在觀展。

  近日,陝西西安博物院“樂居長安—唐都長安人的生活”展開展。本次展覽從西安博物院11萬餘件館藏文物中,特別遴選出280餘件/組唐代精品文物,分別圍繞唐長安的“城、人、衣、食、行、娛”等內容,對唐都長安人的生活進行了全面的再現和闡釋。新華社記者 劉瀟攝

本站聲明:網站內容來http://www.societynews.cn/html/wh/fq/,如有侵權,請聯繫我們,我們將及時處理

※評比南投搬家公司費用收費行情懶人包大公開

搬家價格與搬家費用透明合理,不亂收費。本公司提供下列三種搬家計費方案,由資深專業組長到府估價,替客戶量身規劃選擇最經濟節省的計費方式

盛世修典,築起民間文化長城_網頁設計公司

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

節能減碳愛地球是景泰電動車的理念,是創立景泰電動車行的初衷,滿意態度更是服務客戶的最高品質,我們的成長來自於你的推薦。

  民間藝人在進行藝術表演。資料圖片

  中國民間文學大系出版工程首批成果——《中國民間文學大系》12個示範卷 資料圖片

  2019年12月25日,中國民間文學大系出版工程(以下簡稱“大系出版工程”)首批成果發布會在人民大會堂舉行,發布了《中國民間文學大系》(以下簡稱《大系》)的12個示範卷,涉及神話、史詩、傳說、故事、歌謠、長詩、說唱、小戲、諺語、謎語、俗語和理論12個門類,共計1200多萬字。作為大系出版工程的成果,《大系》文庫是我國有史以來記錄民間文學數量最多、內容最豐富、種類最齊全、形式最多樣、最具活態性的文庫。

  最大規模的民間文學出版工程

  2017年1月,中辦國辦印發《關於實施中華優秀傳統文化傳承發展工程的意見》。作為《關於實施中華優秀傳統文化傳承發展工程的意見》的15個重點工程之一,大系出版工程在中宣部、中國文聯的領導下,由中國民間文藝家協會團結民間文學領域的專家學者具體實施。

  《大系》涉及神話、史詩、傳說、故事、歌謠、長詩、說唱、小戲、諺語、謎語、俗語、理論12大門類。首批出版的12個示範卷各門類分別一本,每本100萬字左右,共計1200多萬字、300餘幅圖片。12個示範卷分別為《神話·雲南卷(一)》《史詩·黑龍江卷·伊瑪堪分卷》《傳說·吉林卷(一)》《故事·河南卷·平頂山分卷》《歌謠·四川卷·漢族分卷》《長詩·雲南卷(一)》《說唱·遼寧卷(一)》《小戲·湖南卷·影戲分卷》《諺語·河北卷》《謎語·河南卷(一)》《俗語·江蘇卷(一)》《理論(2000—2018)·第一卷(總論)》。

  《大系》所收作品按照科學性、廣泛性、地域性、代表性的原則編選,在田野普查、文字記錄、圖片拍攝和音頻視頻等信息採集以及查閱大量歷史資料的基礎上,強調學術規範,把握民間文學的“活態性、生活性、歷史性和文化性”,注重《大系》內容的全面性、代表性、真實性,多維度、多向度、全方位展現了民間文學的歷史風貌與新時代人文精神。

  示範卷在內容、形式、類型等方面力求反映出民族風格和文化底蘊。比如,《長詩·雲南卷(一)》編選了彝、白、哈尼、傣、壯、苗、傈僳、拉祜、納西、瑤、藏、基諾等12個民族的30部反映婚姻愛情的敘事長詩,這些作品大多採集於20世紀五六十年代,演唱者多為少數民族歌手和民間藝人,並且首次將《宛納帕麗》《南波冠》《葫蘆信》校正為傣族“三大愛情悲劇”;《傳說·吉林卷(一)》中的180餘篇作品,均取自原始採集的資料,在文本規範上進行了重新梳理並增加註釋,盡可能地還原吉林地方文化特色和民間韻味,其中的人蔘傳說、漁獵傳說、淘金傳說和木幫傳說等都是吉林省的特色文化。

  《大系》文庫既有精緻的傳統紙媒產品,也在書中以二維碼的形式鏈接相關民間文學音視頻,拓展了紙質書的內容維度,從而演示活態傳承樣本。比如,在《史詩·黑龍江卷·伊瑪堪分卷》中,讀者可以通過視頻欣賞赫哲語說唱,了解被聯合國教科文組織列為“急需保護的非物質文化遺產名錄”的赫哲族古代部落時期關於征戰、遷徙、社會、生活等英雄史詩;《小戲·湖南卷·影戲分卷》收錄了“儀式性”“非儀式性”劇本及“混合本”135個,建立了視頻資料庫,以最大程度保留和還原各區縣小戲的地方韻味,並通過地域腔調延續歷史文脈。

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

透過選單樣式的調整、圖片的縮放比例、文字的放大及段落的排版對應來給使用者最佳的瀏覽體驗,所以不用擔心有手機版網站兩個後台的問題,而視覺效果也是透過我們前端設計師優秀的空間比例設計,不會因為畫面變大變小而影響到整體視覺的美感。

  續存民族文化的集體記憶

  盛世修典是我國自古以來的文化傳統,從《詩經》到《樂府》,從《史記》到《四庫全書》,都為中華民族文化的延續作出了獨特貢獻。編纂出版民間文學大系的意義在於續存民族文化的集體記憶,傳承民族發展的文化基因,並努力實現從立檔存志、強基固本到實現中華優秀傳統文化創造性轉化、創新性發展的銜接和提升,進而築牢中華文化共同體。

  此次出版的《大系》“神話卷”,透過“神話中國”的視角展現了中華文明的構成;“史詩卷”中代代相傳的民族史詩,蘊含珍貴的集體記憶,也是民族語言的歷史文本;“傳說卷”里豐富的民間敘事,包含民間的價值理想、生活哲學;“故事卷”里的民間故事在變化的語境中呈現了歷史經驗、文化律動中永恆不變的存在;“歌謠卷”展現了生活之歌、自然之歌,是精神情感的記錄,也是中華民族語言的瑰寶;“長詩卷”在深入發掘和打撈的過程中萃取經典,體現了長詩佳作的魅力;“說唱卷”作品中樸實的語言、真摯的情感、鮮明的個性,展示了說唱文學演繹故事、塑造典型、表達心靈乃至揭示人性的力量;“小戲卷”讓人們進一步認識和體會民間小戲的審美品格、文化價值;“諺語卷”是文學樣式、文化現象的綜合呈現,短小精悍且充滿了生產生活的智慧;“謎語卷”全面展示了跟謎語相關的文化景觀,許多資料難得而又珍貴;“俗語卷”的作品反映民俗生活,具有地方風情,是對民間口頭語言的發掘梳理和研究;“理論卷”是21世紀以來我國民間文學界第一次對最新的理論研究成果進行大規模收集、整理、編纂、回顧。總之,口耳相傳的民間文學既是民族文化的活化石,又是一部發展中的民族生活史、文化史、思想史,聯繫着民族文化的源頭並指向廣闊的未來。

  文學總體上分為兩種:一種是個人用文字創作的,以書面傳播的文學;另一種是民間集體口頭創作的,口口相傳的民間文學。後者是前者的源頭,是根性的文學。中國民間文學大系,強調文學的民間性,反映的是中國社會生活的面貌。從某種程度上說,民間文學大系就是我們民間生活的百科全書,包含民俗學、歷史學、藝術學等學科內容,蘊含豐富的史料細節,可以為民族學、民俗學的研究提供基礎性資料和基本理論,可以作為人文社會研究的基礎文獻,也可以作為教材的資料基礎,有助於生動傳承民族文化,增強中華民族的文化認同感和凝聚力。

  民間文學研究整理的總動員

  中國民間文學大系出版工程是在中國民間文藝家協會70年文獻積累的基礎上實施的。中國民協的前身是成立於1950年的中國民間文學研究會,70年來民間文學一直是其關注重點。新中國成立以來,中國民協(包括其前身“民研會”)開展了3次大規模的民間文學搶救性調查、收集、整理工作,這包括1957年的民歌調查運動、20世紀80年代的中國民間文學“三套集成”(《中國民間故事集成》《中國歌謠集成》《中國諺語集成》)普查編纂工作和始自2002年的中國民間文化遺產搶救工程。

  在數十年採集整理民間文學資料的基礎上,中國民協組織實施中國民間文學大系出版工程,進一步對“中國口頭文學遺產数字化工程”数字化搶救和整理的11000餘冊、約18億字資料進行研究、整理和編纂,並補充和完善新世紀以來的民間文學作品。

  中國民間文學大系出版工程啟動之初,我們便成立了“大系出版工程”學術委員會、編纂出版工作委員會及12個編輯專家組,以把握民間文學的實質,尊重民間文學的規律,保障編纂出版的質量和水平。工程的實施以中國民協為主,同時各級民協上下聯動,充分調動高等院校、科研院所及有關部門和機構參与的积極性,團結全國各地近千名專家學者參与編纂,凝聚了一批民間文學的專家學者和愛好者,培養了一批有能力有擔當的民間文學梯隊人才。

  大系出版工程從啟動伊始就確立了“示範帶動”的方法。一方面,在具有突出優勢的省區市部署共計55個示範卷的編纂任務;另一方面,形成了《〈中國民間文學大系〉編纂工作規範及實施辦法》《中國民間文學大系授權書》《中國民間文學大系出版工程編纂出版工作流程和相關職責》《中國民間文學大系辦公室工作分工》《中國民間文學大系出版工程相關簡稱使用規範》等系統的工作規程,以保證各項工作科學規範開展。

  《大系》編纂過程中嚴守學術規範,尊重民間文化的發展規律,關注民間文學的“活態性、生活性、歷史性和文化性”,注重大系的全面性、代表性、真實性。同時,我們還不間斷地開展研討,舉辦培訓講座,僅2018年8月以來,就在各省區市召開示範卷編纂工作啟動會、座談會、研討會20餘次,保證了《大系》內容的學術性、專業性。

  經過近三年的辛勤工作,大系出版工程取得了顯著成果。截至2019年12月17日,全國共有134卷啟動了編纂工作,其中12個示範卷已經面世,還有34卷已進入審稿、修改階段,1卷已進入出版社編校環節,其餘卷本正在補充和修改。根據規劃,大系出版工程將在2025年前出版《中國民間文學大系》大型文庫,建成电子文獻數據庫,同時開發一批經典讀本、實用讀本、普及讀本和對外宣傳推介產品和衍生產品。

  (作者:潘魯生,系中國文聯副主席、中國民間文藝家協會主席、中國民間文學大系出版工程編纂出版工作委員會主任)

本站聲明:網站內容來http://www.societynews.cn/html/wh/fq/,如有侵權,請聯繫我們,我們將及時處理

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

搬家費用:依消費者運送距離、搬運樓層、有無電梯、步行距離、特殊地形、超重物品等計價因素後,評估每車次單

國外用戶抱怨 AirPods Max 長時間使用後,耳罩裡面會出現水滴_包裝設計

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

AirPods Max 台灣要等到明年 2 月才會到貨,但國外第一批購買者早在 12/15 就已經拿到,離現在也過了半個月,果然不出所料,新產品多少都有些問題,最近就有多位用戶在 Reddit 論壇上反應,他的 AirPods Max 使用一段時間後,耳罩內會出現水滴,雖然目前還沒有引起任何故障,但畢竟 AirPods Max 不防水,還是會讓人擔心這個現象。

國外用戶抱怨 AirPods Max 長時間使用後,耳罩裡面會出現水滴

近日一名 Donald_Filimon 網友在 Reddit 論壇上抱怨,長時間使用 AirPods Max 之後,耳罩裡面都會有多個凝結小水滴。他沒有在潮濕的環境使用過,主要都是坐在辦公桌前,如果不是某次耳機檢測不正常,他也不知道裡面發生這種情況:

從他 Twitter 推文圖片可以看到,這凝結小水滴數量還真的不是普通的多,感覺很像流汗:

So, uhh… my AirPods Max form condensation after extended use. They’ve never been used in any humid environment. The water gets inside the drivers and has caused ear detection problems. I’ve been wearing them inside sitting at a desk mainly, nothing crazy. Super concerning issue pic.twitter.com/0pWicvxLv9

— Donald Filimon (@donaldfilimon) December 27, 2020

隨後陸續有多位用戶也回報發生同樣狀況像這位 Natural-Peak-4366,他住在佛羅里達州的布雷登頓(Bradenton),使用 AirPods max 長達 2.5 個小時,想清潔耳將耳罩取下後,才注意到裡面有凝結小水滴,他覺得很奇怪,因為他沒有四處走動。這狀況真的嚇到我了:

※產品缺大量曝光嗎?你需要的是一流包裝設計!

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

好消息是,至少這幾位用戶把水滴弄乾之後,AirPods Max 並沒有出現任何故障問題,依舊可以正常使用。

不過不確定這是個案,還是每一個 AirPods Max 都有這狀況,但至少 AirPods Max 的耳罩還蠻容易拆卸的,手邊已經有的人,建議每使用一段時間就打開來擦拭一下,來避免這些凝結小水滴產生更大的問題。

至於導致水滴的原因,有人猜測可能是最近冬天比較冷,房間溫度低,長時間使用才會發生這狀況。

資料來源:Reddit

日本網友異想天開使用民間散熱法 + 散熱片幫 MacBook 散熱,結果螢幕不小心蓋上整個毀了

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

上新台中搬家公司提供您一套專業有效率且人性化的辦公室搬遷、公司行號搬家及工廠遷廠的搬家服務

被纏上了,小王問我怎麼在 Spring Boot 中使用 JDBC 連接 MySQL_貨運

※評比南投搬家公司費用收費行情懶人包大公開

搬家價格與搬家費用透明合理,不亂收費。本公司提供下列三種搬家計費方案,由資深專業組長到府估價,替客戶量身規劃選擇最經濟節省的計費方式

上次幫小王入了 Spring Boot 的門后,他覺得我這個人和藹可親、平易近人,於是隔天小王又微信我說:“二哥,快教教我,怎麼在 Spring Boot 項目中使用 JDBC 連接 MySQL 啊?”

收到問題的時候,我有點頭大,難道以後就要被小王纏上了?

沒等我發牢騷,小王就緊接着說:“二哥,你先別生氣,上次你幫了我的忙后,我在心裏感激了你一晚上,想着第一次遇到這麼親切的大佬,一定要抱緊大腿。。。。。”

馬屁拍到這份上,我的氣自然也就消了。隨後,我花了五分鐘的時間幫他解決了苦惱,沒成想,他又發給我了一個小紅包,表示對我的感謝。並建議我再寫一篇文章出來,因為他覺得像他這樣的小白還有很多。沒辦法,我關上門,開了燈,開始了今天這篇文章的創作。

01、初始化 MySQL 數據庫

既然要連接 MySQL,那麼就需要先在電腦上安裝 MySQL 服務(本文暫且跳過),並且創建數據庫和表。

CREATE DATABASE `springbootdemo`;
DROP TABLE IF EXISTS `mysql_datasource`;
CREATE TABLE `mysql_datasource` (
  `id` varchar(64NOT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB DEFAULT CHARSET=utf8;

02、使用 Spring Initlallzr 創建 Spring Boot 項目

創建一個 Spring Boot 項目非常簡單,通過 Spring Initlallzr(https://start.spring.io/)就可以了。

勾選 Lombok、Web、MySQL Driver、Actuator、JDBC 等五個依賴。

1)Lombok 是一種 Java 實用工具,可用來幫助開發人員消除 Java 的一些冗餘代碼,比如說可以通過註解生成 getter/setter。使用之前需要先在 IDE 中安裝插件。

2)Web 表明該項目是一個 Web 項目,便於我們直接通過 URL 來實操。

3)MySQL Driver:連接 MySQL 服務器的驅動器。

4)Actuator 是 Spring Boot 提供的對應用系統的自省和監控的集成功能,可以查看應用配置的詳細信息,例如自動化配置信息、創建的 Spring beans 以及一些環境屬性等。

5)JDBC:本篇文章我們通過 JDBC 來連接和操作數據庫。

選項選擇完后,就可以點擊【Generate】按鈕生成一個初始化的 Spring Boot 項目了。生成的是一個壓縮包,導入到 IDE 的時候需要先解壓。

03、編輯 application.properties 文件

項目導入成功后,等待 Maven 下載依賴,完成后編輯 application.properties 文件,配置 MySQL 數據源信息。

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springbootdemo
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

1)spring.datasource. 為固定格式。

2)URL 為 MySQL 的連接地址。

3)username 為數據庫的訪問用戶名。

4)password 為數據庫的訪問密碼。

5)driver-class-name 用來指定數據庫的驅動器。也可以不指定,Spring Boot 會根據 URL(有 mysql 關鍵字) 自動匹配驅動器。

※智慧手機時代的來臨,RWD網頁設計為架站首選

網動結合了許多網際網路業界的菁英共同研發簡單易操作的架站工具,及時性的更新,為客戶創造出更多的網路商機。

04、編輯 Spring Boot 項目

為了便於我們操作,我們對 SpringBootMysqlApplication 類進行編輯,增加以下內容。

@SpringBootApplication
@RestController
public class SpringBootMysqlApplication {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @RequestMapping("insert")
    public String insert() {
        String id = UUID.randomUUID().toString();
        String sql = "insert into mysql_datasource (id,name) values ('"+id+"','沉默王二')";
        jdbcTemplate.execute(sql);
        return "插入完畢";
    }

}

1)@SpringBootApplication、@RestController、@RequestMapping 註解在[之前的文章]()中已經介紹過了,這裏不再贅述。

2)@Autowired:顧名思義,用於自動裝配 Java Bean。

3)JdbcTemplate:Spring 對數據庫的操作在 jdbc 上做了深層次的封裝,利用 Spring 的注入功能可以把 DataSource 註冊到 JdbcTemplate 之中。JdbcTemplate 提供了四個常用的方法。

①、execute() 方法:用於執行任何 SQL 語句。

②、update() 方法:用於執行新增、修改、刪除等 SQL 語句。

③、query() 方法:用於執行查詢相關 SQL 語句。

④、call() 方法:用於執行存儲過程、函數相關 SQL 語句。

本例中我們使用 execute() 方法向 mysql_datasource 表中插入一行數據 {id:uuid, name:'沉默王二'}

05、運行 Spring Boot 項目

接下來,我們直接運行 SpringBootMysqlApplication 類,這樣一個 Spring Boot 項目就啟動成功了。

這時候,我們可以直接瀏覽器的 URL 中鍵入 http://localhost:8080/insert 測試 MySQL 的插入語句是否執行成功。很遺憾,竟然出錯了。

該怎麼辦呢?這需要我們在連接字符串中顯式指定時區,修改 spring.datasource.url 為以下內容。

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springbootdemo?serverTimezone=UTC

重新運行該項目后再次訪問,發現數據插入成功了。

為了確保數據是否真的插入成功了,我們通過 Navicat(一款強大的數據庫管理和設計工具)來查看一下。

情況不妙,中文亂碼了。該怎麼辦呢?需要我們在連接字符串中顯式指定字符集,修改 spring.datasource.url 為以下內容。

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springbootdemo?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC

重新運行該項目后再次訪問,發現中文不再亂碼了。

快給自己點個贊。

06、鳴謝

我是沉默王二,一枚有趣的程序員。如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀,回復【666】更有我為你精心準備的 500G 高清教學視頻(已分門別類)。

本文 GitHub 已經收錄,有大廠面試完整考點,歡迎 Star。

原創不易,莫要白票,請你為本文點個贊吧,這將是我寫作更多優質文章的最強動力。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※回頭車貨運收費標準

宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念

這些Java8官方挖過的坑,你踩過幾個?_網頁設計公司

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

搬家費用:依消費者運送距離、搬運樓層、有無電梯、步行距離、特殊地形、超重物品等計價因素後,評估每車次單

導讀:系統啟動異常日誌竟然被JDK吞噬無法定位?同樣的加密方法,竟然出現部分數據解密失敗?往List裏面添加數據竟然提示不支持?日期明明間隔1年卻輸出1天,難不成這是天上人間?1582年神秘消失的10天JDK能否識別?Stream很高大上,List轉Map卻全失敗……這些JDK8官方挖的坑,你踩過幾個? 關注公眾號【碼大叔】,實戰踩坑硬核分享,一起交流!

@

目錄

  • 一、Base64:你是我解不開的迷
  • 二、被吞噬的異常:我不敢說出你的名字
  • 三、日期計算:我想留住時間,讓1天像1年那麼長
  • 四、List:一如你我初見,不增不減
  • 五、Stream處理:給你,獨一無二
  • 六、結尾:紙上得來終覺淺,絕知此事要躬行!
  • 推薦閱讀

一、Base64:你是我解不開的迷

出於用戶隱私信息保護的目的,系統上需將姓名、身份證、手機號等敏感信息進行加密存儲,很自然選擇了AES算法,外面又套了一層Base64,之前用的是sun.misc.BASE64Decoder/BASE64Encoder,網上的資料基本也都是這種寫法,運行得很完美。但這種寫法在idea或者maven編譯時就會有一些黃色告警提示。到了Java 8后,Base64編碼已經成為Java類庫的標準,內置了 Base64 編碼的編碼器和解碼器。於是乎,我手賤地修改了代碼,改用了jdk8自帶的Base64方法

import java.util.Base64;

public class Base64Utils {

    public static final Base64.Decoder DECODER = Base64.getDecoder();
    public static final Base64.Encoder ENCODER = Base64.getDecoder();

    public static String encodeToString(byte[] textByte) {
        return ENCODER.encodeToString(textByte);
    }

    public static byte[] decode(String str) {
        return DECODER.decode(str);
    }

}

程序員的職業操守咱還是有的,構造新老數據、自測、通過,提交測試版本。信心滿滿,我要繼續延續我 0 Bug的神話!然後……然後版本就被打回了。

Caused by: java.lang.IllegalArgumentException: Illegal base64 character 3f
    at java.util.Base64$Decoder.decode0(Base64.java:714)
    at java.util.Base64$Decoder.decode(Base64.java:526)
    at java.util.Base64$Decoder.decode(Base64.java:549)

關鍵是這個錯還很詭異,部分數據是可以解密的,部分解不開

Base64依賴於簡單的編碼和解碼算法,使用65個字符的US-ASCII子集,其中前64個字符中的每一個都映射到等效的6位二進制序列,第65個字符(=)用於將Base64編碼的文本填充到整數大小。後來產生了3個變種:

  • RFC 4648:Basic
    此變體使用RFC 4648和RFC 2045的Base64字母表進行編碼和解碼。編碼器將編碼的輸出流視為一行; 沒有輸出行分隔符。解碼器拒絕包含Base64字母表之外的字符的編碼。​
  • RFC 2045:MIME
    此變體使用RFC 2045提供的Base64字母表進行編碼和解碼。編碼的輸出流被組織成不超過76個字符的行; 每行(最後一行除外)通過行分隔符與下一行分隔。解碼期間將忽略Base64字母表中未找到的所有行分隔符或其他字符。
  • RFC 4648:Url
    此變體使用RFC 4648中提供的Base64字母表進行編碼和解碼。字母表與前面显示的字母相同,只是-替換+和_替換/。不輸出行分隔符。解碼器拒絕包含Base64字母表之外的字符的編碼。
S.N. 方法名稱 & 描述
1 static Base64.Decoder getDecoder()
返回Base64.Decoder解碼使用基本型base64編碼方案。
2 static Base64.Encoder getEncoder()
返回Base64.Encoder編碼使用的基本型base64編碼方案。
3 static Base64.Decoder getMimeDecoder()
返回Base64.Decoder解碼使用MIME類型的base64解碼方案。
4 static Base64.Encoder getMimeEncoder()
返回Base64.Encoder編碼使用MIME類型base64編碼方案。
5 static Base64.Encoder getMimeEncoder(int lineLength, byte[] lineSeparator)
返回Base64.Encoder編碼使用指定的行長度和線分隔的MIME類型base64編碼方案。
6 static Base64.Decoder getUrlDecoder()
返回Base64.Decoder解碼使用URL和文件名安全型base64編碼方案。
7 static Base64.Encoder getUrlEncoder()
返回Base64.Decoder解碼使用URL和文件名安全型base64編碼方案。

關於base64用法的詳細說明,可參考:https://juejin.im/post/5c99b2976fb9a070e76376cc

對於上面的錯誤,網上有的說法是,建議使用Base64.getMimeDecoder()Base64.getMimeEncoder(),對此我只能建議:老的系統如果已經有數據了,就不要使用jdk自帶的Base64了。JDK官方的Base64和sun的base64是不兼容的!不要替換!不要替換!不要替換!

二、被吞噬的異常:我不敢說出你的名字

這個問題理解起來還是蠻費腦子的,所以我把這個系統異常發生的過程提煉成了一個美好的故事,放鬆一下,吟詩一首!

最怕相思濃
一切皆是你
唯獨
不敢說出你的名字
— 碼大叔

這個問題是在使用springboot的註解時遇到的問題,發現JDK在解析註解時,若註解依賴的類定義在JVM加載時不存在,也就是NoClassDefFoundError時,實際拿到的異常將會是ArrayStoreException,而不是NoClassDefFoundError,涉及到的JDK里的類是AnnotationParser.java, 具體代碼如下:

private static Object parseClassArray(int paramInt, ByteBuffer paramByteBuffer, ConstantPool paramConstantPool, Class<?> paramClass) {
    Class[] arrayOfClass = new Class[paramInt];
    int i = 0;
    int j = 0;
    for (int k = 0; k < paramInt; k++){
        j = paramByteBuffer.get();
        if (j == 99) {
            // 注意這個方法
        	arrayOfClass[k] = parseClassValue(paramByteBuffer, paramConstantPool, paramClass);
        } else {
        	skipMemberValue(j, paramByteBuffer);
        	i = 1;
        }
    }
    return i != 0 ? exceptionProxy(j) : arrayOfClass;
}
private static Object parseClassValue(ByteBuffer paramByteBuffer, ConstantPool paramConstantPool, Class<?> paramClass) {
    int i = paramByteBuffer.getShort() & 0xFFFF;
    try
    {
        String str = paramConstantPool.getUTF8At(i);
        return parseSig(str, paramClass);
    } catch (IllegalArgumentException localIllegalArgumentException) {
        return paramConstantPool.getClassAt(i);
    } catch (NoClassDefFoundError localNoClassDefFoundError) {
         // 注意這裏,異常發生了轉化
        return new TypeNotPresentExceptionProxy("[unknown]", localNoClassDefFoundError);
    } catch (TypeNotPresentException localTypeNotPresentException) {
        return new TypeNotPresentExceptionProxy(localTypeNotPresentException.typeName(), localTypeNotPresentException.getCause());
    }
}

parseClassArray這個方法中,預期parseClassValue返回Class對象,但看實際parseClassValue的邏輯,在遇到NoClassDefFoundError時,返回的是TypeNotPresentExceptionProxy,由於類型強轉失敗,最終拋出的是java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy,此時只能通過debug到這行代碼,找到具體是缺少哪個類定義,才能解決這個問題。

筆者重現一下發現這個坑的場景,有三個module,module3依賴module2但未聲明依賴module1,module2依賴module1,但聲明的是optional類型,依賴關係圖如下:

上面每個module中有一個Class,我們命名為ClassInModuleX。ClassInModule3啟動時在註解中使用了ClassInModule2的類,而ClassInModule2這個類的繼承了ClassInModule1,這幾個類的依賴關係圖如下:

如此,其實很容易知道在module運行ClassInModule3時,會出現ClassInModule1的NoClassDefFoundError的,但實際運行時,你能看到的異常將不是NoClassDefFoundError,而是java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy,此時,若想要知道具體是何許異常,需通過debug在AnnotationParser中定位具體問題,以下展示兩個截圖,分別對應系統控制台實際拋出的異常和通過debug發現的異常信息。

控制台異常信息:

注意異常實際在紅色圈圈這裏,自動收縮了,需要展開才可以看到通過debug發現的異常信息:

如果你想體驗這個示例,可關注公眾號碼大叔和筆者交流。如果你下次遇到莫名的java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy,請記得用這個方法定位具體問題。

三、日期計算:我想留住時間,讓1天像1年那麼長

Java8之前日期時間操作相當地麻煩,無論是Calendar還是SimpleDateFormat都讓你覺得這個設計怎麼如此地反人類,甚至還會出現多線程安全的問題,阿里巴巴開發手冊中就曾禁用static修飾SimpleDateFormat。好在千呼萬喚之後,使出來了,Java8帶來了全新的日期和時間API,還帶來了Period和Duration用於時間日期計算的兩個API。

Duraction和Period,都表示一段時間的間隔,Duraction正常用來表示時、分、秒甚至納秒之間的時間間隔,Period正常用於年、月、日之間的時間間隔。

網上的大部分文章也是這麼描述的,於是計算兩個日期間隔可以寫成下面這樣的代碼:

// parseToDate方法作用是將String轉為LocalDate,略。
LocalDate date1 = parseToDate("2020-05-12");
LocalDate date2 = parseToDate("2021-05-13");
// 計算日期間隔
int period = Period.between(date1,date2).getDays();

一個是2020年,一個是2021年,你認為間隔是多少?1年?
恭喜你,和我一起跳進坑裡了(畫外音:裏面的都擠一擠,動一動,又來新人了)。
正確答案應該是:1天。

這個單詞的含義以及這個方法看起來確實是蠻誤導人的,一不注意就會掉進坑裡。Period其實只能計算同月的天數、同年的月數,不能計算跨月的天數以及跨年的月數。

正確寫法1

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

透過選單樣式的調整、圖片的縮放比例、文字的放大及段落的排版對應來給使用者最佳的瀏覽體驗,所以不用擔心有手機版網站兩個後台的問題,而視覺效果也是透過我們前端設計師優秀的空間比例設計,不會因為畫面變大變小而影響到整體視覺的美感。

 long period = date2.toEpochDay()-date1.toEpochDay();

toEpochDay():將日期轉換成Epoch 天,也就是相對於1970-01-01(ISO)開始的天數,和時間戳是一個道理,時間戳是秒數。顯然,該方法是有一定的局限性的

正確寫法2

long period = date1.until(date2,ChronoUnit.DAYS);

使用這個寫法,一定要注意一下date1和date2前後順序:date1 until date2。

正確做法3(推薦)

 long period = ChronoUnit.DAYS.between(date1, date2);

ChronoUnit:一組標準的日期時間單位。這組單元提供基於單元的訪問來操縱日期,時間或日期時間。 這些單元適用於多個日曆系統。這是一個最終的、不可變的和線程安全的枚舉。

看到”適用於多個日曆系統“這句話,我一下子想起來歷史上1582年神秘消失的10天,在JDK8上是什麼效果呢?1582-10-15和1582-10-04你覺得會相隔幾天呢?11天還是1天?有興趣的小夥伴自己去寫個代碼試試吧。

打開你的手機,跳轉到1582年10月,你就能看到這消失的10天了。

四、List:一如你我初見,不增不減

這個問題其實在JDK里存在很多年了,JDK8中依然存在,也是很多人最容易跳的一個坑!直接上代碼:

public List<String> allUser() {
    // 省略
    List<String> currentUserList = getUser();
    currentUserList.add("碼大叔");
    // 省略
}

就是上面這樣一段代碼,往一個list里添加一條數據,你覺得結果是什麼呢?“碼大叔”成功地添加到了List里?天真,不報個錯你怎麼能意識到JDK存在呢。

Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.AbstractList.add(AbstractList.java:148)

原因
因為在getUser方法里,返回的List使用的是Arrays.asList生成的,示例:

    private List<String> getUser(){
        return Arrays.asList("劍聖","小九九");
    }

我們來看看Arrays.asList的源碼

    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }
 private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
   		private final E[] a;
        // 部分代碼略
        ArrayList(E[] array) {
            // 返回的是一個定長的數組
            a = Objects.requireNonNull(array);
        }
        // 部分代碼略
   }

很明顯,返回的實際是一個定長的數組,所以只能“一如你我初見”,初始化什麼樣子就什麼樣子,不能新增,不能減少。如果你理解了,那我們就再來一個栗子

   int[] intArr  = {1,2,3,4,5};
   Integer[] integerArr  = {1,2,3,4,5};
   String[] strArr = {"1", "2", "3", "4", "5"};
   List list1 = Arrays.asList(intArr);
   List list2 = Arrays.asList(integerArr);
   List list3 = Arrays.asList(strArr);
   System.out.println("list1中的數量是:" + list1.size());
   System.out.println("list2中的數量是:" + list2.size());
   System.out.println("list3中的數量是:" + list3.size());

你覺得答案是什麼?預想3秒鐘,揭曉答案,看跟你預想的是否一致呢?

list1中的數量是:1
list2中的數量是:5
list3中的數量是:5

是不是和你預想又不一樣了?還是回到Arrays.asList方法,該方法的輸入只能是一個泛型變長參數。基本類型是不能泛型化的,也就是說8個基本類型不能作為泛型參數,要想作為泛型參數就必須使用其所對應的包裝類型,那前面的例子傳遞了一個int類型的數組,為何程序沒有報編譯錯誤呢?在Java中,數組是一個對象,它是可以泛型化的,也就是說我們的例子是把一個int類型的數組作為了T的類型,所以在轉換后在List中就只有1個類型為int數組的元素了。除了int,其它7個基本類型的數組也存在相似的問題。

JDK里還為我們提供了一個便捷的集合操作工具類Collections,比如多個List合併時,可以使用Collections.addAll(list1,list2), 在使用時也同樣要時刻提醒自己:“請勿踩坑”!

五、Stream處理:給你,獨一無二

Java8中新增了Stream流 ,通過流我們能夠對集合中的每個元素進行一系列并行或串行的流水線操作。當使用一個流的時候,通常包括三個基本步驟:獲取一個數據源(source)→ 數據轉換→執行操作獲取想要的結 果,每次轉換原有 Stream 對象不改變,返回一個新的 Stream 對象(可以有多次轉換),這就允許對其操作可以 像鏈條一樣排列,變成一個管道。

項目上千萬不要使用Stream,因為一旦用起來你會覺得真屏蔽詞爽,根本停不下來。當然不可避免的,還是有一些小坑的。

假設我們分析用戶的訪問日誌,放到list里。

list.add(new User("碼大叔", "登錄公眾號"));
list.add(new User("碼大叔", "編寫文章"));

因為一些原因,我們要講list轉為map,Steam走起來,

private static void convert2MapByStream(List<User> list) {
    Map<String, String> map = list.stream().collect(Collectors.toMap(User::getName, User::getValue));
    System.out.println(map);
}

咣當,掉坑裡了,程序將拋出異常:

Exception in thread "main" java.lang.IllegalStateException: Duplicate key 碼大叔

使用Collectors.toMap() 方法中時,默認key值是不允許重複的。當然,該方法還提供了第三個參數:也就是出現 duplicate key的時候的處理方案

如果在開發的時候就考慮到了key可能重複,你需要在這樣定義convert2MapByStream方法,聲明在遇到重複key時是使用新值還是原有值:

    private static void convert2MapByStream(List<User> list) {
        Map<String, String> map = list.stream().collect(Collectors.toMap(User::getName, User::getValue, (oldVal, newVal) -> newVal));
        System.out.println(map);
    }

關於Stream的坑其實還是蠻多的,比如尋找list中的某個對象,可以使用findAny().get(),你以為是找到就返回找不到就就返回null?依然天真,找不到會拋出異常的,需要使用額外的orElse方法。

六、結尾:紙上得來終覺淺,絕知此事要躬行!

所謂JDK官方的坑,基本上都是因為我們對技術點了解的不夠深入,望文生義,以為是怎樣怎樣的,而實際上我們的自以為是讓我們掉進了一個又一個坑裡。面對着這些坑,我流下了學藝不精的眼淚!但也有些坑,確實發生的莫名其妙,比如吞噬異常,沒有理解JDK為什麼這麼設計。還有些坑,誤導性確實太強了,比如日期計算、list操作等。最後只能說一句:

紙上得來終覺淺,絕知此事要躬行!
編碼不易,且行且珍惜!

推薦閱讀

Try-Catch包裹的代碼異常后,竟然導致了產線事務回滾!
Redis 6.0 新特性-多線程連環13問!
報告老闆,微服務高可用神器已祭出,您花巨資營銷的高流量來了沒?
我成功攻擊了Tomcat服務器,大佬們的反應亮了

公眾號:碼大叔
資深程序員、架構師技術社區
微服務 | 大數據 | 架構設計 | 技術管理
個人微信:itmadashu

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

節能減碳愛地球是景泰電動車的理念,是創立景泰電動車行的初衷,滿意態度更是服務客戶的最高品質,我們的成長來自於你的推薦。

年畫“圈粉”路在何方?_貨運

※評比南投搬家公司費用收費行情懶人包大公開

搬家價格與搬家費用透明合理,不亂收費。本公司提供下列三種搬家計費方案,由資深專業組長到府估價,替客戶量身規劃選擇最經濟節省的計費方式

“如果年畫進行創新,你會喜歡什麼樣的年畫?為什麼會喜歡它?”

“我覺得你一定不是年輕人。”“現在的年輕人多喜歡‘傳統文化’。”……

這是一次有意思的採訪。當記者對遠在廣東佛山的一位“90后”孫璐璐拋出這樣的問題時,就被“呼之欲出”的代溝感震得“花枝凌亂”。自認為通過工作接觸,已經潛入“00后”的審美世界,而又得以在眾“90后”同事的熏陶中了解了年輕人的審美趣味,但這次的採訪才讓記者發覺,過往一切認知都只是皮毛。

孫璐璐說:“現在的年輕人多喜歡‘傳統文化’,覺得李寧酷斃了,漢服超級美,大白兔奶糖的香水、六神花露水的雞尾酒就是流行元素符號……”

從一番激動的言辭中,記者依稀看到當下年輕人對傳統文化那份堅定的認同。這些年,漢服熱、懷舊復古風等潮流風行,讓越來越多的人重新感受到傳統的別樣韻味。傳統文化復蘇、文化自信增強,在這樣的大背景下,作為與傳統文化息息相關的非遺,應該抓住時機,重新綻放新顏。比如,曾在春節年俗中不可或缺的年畫,如何在保留美好文化內涵的前提下,另闢蹊徑,重振春節傳統年俗?

嫁接新平台興起來

在年畫的發展歷史中,也曾起起落落。年畫內容不斷更迭、不斷充盈,跟進時代的步伐。1949年後,年畫曾迎來小陽春。那時,許多專業畫家加入年畫的創作隊伍,打破了舊年畫的一些固定模式,大膽借鑒其他畫種的表現手法,使年畫的面貌煥然一新。

時代在變,人們的審美觀念在變。傳統文化雖好,卻不能囿於過去的框架止步不前,在萬象更新的時代進程中,傳統文化需要年輕化的表達。基於這樣的理念,清華大學等高校與傳承人結對子,走出了一條年畫與時代接軌的新路子。

2019年,在文化和旅遊部非物質文化遺產司的指導與支持下,由傳統工藝與材料研究文化和旅遊部重點實驗室(清華大學)主辦,清華大學美術學院視覺傳達設計系和繪畫系共同承辦的清華大學年畫日新創作營,精選了天津楊柳青、蘇州桃花塢、山東高密、山東楊家埠、陝西鳳翔、河北武強等11個代表性年畫產地的年畫人與設計師、美術創作者組成團隊,在10周的時間里創作出作品48件(套),後期成果將投入商業渠道。

創作營的成果首先在北京國際設計周上展示發布,經過3個月的不懈努力,最終落地京東年貨節。佛山、開封、楊柳青、武強、綿竹、灘頭、高密七大年畫項目帶着濃郁的地域特色,將祥和喜慶的產品帶進電商平台。除了藉助京東非遺頻道銷售年畫和年畫衍生品,“萌萌噠”的年神當起了京東數碼產品的“導購”,一些年畫項目還與戴爾、Kindle、華為等知名品牌深度合作,推出了富有中國風味的春節禮盒。

清華大學美術學院副教授、藝術史論系主任陳岸瑛說,這次嘗試的成功說明古老的年畫並非註定落伍於時代,而是具有強大的發展潛力,特別是在新媒體時代,只要把年畫中的美好寓意挖掘出來,與人民群眾對美好生活的嚮往結合起來,年畫就不愁沒有粉絲。

清華大學年畫日新創作營營員石彥敏創作的楊柳青年畫《連年有餘》

助力老手藝活起來

有一件事曾讓楊柳青木版年畫國家級代表性傳承人霍慶有很有感觸,2015年他在天津圖書館舉辦個人年畫藝術展,來看展覽的人不少,但普遍是中老年人,幾乎沒有年輕人,更別提小朋友了。“這給我一個提示,要想讓年畫藝術傳承發展下去,就應該思考如何把孩子們吸引過來。讓年畫走到孩子身邊,陪伴下一代成長。”霍慶有說。

年畫的遠離,帶來的是年味的變淡,不僅是孩子、年輕人對它越來越陌生,曾經充盈民間的紅火年味也不知所蹤。在過去,過年貼年畫,圖的是喜慶吉利,蘊含的是祈福祝願。在那個穿新衣戴新帽、鞭炮聲聲的年節里,我們用一張張年畫集結普天下的喜慶顏色,把一段叫作“年”的時光裝扮起來。如今,物質生活越來越豐富,傳統的年俗年味卻越來越淡了。

時代的車輪滾滾向前,現代印刷術的出現,令傳統印刷的木版年畫受到了衝擊,有的地區年畫一度失傳。2002年,還是一位生意人的張榮強,因緣際會接到了數十年來投身文化遺產搶救的文化學者馮驥才的電話。那時,馮驥才想去尋找四川夾江年畫,卻發現已經沒有可以製作年畫的師傅了。一直酷愛美術的他,決定將精力投入到拯救夾江年畫上。

2010年,他拜夾江年畫老字號作坊“董大興榮”傳人董貴中為師,希望盡最大努力讓這項傳統文化繼續傳承下去。但這條路走起來比想象中更難。起初,擺在張榮強面前的難題是幾乎沒有可利用的資源,沒有夾江年畫製作流程的文字記載,也沒有會實際操作的師傅,甚至連照片都寥寥無幾。此後,張榮強走訪了大量民間手藝人,並不斷尋找相關資料,經過反覆推敲、多次修改,終於成功恢復了夾江年畫的木版套色印刷技藝,並復刻出《鯉魚跳龍門》《福祿宮》《榮華富貴》《財源湧進》《陳姑趕潘》等近30張經典年畫作品。

※智慧手機時代的來臨,RWD網頁設計為架站首選

網動結合了許多網際網路業界的菁英共同研發簡單易操作的架站工具,及時性的更新,為客戶創造出更多的網路商機。

福美祥作坊創作的灘頭年畫《鼠慶豐年》

轉換新語境潮起來

中國人民大學美學與現代藝術研究所研究員張成源在研究東方美學與年畫的關聯時發現,年畫蘊含的東方美學內涵非常豐富,它包含祈禱、健康、豐收、忠孝等。而這些寓意是古今相通的,留存於每一代國人的思想中。因此,年畫重回春節、重回現代生活無需刻意,只要將這一內涵重新挖掘出來,轉換新的話語形式,年畫依然會受到人們的青睞。

在新一代年畫傳承人中,年畫女俠kk的網名似乎比她劉鍾萍的本名更具傳播效應。她當旅遊講解員時與佛山木版年畫結緣。原先的師兄師姐都各尋出路去了,給師父打下手的劉鍾萍卻留了下來。師父馮炳棠是佛山唯一一位掌握木版年畫全套工序的老藝術家,“唯一”二字讓劉鍾萍在不知不覺中對這份工作有了擔當。師父故去后,她挑起了傳承佛山木版年畫的重任。

年畫需要傳承,也需要創新。為了將傳統年畫與現代需求嫁接,她把傳統技藝與現代文化相結合,創作出“諸神復活”系列年畫,受到了市場的歡迎,而傳統年畫與新潮包裝的結合,也迅速俘獲了年輕人的心。起初有人來求購姻緣年畫,受到啟發,她將喜神和合二仙賦予“脫單神器”的新闡釋,頓時使傳統年畫穿越時空與現代接軌。之後,“一個億小目標的財神”“考神狀元及第逢考必過”“天姬送子兒女雙全的二胎神器”“新房入夥鎮宅神器紫薇大神”等系列相繼面世。

劉鍾萍說:“過去的傳統年畫很難吸引到年輕人,我就讓年畫跟他們的生活發生聯繫,他們帶着願望而來,我就負責把他們的願望通過年畫的寓意傳達出去。”

章昉創作的綿竹年畫《老鼠嫁女》

清華大學美術學院教授唐薇創作的佛山年畫《子開鴻蒙》

傳統文化並不缺市場,缺少的是我們重新打扮它的用心。一個鄉村女孩李子柒,將恬靜、本真的鄉村生活用鏡頭表現出來,在互聯網上贏得了國內外網友廣泛的共鳴。因此,承載着人們對美好未來憧憬和嚮往的年畫也可以迎來複興,只要更多力量聚集於此,並賦予年畫一些創新發展的思路和內容,年畫“圈粉”時機來了。

傳統文化並不缺市場,缺少的是我們重新打扮它的用心。一個鄉村女孩李子柒,將恬靜、本真的鄉村生活用鏡頭表現出來,在互聯網上贏得了國內外網友廣泛的共鳴。因此,承載着人們對美好未來憧憬和嚮往的年畫也可以迎來複興,只要更多力量聚集於此,並賦予年畫一些創新發展的思路和內容,年畫“圈粉”時機來了。

專家觀點

中國人民大學美學與現代藝術研究所研究員 張成源

科技的日新月異不會停下前進的腳步,傳統文化的熏陶也應注重其形式。我們要將其中的倫理道德、家風家訓、行為底線、人格境界提煉出來,用新的形式讓它重新被人們接受。在現代社會中,不能強求年畫的量化,因為傳統技藝無法規模生產,但是年畫衍生品可以。年畫也許是小眾文化,年畫衍生品卻可以是大眾文化。因此,當我們在極力調動眾多力量加入年畫的創新中時,欠缺的不僅是年畫衍生品的創作人才,更重要是的消費人群對它的認識。

中國傳媒大學文化產業管理學院副教授 楊 紅

自2019年開始,圍繞“年畫重回春節”這一主題,非遺傳承人與各類互聯網企業、文化創意團隊合作,開始藉助新媒體開發年畫拼圖微信小遊戲、年畫體驗類H5、年畫賀歲動漫、年畫音頻故事課、年畫微信表情包等多種形態的年畫主題創新應用,傳統年畫成為了春節里熱門的網絡傳播內容。

這些嘗試說明,儘管不少與節日相關的非物質文化遺產在當代的應用場景逐漸消失,但它們卻可以藉助網絡與創意,與我們再次相遇,攜帶着不可或缺的文化意義重回節日場景。在這個過程中,給傳統文化適當“減負”,為文化普及適當“減壓”,讓年畫等越來越多傳統IP在當代實現全民認知與共享,是新時代弘揚中華優秀傳統文化的重要路徑。(杜潔芳 王學思)

本站聲明:網站內容來http://www.societynews.cn/html/wh/fq/,如有侵權,請聯繫我們,我們將及時處理

※回頭車貨運收費標準

宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念