From eba65e4c121d4cbfe67f2a5c4ca1a8d3e4d9ca6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Sat, 5 Jun 2021 22:11:52 +0800 Subject: [PATCH 01/29] =?UTF-8?q?Update=20Android=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E7=82=B9=E6=B1=87=E6=80=BB.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/Docs/Android\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" "b/Docs/Android\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" index 6d5688d..bfcc937 100644 --- "a/Docs/Android\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" +++ "b/Docs/Android\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" @@ -555,7 +555,7 @@ imageView.setOnTouchListener(new View.OnTouchListener() { 如果是监听滑动相关,建议在 ``onTouchEvent`` 中实现,如果要监听双击,那么就使用 ``GestureDectector``。 ## Scroller -弹性滑动对象,用于实现 View 的弹性滑动,**Scroller** 本身无法让 View 弹性滑动,需要和 View 的 ``computeScroll`` 方法配合使用。``startScroll`` 方法是无法让 View 滑动的,``invalidate`` 会导致 View 重绘,重回后会在 ``draw`` 方法中又会去调用 ``computeScroll`` 方法,``computeScroll`` 方法又会去向 Scroller 获取当前的 scrollX 和 scrollY,然后通过 ``scrollTo`` 方法实现滑动,接着又调用 ``postInvalidate`` 方法如此反复。 +弹性滑动对象,用于实现 View 的弹性滑动,**Scroller** 本身无法让 View 弹性滑动,需要和 View 的 ``computeScroll`` 方法配合使用。``startScroll`` 方法是无法让 View 滑动的,``invalidate`` 会导致 View 重绘,重绘后会在 ``draw`` 方法中又会去调用 ``computeScroll`` 方法,``computeScroll`` 方法又会去向 Scroller 获取当前的 scrollX 和 scrollY,然后通过 ``scrollTo`` 方法实现滑动,接着又调用 ``postInvalidate`` 方法如此反复。 ```java Scroller mScroller = new Scroller(mContext); From 9d76096c88884322577982cd3989d31a6f284c05 Mon Sep 17 00:00:00 2001 From: liut Date: Fri, 9 Jul 2021 11:49:56 +0800 Subject: [PATCH 02/29] =?UTF-8?q?Update=20=E6=80=A7=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=9F=A5=E8=AF=86=E7=82=B9=E6=B1=87=E6=80=BB.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 线程中断flag添加volatile --- ...\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/Docs/\346\200\247\350\203\275\344\274\230\345\214\226\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" "b/Docs/\346\200\247\350\203\275\344\274\230\345\214\226\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" index 266b2f8..efb40ab 100644 --- "a/Docs/\346\200\247\350\203\275\344\274\230\345\214\226\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" +++ "b/Docs/\346\200\247\350\203\275\344\274\230\345\214\226\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" @@ -2425,7 +2425,7 @@ Thread使用需要注意的点: ```java public static class CustomThread extends Thread{ private int count=0; - private boolean isCancel = false; + private volatile boolean isCancel = false; @Override public void run() { super.run(); From 8b0a9a082e7c957160593f2ab06e640c7e7d27b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Mon, 9 Aug 2021 20:52:13 +0800 Subject: [PATCH 03/29] Add files via upload --- ...241\344\272\214\347\273\264\347\240\201.jpg" | Bin 0 -> 40759 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 "screenshots/\345\276\256\344\277\241\344\272\214\347\273\264\347\240\201.jpg" diff --git "a/screenshots/\345\276\256\344\277\241\344\272\214\347\273\264\347\240\201.jpg" "b/screenshots/\345\276\256\344\277\241\344\272\214\347\273\264\347\240\201.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..f3f7aeec544667d935dc6bfa7783510747dddf70 GIT binary patch literal 40759 zcmd443qXu%`#=69NhvuLBF3&m2%#vard@}e2T2ISI)soU6*W&-Y)K($9mhF}21zJQ zDMe8%>7=F_iE3)5G)>LSGr#M8X3`vb_ucmU{r~>IecxWotL8b}_kCU0=lXm;mn>ga ziuIT`d+uyZOAEua;6IqG2%CXvcj|=x3;%S6|LSzr(dpb-M_*UBOIHJZ1A}h*-MSfe z@72Swd(ZCOy7lPOqi1hp6B82yqrUz6829UCY+{Ulh?X{dPiGxH9UVPm!)}Jg9sWm_ zjP>lQ6{8iSt@Sh3si&59Pc2y*MuF?p(IWrB6#r;-f@|!e+f`4$8~la19#|(WZS78- zwb50>-}Z<9AM4yxr`J#8f9cYD$$H(NJ&Y$DIC-V(kYDeom@IwAA8Pg6?t^;zefst@ z?LTa|`G}FDtS3&gv7J0+#>`o>9p=oP=eW#ix$}yZt2S)hwAppb)@`19y!Lwg`0hLO z`{5%;{|F2^6?QuOOvImO&t1KC{l?A6TeqVg#6FCB6#w|i)90yaFVZt!zRLXT{fFF- zdHDr}W#!z@6<;d9eybAHH+*jtHi?>B(0yrP+Vb0i|CQgE+I{te`|8xWvvy})bYEJX zyx~i`XJ?(C#&_xU%M#u79=(5_aG4HKYSBk$~= z-Ppl@b!R_rtnI!EF#~NaczN1AF)Ai?yc#qD`@j2DVK>rmDL*5{z0)?v1i_g|>G85> zHkslzbp;j7tcGE8!afJC%w9XwY-IXwpU!V}ooD$)%P=Dc>7VScH)U9b_A(jvc^kFt zs|<_nD>ddwE^lK>j@wh5me5DZu+(=lY_ny{NOrS*F15LTsAwOx=7kKKWh28z^pBKb zLl*1iE_2Rg^S|Q-$A}?HP5nCFVPP2TSli7u#Wk-1u27nwEVfdF?aH`PFu92uO>OEe=tS$4lWx--HLH6bAMrJ;J1}O{4Ce9 zOBG&ZH{e4e=fPU(r4`e1#ySTQbDdVN=XzuYbB7!>^xA_hnPm)LelkojU)U(>&3DPz z$2{b>iOZ~U@3l3_xv+-)z_PMl^77QO=JEq4Du&KTHFMQHF$?q8`hVu0S>d?aN^sD8 zY1&m$78P`jrLaVESz#?vK^B!VEQlV@N@gA`nfgI8fNNq`6lvRs$OAU>k8<*Hq zGM|}9m#P2iTxujSp%Ap#a(8Je^?Tk{ID_yM1N_S){8Mik=3ZUJ5{;rt{$!?c#7FRN zfBl6v@xgvF?9Fx=_Sl@@aTi?IDX(CDDS9 zx+cS33H{(qd+bV_-i~E@-atz5a&t#;bPS8*SXXX{VDm$H6r;ZPb_|`oB0qP z0zLB?X1s(R#pKVRG~Z=5b(Ud8Etn+wDmw}y2rHR_?v?2(zt(;k_o4;@GCofA0{Q>I#=Vb0wojRGMU81vDH;kus`%XWY}-iTV)E$@H$hIWjDclFSxur5&&vF7iMzv1_F; zuK+K171H8-?G}E3NE%ggKDJ_qICVg5_Bea$xlD^jN5X7_40DQ=VPj|nkCpipv&d!W z5Fc>ZOPkp|w#a!9ywl%3#cnUfc#F>6yo;ZkFFCh}m^-Hx*wtDkTqxa;8C}K7-yno7 zb_AZm??r@#yJ!ZxtiRYu@|;mDWIpu2Mx1{U>s}yjNO&|EBagp_bz}~)6_*?*Bge(o zaBm6WJ~|iQMimV+msru0nZ5nscsLB3N{=G6gu7V@_tjM3S2v|<1oflzwvhX#xM|T| zZt!P6t@d6+5-z$Sn4c!Y`ulm`7G4vNyhV+^M}Cw_c+L?F@4r*45D;*b}W4A|}kmW&)@f_}C) ze9F+Mm%C5dRxGkQ`Lat=fb@wBTOTU=txohSUj7K4O$O169*@5^uc2-hQwv~wCMGob zUn?50zcMs@3py$GQPtKAoIO*xWHtmRL%0;);VCzM7q%0%teMjYTeH3zw&y5J6b-3_ zOIheaPF!e+-+k$gt-Q(qlrF=&UrXCdvDv z*Jv456`%CUH-RC@{v^17896}~hu*_ZT=5>@RBU|@E3Z!B48lgJ9;=a@*c4@;T>lQa{m{(uGzjNrvyK_Y-YyHnTTA5YLQl zW^80xNa|xJXT7XEW1Fpu=~pnOFtV5i)UC7{nrH=K3YQcr?wcdlZSl`#g;n}8+*<-2 z_xjx;gdXw18!Id5g`tE#>}Z2L^nSWi%baoYzUiW~@C0TE;pxT&;f?f7{*N2>O1T%G zaBT-cOfkxaEpINvBH;M(fE1k0uMUO$J1~N9Zln=&uMq=1Q2q@;Ad`US`)XnZ{XwO~ z#GUBx$c;zO_jwFl8SH%?U0?1k#h1wOUVk?0dD84a-nJ!@%%(CB+N7rJ@2n0}tPI<%NM ziu&xr4zB5B$#PaMO)17hanfFphsT6#WI!Tq6hMua*z2@@wUZG!=Q=@Aq!N1VDFG9u z%be?3^xRpHzO)m6>+k-!j2cC5HL(yv5Gp^k;9J$xC^|b=q^-E_wr?O8jZ*3XyCIXU zbgzu4#abw!RfobuMCkATY_-Q z*MY-mmxgbgJhulw!!+e{Px>~vhmq86)FYHwx?~Imc7-DDh%IXzFy8X>lrDb~pi zEo9_)nS9_RJq;S+xP4MRJDWRo*Mu}}zAidyQ6wUdT8Koaw#f4&p`ScYI#*lW)DmC^ zuUEAk-4C0P6STIU=soQI^tFZ`AYJmax$Vo|_)I+Qp6{jAtlpASQIAv8x#6bi4xRfG z>!KmSQO&k48$pJA@Gg>CcJ z82iIw-3D#J{4#ekq&f24gCFL!)}Tr6P)O@Fk2!4tM6{pELaYwG{5dHXE^#U218zXw z%oeSp^6yLF;i5D{NYAIf9UGHaXU>DO>kr5d@(H0;LU1WJlTD^%O6NlJ@&GEG?M^Ss zE2X6~3#Mk+8S>pj@JD`gZ~GsuIYTe#%lEysMw-ZK$m(?Y`RvLwrs=vv^(6;T3QJ~y zB5gPf#bfFT*b|hj#|G$;B}!gZ|snge=A2Y*NeF>ti7QC_O@L zaG}=f$*}nE?NkO6Q8lGm>0r2%@se3k(#XFM*rRf;(VY-Vq*0#bk(CfS&;XHwHGLBy zQfN_cgIeY)#2$o3{B2HoOB>!Ss?Ri%d6o13vQhh0oW}Q&@C`}^twnW?f{5TaDv#M~ z+gj(sI!FcwxkmJw$s9WG*0__G%Vv9oy&4`soyS&sVvQQ56m0&#D`Eqvm|M#&R>fMV z`>+wLmqtZ-knr*NF2ehQoI`#C5O`$xjlEtxEJ8Tzci6e>-67*h&fZ-=xgx~sKys&~ z0L9jopDwL?mpon2-W`Ju7jo~0{m45$Yv*dzQ&3Jpg{e|^v9T@MaBb?fC>xK~*hheu zsXT_3+w^c;Igu1Q!fo9s#}9HIc!&6HDlMjo2c(@t1=@b0PbB2AVjt$`@Ri+{I+)!~ z-#r<7tNYd%g)%@4RZ9psr>UALNlP@*b%P9>7)z8g#rO|QLb+~L*);NeMQztW`i;vE zHKjUJqIKB_ifhe}{6|@kMh=6rfX?G(*f}T?cG0A47Cg7Vt86u%G%MUq4urIU1J9tq zp9`nhICN}6WP6^WI5!LMb#QKykZD{qyJ{Gmo7cnz=_TXCP8bD@VCB`pV$(-JrErlT z2QaZxcN8ViVyhEAlfYub-66hZw^csu!T*yL@x~PayXAOI-Vb0m1mEFH;<|B98xol$ zuG^cJao)T*-P`km!9Ef(R(by&*zO3)0UUaf43h)V5eSIZDCYt8p{liE4`3dY}#tE*J9q~An@^fOR04e}CR|E80zv@|q zp`~<*btI~EZ3&3;DBrGO3#`QEg^A4JRU>6st~c!pIkvDDU9QidO{sr-(WJFi)Wl}GTZ z`R1kZA&t~7qO1%mDi8cvaRvBBI)L^khu&^@q5V_loTEov4EE6P5jE5XJ+Vad! z$JX3Sk=iJo0zc3ugs`dRh?uW450H$`*8wIuE#j5C{{bcRL%g-jrj`v7JBNKPe4*RI zi5p(i?2_~~y~Z<`=_EZul|u*%**_qJdooOcArw-TAOu|GBAA~_>F=j^(Q}J;=&S|+r=RaWA>Pjz4nKJBvo%>m#3=>XjroTw2pq4#>WUzuF2%++Shx7C) zthM*VW;)NEN4qZ#l~Rf6iGNIkr?|0RhJ74cEPhh-uo*B^cK}M(i|3Km^NINo`ODI$Gg#Nhrh1 zSRKm1(8EzFlN>Z^1QMuYcuSX1CGf_@L7^hppuP^0joy;sP_(hjtYz3)H?g_cu?B}^ z!yYbbu1*4()TiP(~ z@g?WxiUW(+5An=0;A)@J!Tg+|o`<&`1qtDqNEx;lu3qK$c~v)zwVMCy-zsDpL+O2h zsz7DdPA1X?!%-7FOZ+pTfMrIQc3(Pk+W7QczMY*dIvlk%PM7vu!>tJr`D=TP8|VoW zPSq{9E*q&=7SQE_R2%k?YI~~L8JdmlHdy?k>1+;veke5kj?kMRlDxlB485rTnqtEx z{C(F93h1%kl6O?`aT&%t{G*N6ThYct4a8+66r*lyRa=Vfy;^@KvUmK?5Qt2_rKoXo+^#bs9gMLOyQc1o2#2iT;e1F)GJ$f? z+ah?{7N7MO*74`W#B{XFVXqsG!%+|97A#BsN6_)U*$RrYfMM`?;;8V*o(xn$F8Oq)% z!~Ue@Gu{aCSlCj&-nC+fN0XXzFtf-S6Mj4-q7VkOeMsmRWuozL$nbihFwnP`%2Q?K z$uK}M5o1V>V#4`0G_Qh=nnAQ!MKdVR8vsi!z>756VWmcep^#WG#eMzu3T#qXht`sb zMOpljq%3JTBo-f0Vi6Zq<<40%Yjn<2w?KL&Bo_S^V~upx_t4QeK6ns1L#|725$42S zOq9*HKK&Z@J^b>j!e%?~LJ6m_CnWSsq%~akey?)aL<#UqXg`W+GMNio2mwveMkI^r zXeep8;m{---2b0fG+fj-J%IE7Vs`Rd$mfdPiJmz;)&m)qmQ%yrR_}NI7Ccy|XEv^x zMNLO+TraE(U|47rx^TEcH^U=Tx;#e`M-@K`Rr+^19HL4bfFdXwC;KG@EW*TFVi5}c zlF8CIXw5MOB%IFV#t6gr8P5L{bn+pnG_f^OViwq!%7n`>=$=t^#e61VCk7Ibk44gq!xUTPCwAw490R^ra* zHN&pSw?KdxqzRBClWZy;)3A!ZQ@jQNVvr}S_fRdx+4O2iXZmd*2C`(B=|D1v8V#AH z4EwT*ctvE1U5lu1socsOW>$faZ6$TtSrC7`=zvC3jU=@xb&yC>n+lka3y_j+1`+K_ ztYS)w9F+duUKb^B14cmlQAG5gN{qFX{DiM3Qea=v^N9js=tCKn5qk2AyLcG8J`NH`;jvKT+79$Kl4JdU{hxGb)H+D6hcFOImzGS&Hf(i?U{|4{mXT|5D z6@#GrZ89v7DH%%L)R0(x+Z9%+(77no#=T^6(Iu+`Rcw_;@sGY%8)pbOG!Q{kJt3{m zq;k#hGCkl7c)>f3l)PpaatfIn#FQ3XvfsCd?|I@?g$%p1CZWkQek-*so!aO^t%rt9 zZ6P$VH9$;+nk!eI(de+@Mgm@w;SS6MBCRcl6w?u2^AT7HHE`O-`@cJ$UuilRk&{MJ zH{~3mJdBjLwM^k#098P3t#3`s;eaOFTDKGImf`zti_#}}wm3p=NaAgf8~%XXl7MG` z$JHTX;3c4-U4%-&BlCjmkQ0U|HB`Y0;e_8t4Plbcc%gtI?o9>+bPmMwaP)+4U6x@k z_uHrw*uJ~?sKypJic~trOVm=#x4IQ;_OUt$&dEW8&Fqj81uI3t;{aC5gXR*iJC2Bz zBKG*g6K72IOy4!#S-x1|ER`?T9(AJGN#tUU_mAXJ&{`F2FB0N@wY(7f$uvY6%y3sk zZ$ueHY&|(;aJ?da8|!n8`VZ>o+HYDfi)3}iiz?)F*(i5jJ0vcPrj~Y)U95RI*;Ik^ zP^m#!u?j$D)Y>yIkQ1rg5>QhCkS_IRd!v)FR;J$=lD1LO837^+*2a0^e8@ha`8^Nd z5DO(PQV^uP9X)Ktgl48Ja7GXyH$I&SWMC2kB-@oF1bFYz>-q`=0qOwB(a69mc#xh+ zDKw}5ZJXK}T0Laz8#1~yLY~nritvKotrNSzvmED*1PCXAFv&QAmjPXb_y9B`|GPkf z@DIxb`tGPU`fdwLogLz|SogDTjR~AWd!cS#AsUdrb^_ltg+{R=M25{oRc3C4JrbGk!ALW?k65^OrBa-a@pvM;Q#==?kg#ZhO{CO8cE` zNcq%}h-ePcM*?JaSif5@P2lg0l*X1loByWuYAaBW$SHl0joGd23ATgt8g$wz|NNck zR7k}-0xh(33Ic#v173gbq7Q;HdaIRtQIdz$SGe3iu^Az5x6IOHl#AuFP$_ zFblzZ;c7@+CjxIBC)+VYm_=*xeCm7NPH3fPHQ*oS!O@t}bpPTfS6!essMWA!#g`3T zB6HyPu%-7d8wCN1gop^Oy8!|lA|e5d1bnwMt-B#yxvQ)J??XS2#Q**a42D|e7_{Qa zo)S%Z4$2BRoLldLE^jK4{Ti~V1k9uj;{5Ddt3#nUa4`Fbm|R-TMJ}_8$YudiAQ*l_ zNO`c*|LO>!%o8O5er)>X_v1OEt%AGHG5N03E}ucV3|@tbECAjq@MZTyE#5I0>N1t& z{G=K|3foHgK`AUr`H4muETa6N43_#VxRz^cD}t3k1lje01=NWF9pD9g_d|Z3Qk4C` zDuj$v0vTs-IKQUdQ(EDdQL9NcGyqiw(5&NFywgi zqPRu_LYBFjn)Pr?9&y>ose>GaIO;l9z6Ccv#l0{CrEd`^lT@sgLSb*nBo9OWHfw4d zu3QE2r>*S|34^^3T7v2-k(Q`dlCkRR@q@ji;{w%EKL%7w(QM#R$ZJ&|?Z=r|FpbqH zfO2W6|0k}y?aQ2GIe#2romM6}`Kw8T{3XKyqr*lA3<)+Of)NQe3TPG05kdquJ}|o; zGTAZs^KR(RULJw+I}M6uT_a|KfHNJXodmrn3EbJiO8iZ4XgY9+{KdME>8m0m2v&sN zvu2el$4}wUm8gs)j*L_-B)JH(o>OlBzHr5)x7&ft&=srE8$g->^#XY!ArcnAedt7z z!+qq51T3r(Ch3E$r;W=V=QDJ%Zh)M04!9JFTagToHaeIy;4eySFtKBK1eE&bTpD!e zNDfPp-Ovw-thf>x=Ei|a-(x5&peX>(9y)BAP8)ho#1dA@u;XrUPQYU|;L5N*sCyHS zyW_7Nd61=33)oz7PQB1ShAUlLTtsZ=t^N;mI)8xP#{v}ze$Ea`NQtRvq96i#Sbs_; z2`mfQaeHX@R^>}0YlIJqXNap;Ts=GK4K%4ktm1V?&7tBX{H}z_AmImFCvg}of`QUI z)4a-`^-bMpo<@7*HswXl6tCm=-&y}DDX2KP;keDj$4*(}On<|2hU!aW_-TcCsX0e4 zQQjvyvuIG?`*t&de$Pq61o~>`x!KEttml+3okJP>>!{kXm7}$a^A0eIA>Bz_D2Nw2 z3Nj8RQr10X{?SOiCq0u}8G;dO8iDonGV>ZVz-YD^j%&lZv&wysO$*yP=}O4*o^GRG z=w87mdQ<$beWOib{&7^o-inJX!=PNw-F<(x#13BnGU6}J_n%k6MioNR9Ewu-+tM5C zuQTHyV)Ks5utBU(J#UNtfNq-bj4fm$eoP4?OzfDp8WQVoO!-fHm-+mwg!<2ZT2ar9pZg|(SQ|Z4NaV!6dx8JvuK_{M5_)y-5 zP&>1?hHoSw2LDSr*cXZl7fsx3RZRXxhGw zH{Jdi?d3MU>FR^P(u)qeuUyQ{5l^Py9lN~m)NP|;n73W3KK3XnoHzW@S??ji!J&Y9 z?-^X*b#UJALge)9^>h72qLKlkI@$4@n zg*o@7%o>hBTG_ncF3*Et?c~X@Nqb{3kP5XIabQ!nM`Kfxgtl|Iw?rj%FmE!}l2jw@ z5_KwZi%G1(r7!5C$b(P0?-;6)bidW*(Q2Lu-b1Ghk?dhZL2e4!5RmN8__I)#%xg5I zdan{kVU$AWu!GX7bC^o>WFegckF!BtoWY+@Ct34nj2hY7AUE~)e}C~YcQOUf8!4ENN}+B(z2&?< zM63f6yJ0ug=X^O8AwLe(M@d;S ztXGc6Au0EB^Q8N~5b?aZ33kito{mp2oRJ0V-3TwR40WX8-^vP4~Af6$^v=Ui>gYjw` z@T2Z;1kTSnC?Qppb%1QPvZ#qX5|)6v?u=KP_Oiu^MBgLq%!xO+9DR2sA#a4fM-~?Q zme3IT-V5X)_oX$i-li$ZTH>(?k1IPzO!aYUcridT-&yz6 zGv3T~NL>pg*E!6B^Mu|+-5P-)=Q*cG3}cu>`K|8`mzLa^UzW0Sn0NfCq3+{~=RUkP z=YjUa`a}Jy?B3;;H~4v5hrIZ`|Ms7UP4AKTpl--p<`^sDYs`zBx(yOMeD}UTbAs#G zH5AB)u8kda?a72eT@1(0`f2j@yMe!mxZmj3jJH5*H4$FC^{R}m|H*pkWvTQ{?BNrQ zopm#6^Q2eK*m4Kb9{8;jJe5#I`0c7F?cyChiX0LKRCvZYRG$;MEK683={e9~J`}Ka zQkp{g*3pLaFwy&@Vx8Aa-}h&e-fg$|UbHB;h&(^ZyH8cS zi#Oz3U%#gt^Df=(CjVx1l+SR%*gZRDS1H)tJh>UGYMzWnu~4J{r8}3#+rVX- z`q(&*v*rlgw`ZKJ=KMR=R+;G_^bIASDuX@IJ%{^pOW>?Ik*Ra&%O zp!YmM={TnyFlD-OghH9cz#r$P&>W>ibSRkuT?>iUQ}Z{#1QTk5cV^hWs*eH{EC64S zuJ|+M>B`N7r}eA@_=8GF9ts_y3DycN)&s$c=>`O=5?AO)Edv1$E#bnBN>kI8XyIz| z_Fr%YCRzz1xR!uLb{Fj^1AfZ3YTP|8No-R%W>ehJrb!R7tZL6ehT4O>cad=~jJnMY znn=nN#&*b6fKrBo+N%?s%IApABGei?c(dqcsZuY0IPJn zD8sC21Q!>tr*HFH3NN&?q>rE^vXo=$1FBUZMWZIQoVqp?s6m&Nv^3{90Itft5ftER zAo~Zns)MMEa&T428zI0i8mPhDAU9Wlv|bTUNHv2Jdf6facAz?NfP4T(T<)au1sn*HHGT`2 zJW3)GH70lXp2fOASyI&!AbGNmv9pG%5NHRg@THt7ttJ_7EB&1-)*ul!D6J6Mp(zm; zX+tUE0K##lLOJsxh^&x&8^l&i*ezdW80E8D*oSn#tx&kG6Vs)p9&0wpu$WK*G5g8% z1>E!qj?f75%XbkbOFl*1A6)&*Wb1^jU0-bfrNNnZ@~*Xq$a$XO@4nX)c1;am9NqHF zkL_`HmV3G3xP@-RueiIOFW7N)&!Qe4r)n-^bB2a-p6zGzuEe;kuAZ`NvE^BOBMS?i zJeJa#xqU-R-fPA{evl}h{_K6?42$9v&wjnSX1_T8z;s;Xkp;IFdA>3oIMQjJb*+C= zCrZt$F*$xBFTttQj8Kp6dqg)6Jo@vFSI<2Tv1PN3%H49*-x8u z@?%n|^t>e;XkqhLpBj8>%MpL=Q`BEA%Bg*4?z!zX-+b`+CqoAD9zq5_QOK-G?RuHI3Qs7lxQTOHI|y z0@zMyIRn6ltO8WU>?5us6@hA*g`mK$uqs;h3D7%)JW|Cn0nqTiQ;981X*$qQpOfAO zD*dOg+7+viN(%b6a_xrFo@ri=Tm!V5o#pNU>i#owbqNU~0l~J7+pS$61KA~nxTSE4 zq5NB}{IY@q2x-&k09V)ERj5alRjZKDDs@+>72TMPE7)b5@J~kKsbWAEI{9&|AUmzu zQO`c+ovD?}C}H1o2Kx_%^n;^~Bl{0TjF9&|N$tm(y}(S*80B#x#C~G4HXgJ zs3-#!A7yLs=UZi%m8CFV7@1FG5KI6(N@fn)3`MdpUfp-6^b@a8ncYA#8h|bc-ZF?T z2;MRaQSuIj2H;A+)uE>4M7|r~N=N-B@j*=g;342j9RKsYKKIuw6j$H6akgophlQlp zBSBjqLu?TB2CBh$__Gl*EI5R0@|)yIhBF(+;2WVu>mOQ!f29Z-DpR1rZ+@1zySJ_= zyEqMlUOjgc1z5MK4)}`~@NJeg)O>0~H!-Ei?wyb+64aB#TSz)qChW|K7;t_%&@Q0| zrblu|D?xvVdSD7DRJ<0k`q=T{LgNY%!fbb>>C4aBbbCfBnu7?#;(?o%q*~jkZiMrMnvy}kQ`rU zurtRt1tZ+R&H-_plu0DY6elF0R9DA=(&#kvL1n5@tLoMOAdyzttvI?>c8j96LUs$} zosRN#s80X*wv%;pK-tDZ=w@j=>Qg`#g(P|kZ8C2^Nf}3Jlu{?2VUKvT>On}WWrG-F z6tm5eY#6zWp8TW7{oJK-Wkva{OXWG@;pv87rp~=i!my^x_zsJFSRZyc5)Sk0#JX_q ze9a0%xZXi0{FT6`H}QBIz2%qbjM6G*{S-Q$Z8s1zdUNdg za?z$S1ElO7&&Tdw{B!si-YK8P&P^uu5y5q}*5c-&u}{x^oW0_h^_Y+uXBLE%j&nS8 zDV!r&AUau24P0xPko4$XWWopip(j>NJ=|?)LARegP9#6Lbiwd(j`p;T?5>ifg7_9fqw&mWcUi)T5y0PzOpza$jHNKVJYu}|heonbpe43G- zec0+r2ktC7-Zi|_iEOIS}Jl)jvzXJsFN%#=a^M7ep>P2n2)R7%%0kp zW{%n$F86isDep%7LF@g?9*ovzy9R1VnGfIVl>zaDLxV0it_^^>`1iQ81U>mSY&(Z5o(BM$c^QQwtGCo{Q7qbvEjmo<|Qfeo_h%~BRGvc^{Id~wnnrW;WB zC)HWHXC>@Psa;BSBKNM+eq}q2G>`;HpW&#Hpu z-7u)M9CB#|*VYWPkoz2|h%#YkwE6Zp)QlG!;Hg_+pYM=iCh>qD@{ju;CDv1SifM&* z@8Gt|3*u{t=Heo=fPJi*CnU%5Ap;8c(!6v!u*^2_i_+NJh}?H}DQ+8t%-F&#FQdA< zzXfe9nh5PN+w4zfw+Bm6+Q86PAYqObkvJujxIpJ zaF%bsh9pm0>O_y({KL|tVqIcAzCr9-X!jmspq;~6cFh?XR&BGRpq}gLij+8Huq^>4 z&ZbgDtVJ$YQWj~o1--M;HAhDd30cOSY!={Ft}Ob83>MzS9;g-EkjDNG8J5o^z1O4> z#4x6t*z#iKnpwIn#i2hil)>7$Thv&ukQ!m|%>D(RMegSTXa7IJ8&4QPAX0DnK~kj- zz&6ssVndD=D(p*spoMf%D_V#gOjOW94DrpC8w7;M&-Gk#KG3;(StAh7o2u3PGLphOF_ z{zyWnW>S?4wkdM+0n#>QIGk)C;6S!1I=RT0NYi>iHNS?9uT)ms4@7y>K8c>_J!oz! z!L zmqN);8u%cnd8{Qh?v88z#)s#uN$LJs2duF&NIE;0`gsqe(D(dVMDXL7`*3PIh`TW8m9CH~@#4(3?(Ss%h+XR~LpxAE3IWFI;6=<6=a#u0a2-fImFYfiV|9({L( zx!}pYi!)EW9=L)sgDGHn>UN94t+uV&wwKbyZ_WugeD8i7^XJgbZk%o$N_lf0Z(Jw5 zbO>ct%b719$NZY@=KK7W!Mi^{|7ttRftHzMByx+Md)-Gj{gJ2H8j-upUyYlV6rB2O z@p5|5gU~Cir#oWaW-m_i-gDR5b4#}KQg_cv-hgr^){)2re#w2qs*)`Y6R(F2HK;0^ zo1cm`uBM1gR^&2<=Zj9KT=}}WT0D>V#a-`Q_MF_gX0d%1cg6Z7*W7$!RJe0wVqd|C zD&Yye2C7sVR45~kFjo;{QT;O zR}%$C)A7KjM}2x%JE!mxmz2Mpx%^{@$lrXfdC!^C2jPj8;>3Nul-{Hj|!}~|3PpMvh&im5V&Hai!`!t+5%iUblWB0Wi{zV&b_uB<7#f6z+ zMYR4|#`iBo-^Pv%O6%be$5<}{je=K9;3hMJu;xX>e6MY9eh_kJPIY#bR9n<-S01(N zv~hD^&fm~WhCL0*yvE1$%hHwz_eShK=jQlMVoD@H%5V>`(r?x#%@txVMlnU37!9qk zX?&Z*({@Os%g+lR%-?TRbN;LMpDbQQSkozca58vOam3@mDSOu2938VaEI9Lj!M~r= zCw}u{H5G)R#1EO>G`I-CA0K*9079HC)b@_Rs@jxUR9+XMUx@ZW&w%f zBc21$U(D@2=HofW(9375W*BJ6;dy<`}*yMw9FqQUO+Gof-DMQAF7o*1O5aB z9c3Fnh^ML!iO_yl=>N8A}OSR~LeJg!4vdVl0NSQvV@uJ3sq8vsc@+tyjS*XPtrX_TP3??xA2Mfx%x?N&K z72thqC|t?$-TNXXA#HeVvxRsdo-!wCf=`z4f@pu44Ev*GYM3+s(*0WlC+xR<=+ZAZ zj5grKcAHt9rX1MCKQ*JfR`%>)q#Ff4`P-zQSiQhL_QRN7A8l%4XV=^P<-c+xuyQ=2 zu01}>#p5cNOnywEN7kxsXBwnh&NS%-=U2B zp-zgJ`1aT5CErX9mWB`Xh|}3W=!w|DJaxzLblQM#^@a1F+78a$6E1cNx$@JF)U;6c+S?3*^5ob)Q+2LR(4>u)asDPIh)=8`ja&SXmKkmU51}6zB1M? z&i}ElyM6KOS!?Dz^}gUWV0nVwTalEzuv>X(z_h957j>5$S{xTLd(ieV7lY>=yffu? zYS^KWaF;S_=tK75bEbKTGw|+bjc?^Goj<<$?C10@@3JPnxrTX}G#8gi-Cf@Ayu_Ki zKYZnfYehptA1&(s{$bBkQ$qGT`YkMf=AG`$n-uM9ouyS^)@AvoKfDY3U2u)TpFgpC z@WE@=sQaJe;;8c)>q5Y?k8{~&QgB30>9bLDy(m5APV4g1lw`-3@w&El+i!HYI%QmG z(qNvp{~0YE?^*WvS=!U^0`W}9;X2JpJ8VC-vpuyg=!2+tSprM6e9HVaYm2isZ}jDT z{}`SaP?2qRbC)mp7#{fR(L=*2)1AwQNBd!?2I$P+<5?V#MY+8_G`S^jf$;@L{{`Ic zcWV45U!Qe(i%_`h^L4jVX`jP#L%H<5jGQIbxK85m6`!-VR^ptUZ=`Nz&sJ6Jk&deM z{LA)2a7!t-k;S$%-pc(|w4|)t+&^d!zJ3fGGCn!;`4jWTJA$Vpt}J`Ef;X+Na@cR6 zON>*A1(xM>X)5*mx2TSt#kJ;j^@H1uf!RQwkBA9Y;r^%a0` z`t3B7u0ka8*9N6+B*aQF0zHvQo0hL_FR$8OCKJkgY2j<02C)H4Z4k83hkQ zMGM(w*VyId5E=RkDK(Vl!_rV5bBmZJ@o6S8X%|o;700_~9e=}&12!?@Bq_{A{S3*h z1995r9y_GFXsjxo^(v@R%AmER1hUxcBK)xpWkFh6C3EBkXTIj_POTF&N0BHSC`URnYo+)dmuVEP2EywKm|#AYEG%t`ZCy<#sU{$UG5UcTPt z?lEjgBb1v9&<#|xlFw~)lKbQUZ!0wp$bbq_7~o%B>7*#E0N*ZlB}HMq5pAmiUd4_g z8;^u7&<^g07)@mg4U##)IC2frc=sO~q@JxBB*rK}#z@jXE;l_;u>MiA9aymWAax*Z z4@OtDv9+TS#J>vq7IKXLDb%s=NsQlE?3z!I7=MH08Pg3x{PBB%>^Rj0Lqf78YRw># zHvNCs?hskfDx>yo|IT*$HwNwXy246@^iZ!&dRW{hJ#tkE?NYLh9teK z%mLsjbFSby0A!=?ZR89xf4(mA_tAkZ4X_ya`y8bpghuXwy8u?xPY4%V^@7^MdeG)i zi%cI+L&};v5K06$2>dxb3eQN=r9KfTOz{bCh=FVyNQYE4@1)Pb_%?Gep#CFOG|MCYhqNAKb7FudSwVHG>4dWWsS4BcIK zzV@GFI5^NQdHv(x*cf1;&Y*rT0CV=o{XDIqzuw~`0Ugds8*z2Cp-rRg!!WQt>dccb0h$rCh=7LY|ph&O>%N4Puakpexjx(867&epm$nJWKf*eNL{ar z+$(!BnpW;O9n>5xw2{Un=}M+b&r5zPs~aHVJ5cLa2#D?2Zx9=k6uK~e-t64J#40K&MBJ41lO4uyDz*1yX`wy zSLTc@f6}y&RWq#gn4#TN3wjUHrNWXCl3O1~-tF>2TbuH=I4blAFvpn;ZGK$vnU9x- z2{s-Z%CdSj{l)pI31+8g1&0~CCAxs}j$vf7O}4rNSb7rcw?NM2?f7lsylxfo z4W*|_3{rQDK9%(C%VEYc+v2bQJ<3`<)th+D{Js>gKMJ5%{X{oj74&Jl#TGVF^5>_l zx)J;+%qQZ-uOn@D#1?j%y${cb+ZU2RIh54*>$q)<@q&eq&R#A2vvfYYCnsg=w!80) zPYwBXk*%&>t|$!P{X%IJbjs`@(SLrFMk4%`6Uk2k;q5ZZ0pIFwh_2Dv6)_SpRWjW` z1XJK1w@zXaK11mSTDy*I(e6%~k4{G{d9HOpKkEx#6aNF*-&bf|Y|4=vwP&~+NZ|Nu zHEJ(pw;8p6T%l^z?k~eS2$S1~p#E>D>iRCTmmIF)pd#a@*5YlE42%dl> z^Bdxf5+1MV;^$UDFJh8D{^$vqBOibA33!8#Kluch#QCh&C!ndm`xo-Am%7Qu$O;Kr z5G!PKdvaQxkmUYOq>AoNEB^S3G%{6#@sPe4x-dV!G%aL77zlw@J%2h45+}KIle7&2 z6Er2{08&Z>nEcH+49&Ek4XDSv|2Pq&{w`c6>1@-cgWdy|fZTQ9XQ|Kze)xOC*dIW( z)Y)Z__$bM+0OLTmlI^Pt(VwafDhUUDCqOZOYyFF!q0&dd(@>=_fh$vgWZk5pt*BTd z@9AiI|B`9TW0EEZEohjpGO-wZklMF2M)ndQUHD(HF9tmuasyJcFm7BLgveH^_XKz- z*>t+5>XM9@>`|n5usUg_VwGl-3J6%ruF8xkXCS!#7s?jc3xw|j*4AaCinT%Hub@*@ z6~%y?u6nCb?z^kZ;%Z+u0_#W3_!%6oy8!!4P1J)Nc84WX+i;?8P zjWo(cp#`vCu%GGlG4EAFQr;I0J(B9mQUv9H?Z5(V@#^joRK%)SpSGs zjmTB`3CIj_f}}ry+&^)8`H^xh0J)u!TTDu!lss1)<8 zLT1POYJrBR+*&ZnNm%RxdolC;xXh8fdf6#;1{f%uI>l6K%}Ecp2c zX{ViTkaNRIpqb05BcN9Nqm4Qe-AWxMft0fIKt60lp^b-Dr^ik3zE(+N_`1 z6WJ;>A601-1_4%mhc`Nrd7QG)3KaL06O?!s_#2Ru14#P>xVFMDPq9tAH^sgjY zD0-_uFoeMTtI!4A+BT7-gJfKWWZE@i!c@eka;Edo;jiPQVc=&*y$^b6JR^sx3uLNG zt}SszZ1<_&J-}WIH0`HJGh#!)ar*Wpda&Wi0pRl5vIbB_heDj2@e(YFPjRH1p%!1g zb9)O5E)`j^A4>vOE!Q%Ih1!P$1?FU!{YY@UP?%ja%gv^C=Z!Ociw)CmH(W`t2_DxO zm~bR-t}pp{K${y*<)4xw+T41vO#$%^Xmb^u1m2+hBF;35TEkI}h{9h{?vO%ep|F`8 zOhrREb12?nklJz|<})H#T+kKBhzpLMiefB3AUxkhrI^^Yr6 zebz~jfVL9qp%(b6D?!%nl@tGYoT0paM~Nta zK`?=lGc~n+veMUfkuxT-7Y;)+{9S5n6Y8wc8F_7z^a~JBJaH%Eo{6OjqBxyn-r|up40DtI|t55V%)4fTF z%SPeqGGa29BGqt34oE>L&;Eu}4HM@hvA#P`_y|!P?vUBvikk~#T{4+d40tayPtcOd z_oLRHClz}d_jMWuA&@p)a^j=Hg$s?@Bduge;^U$=-8B0uV&SSL$*81kmpf!n!9j!Y z?`|QAfKJ?9T+xXu26+hChzs(Mt-YebFOXzYcfPHY_;?TdLfc!ARkn@*)r+rtt{L-VNJ@|H0V5lN|COx;1{j1!R9g% zOhT6rd_itQqz`s-8%_cO}o|LXZt@;yXf(XnqM)VFti>iUHI=N5L@xC$c(S2D##D+#!rM(s;c>@Zgoty#)U?6b2N7FOe@^ zjR5(tH9-*Q6=g7Vi!FGu*Uyb%aU)^2VyaN#x>3d?#}WXr4_f+SWLHPJZiKhFZrD>z z)P=WjkRlpC0|!>G=gvqq;OZWn#emd3RmI7gbWPG=Adt2ds>KFsD=Xar zO!};f?6~u-W{G5t&{G7es$LiJG*LB^ZjWV_>m<7SZx#(e3R_pX z?>1@c3jQ7nTi4xMRb(GmY3?+*_q4b_@YC(RPnlN|xzc{btNoL!q=O1`9UDMqN^1*0O!7Bwx`Znlrfprp! zR;?3D$f*tF#1e991DaSeO4%4p`TOMpZ4G~c*G6c{B7_<4Gpn{J14C9pT;T}f3K~qI zQZmkNA-!1^smyg!W1h78xc=j03}i9fRuaU)R2ejU6W~>)KUE#Z^#AazCGwy?fRmH~ zxmoxRe65Cw2m>o3ky$vH1Q#`h?5zwg#pijoBXs|-142(r{=cC1_&RrN4zQ6E3s~K- zr$~Yof@=~zf6%(Yt`YjKXx8Q831-*-@PWbK^<85$M}NQ${{LeF5*S$l1_owE@(IAm zowoar3BWM<7^)$10`OB{$0$z#rXX@Vn{zLt6{LhIF|8mar4^)fXagyCYZ8oLBn@ObcQ}bkeu%PaYRSA!w?=eq^r>u0`oaRDJ_NPL0VXpGBwMxIH}<&xIbu;c%D_ zNUf;@Y+It&;Fkk!<@@oO#25}_rm9kO(!eZ}`oq8M7$U-v{$+iI)JOiLf7xf`jt>51 zdyAQqY{sIKO}*1fI@V+>O_g@8xaTUsC6iwFA0f zfXx2ep>`wSC90U@LPp}-Y5~&N?i?dXby10{ z^7rjgE~>=vZP&{N0O~i}!N=B|;2>27>_eqA6y}G6x)iKx{_{n`>>kxFHc1{q>P#i) zd$Nl?0wWvJYHpXPZ;|;7MgWn$coU6#R=I_*U^o)=9~$xcU@ZXvBmy&3jEK<$J0&bb z1Ikb_garIj&3Hu>UgCwtb648Anqgi55HoD@j zuqSBezW_J?c#^YVXS2N>v~N`fa_WszbY zlN;{-6dCx9y&8AE$!2L*x7s8Br@ONYX(|rGxFsYQVj+dnCwO6K8Wtp>c)ZAxZW8-g z5hP?`A+!RUH~XlBy2wZi6SA1tg%@d~bPXvwR_bh`=Ean0B56p8x;A4j-B!>0KRfg6 zOvsfGbmyIVKHmR%-{*OsA4sns##IO6S7$x(<+tJizvbJg@V22n;G1F7PU@LORAA?5 zirb`a@MLhL2B({S>-2s#xZB(0SA)?an@v?r4lbPU80O0|Rh51PMzWJrn2dRs1p5?( zfZDb}or-Y3ObTKpnaPcCKPloNi81jjst3`O-U(P(7HYaqUg!`n-T2a@MUR51l z88S76y}3JP)ZiN!L9l55#7IUD8o)e%5^Nmj&eMvij8j?KH=V?W-UEx1>M!~8_5%iiLBjF#{F z+tgZZpZ(Fuvp)JoVO(DT1mw3!emFo#lomDb(Yn|U<)4!F(AI(zB0vfSOo-qQJOWE8bSjf)95&QMCcm55oPK37KE%ndXP%Wf3@ zZjT=p!QTxI7}y0Hkn)aG@i{6?U`hnO!GrVc1vydT7{>EL8h31%V>d&O(w#giaFg&M zgz^+Uj`%$)aTJ+{#luN*T)q!RC#eN{a+1xF;rpbYFKIDUpO0h;$4bC(*#GfQ=qjMH zq4jbaa<<}FA}AaHhiwl8C>(Q}F(};B_Svk~(-cJqwdS&=Vm$GBjD{VfMie;l9K8b1 zVFNoySztXoM_FJ!&QS`iPgA|1FGgA94m3!Ka>LB%^0c0i!P-@**l2iyh~fqe!d8~A zH5on?oY^ojzvEnK#7d~(6V!W9Q_J2d>3S(nEohqNY_I!EQX9Eene73LHY Mpu>OobL3CkFE>Q Date: Mon, 9 Aug 2021 21:05:19 +0800 Subject: [PATCH 04/29] Update README.md --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e2fb19..ddc6014 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Emm……辛辛苦苦种树……确定不来个Star鼓励一下? >部分内容收集整理于网络,在此也再次感谢所有内容产出者的贡献! >> [**版权声明**](#版权声明) ->>>如果觉得看起来比较麻烦,需要PDF版本,或是需要更多学习资料、面试资料,进阶、架构资料,都可以加上[**QQ群**](#contanct-me)领取。祝愿每一位有追求的Android开发同胞都能进大厂拿高薪! +>>>如果觉得看起来比较麻烦,需要PDF版本,或是需要更多学习资料、面试资料,进阶、架构资料,都可以加上[**VX或QQ群**](#contanct-me)领取。祝愿每一位有追求的Android开发同胞都能进大厂拿高薪! # Android-Notes @@ -636,6 +636,16 @@ Android开发核心知识点笔记-目录: 祝愿每一位有追求的Android开发同胞都能进大厂拿高薪! +## 微信 + +VX号:**mm14525201314**(需要资料备注一下GitHub,资料) + +
+ +
+ +想加入微信群聊的话,备注:GitHub,加群 + ## QQ群 Android开发交流QQ群:**1078864380** (备注一下GitHub,免得被认成打无良广告的) From 1ab8ce108804a57b1e7a7dd7c3dfd2333aae33b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Fri, 29 Oct 2021 21:54:08 +0800 Subject: [PATCH 05/29] Update README.md --- README.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ddc6014..cf42990 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ >要想成为一名优秀的Android开发,一份知识体系是必不可少的~ -感谢大家的关注与支持,最近工作上比较忙,很难抽出时间来更新~ +感谢大家的关注与支持,工作上比较忙,很难抽出时间来更新~ -结构方面还需要再优化一下,然后打算再整理一下面试题相关的内容,后面计划会给大家制作一份知识体系图…… +接下来会有一个不算太大也不算太小的更新,应该是在11月左右,主要是针对Framework部分和音视频部分。(另外,之前做整合的时候记得有标注过原作者,之前有好友反馈说没保存下来,又得一个一个加上去~~~ + +总的结构方面还需要再优化一下,然后N久以前就打算再整理一下面试题相关的内容……emm,一直没有大家别怪我~后面计划会给大家制作一份知识体系图…… + +时间跨度可能不会很短,但会尽自己能力去更新维护。感谢大家的关注与支持,同时也感谢向我提出修改建议的朋友们! Emm……辛辛苦苦种树……确定不来个Star鼓励一下? @@ -15,13 +19,13 @@ Emm……辛辛苦苦种树……确定不来个Star鼓励一下? # Android-Notes Android开发核心知识点笔记-目录: -| :one: | :two: | :three: | :four: | :five: | :six: | :seven: | :eight: | :nine: | :keycap_ten: | -| :--------: | :--------: | :--------: | :--------: | :--------: | :--------: | :--------: | :--------: | :--------: | :--------: | -| **Java** | **Android** | **Android
扩展** | **性能优化** | **开源库
源码分析** | **Kotlin** | **设计模式** | **Gradle** | **计算机
网络基础** | **C++** | -| [:coffee:](#Java-知识点汇总) | [:iphone:](#Android-知识点汇总) | [:vibration_mode:](#Android-扩展知识点汇总) | [:gear:](#性能优化知识点汇总) | [:mag:](#Android-开源库源码分析) | [:lollipop:](#Kotlin知识点汇总) | [:hammer_and_wrench:](#设计模式汇总) | [:notebook_with_decorative_cover:](#Gradle知识点汇总) | [:computer:](#计算机网络基础) | [:copyright:](#C知识点汇总) | -| :a: | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | -| **常见面试
算法题** | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | -| [:memo:](#常见面试算法题汇总) | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | :u7121: | +| **1** | **2** | **3** | **4** | **5** | **6** | **7** | **8** | **9** | **10** | +| :---------------------------: | :-----------------------------: | :-----------------------------------------: | :------------------------------------: | :---------------------------: | :------------------------------: | :---------------------------------------------------: | :----------------------------------: | :-------------------------: | :------------------------: | +| **Java** | **Android** | **Android
扩展** | **Framework** | **性能优化** | **开源库
源码分析** | **Gradle** | **设计模式** | **C++** | **音视频** | +| [:coffee:](#Java-知识点汇总) | [:iphone:](#Android-知识点汇总) | [:vibration_mode:](#Android-扩展知识点汇总) | [:file_folder:](#Framework-知识点汇总) | [:gear:](#性能优化知识点汇总) | [:mag:](#Android-开源库源码分析) | [:notebook_with_decorative_cover:](#Gradle知识点汇总) | [:hammer_and_wrench:](#设计模式汇总) | [:copyright:](#C知识点汇总) | [:vhs:](#音视频知识点汇总) | +| **A** | **B** | **C** | 持续更新中 | | | | | | | +| **计算机
网络基础** | **Kotlin** | **常见面试
算法题** | 持续更新中 | | | | | | | +| [:computer:](#计算机网络基础) | [:lollipop:](#Kotlin知识点汇总) | [:memo:](#常见面试算法题汇总) | 持续更新中 | | | | | | | ## Java 知识点汇总 From 9dd3fcf7bb2a997cc84cb9b545d0aba5aaa72603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Mon, 1 Nov 2021 13:37:13 +0800 Subject: [PATCH 06/29] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf42990..9e3611d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Android开发核心知识点笔记-目录: | **1** | **2** | **3** | **4** | **5** | **6** | **7** | **8** | **9** | **10** | | :---------------------------: | :-----------------------------: | :-----------------------------------------: | :------------------------------------: | :---------------------------: | :------------------------------: | :---------------------------------------------------: | :----------------------------------: | :-------------------------: | :------------------------: | -| **Java** | **Android** | **Android
扩展** | **Framework** | **性能优化** | **开源库
源码分析** | **Gradle** | **设计模式** | **C++** | **音视频** | +| **Java** | **Android** | **Android
扩展** | **Frame
work** | **性能优化** | **开源库
源码分析** | **Gradle** | **设计模式** | **C++** | **音视频** | | [:coffee:](#Java-知识点汇总) | [:iphone:](#Android-知识点汇总) | [:vibration_mode:](#Android-扩展知识点汇总) | [:file_folder:](#Framework-知识点汇总) | [:gear:](#性能优化知识点汇总) | [:mag:](#Android-开源库源码分析) | [:notebook_with_decorative_cover:](#Gradle知识点汇总) | [:hammer_and_wrench:](#设计模式汇总) | [:copyright:](#C知识点汇总) | [:vhs:](#音视频知识点汇总) | | **A** | **B** | **C** | 持续更新中 | | | | | | | | **计算机
网络基础** | **Kotlin** | **常见面试
算法题** | 持续更新中 | | | | | | | From de2430551f973a00be97b5e7b57d4d17af32c5af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Mon, 13 Dec 2021 22:06:58 +0800 Subject: [PATCH 07/29] Add files via upload --- ...06\347\202\271\346\261\207\346\200\273.md" | 9508 +++++++++++++++++ ...06\347\202\271\346\261\207\346\200\273.md" | 9098 ++++++++++++++++ 2 files changed, 18606 insertions(+) create mode 100644 "Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" create mode 100644 "\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" diff --git "a/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" "b/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" new file mode 100644 index 0000000..445651c --- /dev/null +++ "b/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" @@ -0,0 +1,9508 @@ +# Handler + +## Handler机制实现原理(一)宏观理论分析与Message源码分析 + +### Message: + +定义: + +```java +public` `final` `class` `Message ``implements` `Parcelable +``` + +Message类是个final类,就是说不能被继承,同时Message类实现了Parcelable接口,我们知道android提供了一种新的类型:Parcel。**本类被用作封装数据的容器,是链表结构,**有个属性next和sPool,这两个变量是不同的,具体什么不同看下文。 + +文档描述: + +```java +Defines a message containing a description and arbitrary data object that can be sent to a {@link Handler}. This object contains two extra int fields and an +extra object field that allow you to not do allocations in many cases. +``` + +定义一个包含任意类型的描述数据对象,此对象可以发送给Handler。对象包含两个额外的int字段和一个额外的对象字段,这样可以使得在很多情况下不用做分配工作。尽管Message的构造器是公开的,但是获取Message对象的最好方法是调用Message.obtain()或者Handler.obtainMessage(), 这样是从一个可回收对象池中获取Message对象。 + +#### 看一下全局变量:有好多存数据的对象。 + +```java +public int what; +public int arg1; +public int arg2; +public Object obj; +public Messenger replyTo; +/*package*/ int flags; +/*package*/ long when; + +/*package*/ Bundle data; + +/*package*/ Handler target; + +/*package*/ Runnable callback; + +// sometimes we store linked lists of these things +/*package*/ Message next; + +private static final Object sPoolSync = new Object(); +private static Message sPool; +private static int sPoolSize = 0; + +private static final int MAX_POOL_SIZE = 50; + +private static boolean gCheckRecycle = true; +``` + +1. what:用户定义消息代码以便收件人可以识别这是哪一个Message。每个Handler用它自己的名称空间为消息代码,所以您不需要担心你的Handler与其他handler冲突。 +2. arg1、arg2:如果只是想向message内放一些整数值,可以使用arg1和arg2来代替setData方法。 +3. obj:发送给接收器的任意对象。当使用Message对象在线程间传递消息时,如果它包含一个Parcelable的结构类(不是由应用程序实现的类),此字段必须为非空(non-null)。其他的数据传输则使用setData(Bundle)方法。注意Parcelable对象是从FROYO版本以后才开始支持的。 +4. replyTo:指明此message发送到何处的可选Messenger对象。具体的使用方法由发送者和接受者决定。 +5. FLAG_IN_USE:判断Message是否在使用( default 包内可见) +6. FLAG_ASYNCHRONOUS:如果设置message是异步的。 +7. FLAGS_TO_CLEAR_ON_COPY_FROM:明确在copyFrom方法 +8. 其他参数都比较简单,不详述 + +#### Obtain方法: + +  + +```java +//从全局池中返回一个新的Message实例。在大多数情况下这样可以避免分配新的对象。 +//是一个静态方法 +public static Message obtain() { + synchronized (sPoolSync) { + if (sPool != null) { + Message m = sPool; + sPool = m.next; + m.next = null; + m.flags = 0; // clear in-use flag + sPoolSize--; + return m; + } + } + return new Message(); + } +``` + +  在看它一系列的重载方法: + +```java +/** + * Same as {@link #obtain()}, but copies the values of an existing + * message (including its target) into the new one. + * @param orig Original message to copy. + * @return A Message object from the global pool. + */ +public static Message obtain(Message orig) { + Message m = obtain(); + m.what = orig.what; + m.arg1 = orig.arg1; + m.arg2 = orig.arg2; + m.obj = orig.obj; + m.replyTo = orig.replyTo; + m.sendingUid = orig.sendingUid; + if (orig.data != null) { + m.data = new Bundle(orig.data); + } + m.target = orig.target; + m.callback = orig.callback; + + return m; + } + /** + 设置target + */ +public static Message obtain(Handler h) { + Message m = obtain(); + m.target = h; + + return m; + } + /** + * Same as {@link #obtain(Handler)}, but assigns a callback Runnable on + * the Message that is returned. + * @param h Handler to assign to the returned Message object's target member. + * @param callback Runnable that will execute when the message is handled. + * @return A Message object from the global pool. + */ + public static Message obtain(Handler h, Runnable callback) { + Message m = obtain(); + m.target = h; + m.callback = callback; + + return m; + } +/** + * Same as {@link #obtain()}, but sets the values of the target, what, + * arg1, arg2, and obj members. + 。。。。 + * @param obj The obj value to set. + * @return A Message object from the global pool. + */ + public static Message obtain(Handler h, int what, + int arg1, int arg2, Object obj) { + Message m = obtain(); + m.target = h; + m.what = what; + m.arg1 = arg1; + m.arg2 = arg2; + m.obj = obj; + + return m; + } +``` + +还有几个没列举出来,都是先调用obtain()方法,然后把获取的Message实例加上各种参数。代码一目了然。。。 + +#### recycle():回收当前message到全局池 + +  + +```java +/** + * Return a Message instance to the global pool. + *

+ * You MUST NOT touch the Message after calling this function because it has + * effectively been freed. It is an error to recycle a message that is currently + * enqueued or that is in the process of being delivered to a Handler. + *

+ */ + public void recycle() { + if (isInUse()) { + if (gCheckRecycle) { + throw new IllegalStateException("This message cannot be recycled because it " + + "is still in use."); + } + return; + } + recycleUnchecked(); + } + + /** + * Recycles a Message that may be in-use. + * Used internally by the MessageQueue and Looper when disposing of queued Messages. + */ + void recycleUnchecked() { + // Mark the message as in use while it remains in the recycled object pool. + // Clear out all other details. + flags = FLAG_IN_USE; + what = 0; + arg1 = 0; + arg2 = 0; + obj = null; + replyTo = null; + sendingUid = -1; + when = 0; + target = null; + callback = null; + data = null; + + synchronized (sPoolSync) { + if (sPoolSize < MAX_POOL_SIZE) { + next = sPool; + sPool = this; + sPoolSize++; + } + } + } +``` + +向全局池中返回一个Message实例。一定不能在调用此函数后再使用Message——它实际上已经被释放。 + +getWhen: + +```java +/** + * Return the targeted delivery time of this message, in milliseconds. + */ + public long getWhen() { + return when; + } +``` + +返回此消息的传输时间,以毫秒为单位。 + + + + setTarget,getTarget: + +```java +//设置handler和返回handler +public void setTarget(Handler target) { + this.target = target; + } + /** + * Retrieve the a {@link android.os.Handler Handler} implementation that + * will receive this message. The object must implement + * {@link android.os.Handler#handleMessage(android.os.Message) + * Handler.handleMessage()}. Each Handler has its own name-space for + * message codes, so you do not need to + * worry about yours conflicting with other handlers. + */ + public Handler getTarget() { + return target; + } +``` + +获取将接收此消息的Handler对象。此对象必须要实现Handler.handleMessage()方法。每个handler各自包含自己的消息代码,所以不用担心自定义的消息跟其他handlers有冲突。 + +### setData: + + 设置一个可以是任何类型值的bundle。 + +```java +/** + * Sets a Bundle of arbitrary data values. Use arg1 and arg2 members + * as a lower cost way to send a few simple integer values, if you can. + * @see #getData() + * @see #peekData() + */ + public void setData(Bundle data) { + this.data = data; + } +``` + +  getData,peekData + +```java +public Bundle getData() { + if (data == null) { + data = new Bundle(); + } + + return data; + } +public Bundle peekData() { + return data; +} +``` + +### 发送消息的一些方法: + +```java +/**向Handler发送此消息,getTarget()方法可以获取此Handler。如果这个字段没有设置会抛出个空指针异常。 + * Sends this Message to the Handler specified by {@link #getTarget}. + * Throws a null pointer exception if this field has not been set. + */ + public void sendToTarget() { + target.sendMessage(this); + }   +``` + +#### 构造方法: + +```java +/** Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}). + */ + public Message() { + } +//推荐使用Message.obtain() +``` + +#### writeToParcel: + +```Java +public void writeToParcel(Parcel dest, int flags) { + if (callback != null) { + throw new RuntimeException( + "Can't marshal callbacks across processes."); + } + dest.writeInt(what); + dest.writeInt(arg1); + dest.writeInt(arg2); + if (obj != null) { + try { + Parcelable p = (Parcelable)obj; + dest.writeInt(1); + dest.writeParcelable(p, flags); + } catch (ClassCastException e) { + throw new RuntimeException( + "Can't marshal non-Parcelable objects across processes."); + } + } else { + dest.writeInt(0); + } + dest.writeLong(when); + dest.writeBundle(data); + Messenger.writeMessengerOrNullToParcel(replyTo, dest); + dest.writeInt(sendingUid); + } +``` + + 将类的数据写入外部提供的Parcel中和从Parcel中读取数据。 + +## Handler机制实现原理(二)MessageQueue的源码分析 + +> 看源码有一段时间了,越来越能从代码中感觉到工程师们满满的激情,无论是基础Java语法还是高级的语言特性都被发挥的淋漓尽致,写的恰到好处。分析源码的过程,何尝不是与大神们进行灵魂沟的过程。 + +MessageQueue属于低层类且依附于Looper,Looper外其他类不应该单独创建它,如果想使用MessageQueue可以从Looper类中得到它。 + +### 消息队列存储原理 + +再上一章Message源码分析中我们知道了Message内部维持了一个链表缓存池来避免重复创建Message对象造成的额外消耗,以静态属性`private static Message sPool`作为缓存池链表头,`Message next;`作为链表的next指针。 + +有意思的是Message对象中`next`指针的不止用于链表缓存池,在MessageQueue中也采用同样的方法存储消息对象: + + + +```java +public final class MessageQueue { + ...... + Message mMessages; + ...... +} +``` + +上面代码中的`mMessages`就是MessageQueue用来维持消息队列的链表头,至于它是如何存储的,后面再说。 + +### 使用JNI实现的native方法 + +MessageQueue的源码调用了多个的C/C++方法,这类方法使用前都会用关键字`native`声明一下。 + +这些方法所属的底层C++代码创建了属于native层自己的`NativeMessageQueue`和`NativeLooper`消息模型。它们对Java层作用其实就是**控制线程是否阻塞**。 + +当我们想要从MessageQueue中取出消息时,碰巧队列是空的或即将取出的消息还没到被处理时间,那么我们就需要将线程阻塞掉等待队列中有消息时再取出。 + +下面就是MessageQueue中的`native`方法: + + + +```java + // 初始化 + private native static long nativeInit(); + // 注销 + private native static void nativeDestroy(long ptr); + // 让线程阻塞timeoutMillis毫秒 + private native void nativePollOnce(long ptr, int timeoutMillis); + // 立刻唤醒线程 + private native static void nativeWake(long ptr); + // 线程是否处于阻塞状态 + private native static boolean nativeIsPolling(long ptr); + // 设置文件描述符 + private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events); +``` + +**文件描述符与ePoll指令** + +消息队列里控制线程阻塞状态的`native`代码本质是用Linux指令`ePoll`完成的,在这之前需要先了解一点,Linux内核依靠“文件描述符(file descriptor)”来完成所有的文件读写访问操作,它更像是一个文件的索引值。而我们用到的`ePoll`指令就是用来监听文件描述符是否有可I/O操作的。*(这都是一些Linux相关知识)* + +### 创建与销毁 + +上面说了MessageQueue是依附于Looper的,所以本节分析的创建与销毁方法其实都是给Looper调用的,MessageQueue只提供了一个带参的构造方法来创建对象: + + + +```java + // 当前MessageQueue是否可以退出 + private final boolean mQuitAllowed; + + // native层中NativeMessageQueue队列指针的地址 + // mPtr等于0时表示退出队列 + private long mPtr; + + // native层代码,创建native层中NativeMessageQueue + private native static long nativeInit(); + + // 构造方法 + MessageQueue(boolean quitAllowed) { + mQuitAllowed = quitAllowed; + // 执行native层方法 + mPtr = nativeInit(); + } +``` + +构造方法中`boolean quitAllowed`参数的意思是当前这个MessageQueue是否可以手动退出,为什么要控制能否手动退出呢?这里先说一个结论:Android 系统中要求UI线程不可手动退出,而其他Worker线程则**全部都是**可以的。*(具体的操作在Looper和UI线程中)* + +**那么退出是什么意思呢?** + +退出就是当前这个MessageQueue停止服务,将队列中已存在的所有消息全部清空,看看源码中退出方法都做了什么: + + + +```java + // 是否已经退出了 + private boolean mQuitting; + + // native方法,退出队列 + private native static void nativeDestroy(long ptr); + + + // 退出队列 + void quit(boolean safe) { + + // 如果不是可以手动退出的,抛出异常 + if (!mQuitAllowed) { + throw new IllegalStateException("Main thread not allowed to quit."); + } + + synchronized (this) { + // 如果已经退出了直接结束方法 + if (mQuitting) { + return; + } + // 标记为已退出状态 + mQuitting = true; + + // 两种清除队列中消息的方法 + if (safe) { + removeAllFutureMessagesLocked(); + } else { + removeAllMessagesLocked(); + } + + // 注销 + nativeWake(mPtr); + } + } +``` + +方法`quit(boolean safe)`中的参数`safe`决定了到底执行哪种清除消息的方法: + +- `removeAllMessagesLocked()`,简单暴力直接清除掉队列中所有的消息。 +- `removeAllFutureMessagesLocked()`,清除掉可能还没有被处理的消息。 + +`removeAllMessagesLocked()`方法的逻辑很简单,从队列头中取消息,有一个算一个,全部拿出来回收掉。: + + + +```csharp + private void removeAllMessagesLocked() { + Message p = mMessages; + while (p != null) { + Message n = p.next; + p.recycleUnchecked(); + p = n; + } + mMessages = null; + } +``` + +然而,`removeAllFutureMessagesLocked()`方法的逻辑稍微多一点: + + + +```csharp + private void removeAllFutureMessagesLocked() { + + // 获取当前系统时间 + final long now = SystemClock.uptimeMillis(); + Message p = mMessages; + if (p != null) { + // 判断当前消息对象的预处理时间是否晚于当前时间 + if (p.when > now) { + // 如果当前消息对象的预处理时间晚于当前时间直接全部暴力清除 + removeAllMessagesLocked(); + } else { + Message n; + // 如果当前消息对象的预处理时间并不晚于当前时间 + // 说明有可能这个消息正在被分发处理 + // 那么就跳过这个消息往后找晚于当前时间的消息 + for (;;) { + n = p.next; + if (n == null) { + return; + } + if (n.when > now) { + // 如果找到了晚于当前时间的消息结束循环 + break; + } + p = n; + } + p.next = null; + do { + // n就是那个晚于当前时间的消息 + // 从n开始之后的消息全部回收 + p = n; + n = p.next; + p.recycleUnchecked(); + } while (n != null); + } + } + } +``` + +这么看来这个方法名字起的还挺靠谱的,很好的解释了是要删除还没有被处理的消息。 + +### 消息入队管理enqueueMessage()方法 + + + +```csharp + // 消息入队 + // 参数when就是此消息应该被处理的时间 + boolean enqueueMessage(Message msg, long when) { + // 如果此消息的target也就是宿主handler是空的抛异常 + if (msg.target == null) { + throw new IllegalArgumentException("Message must have a target."); + } + + // 如果此消息是in-use状态抛异常,in-use的消息不可拿来使用 + if (msg.isInUse()) { + throw new IllegalStateException(msg + " This message is already in use."); + } + + //上锁 + synchronized (this) { + + // 如果当前MessageQueue已经退出了抛异常并释放掉此消息 + if (mQuitting) { + IllegalStateException e = new IllegalStateException( + msg.target + " sending message to a Handler on a dead thread"); + Log.w(TAG, e.getMessage(), e); + msg.recycle(); + return false; + } + + // 将消息标记为in-use状态 + msg.markInUse(); + // 设置应该被处理的时间 + msg.when = when; + // 拿到队列头 + Message p = mMessages; + + + // 是否需要唤醒线程 + boolean needWake; + + // p等于空说明队列是空的 + // when等于0表示强制把此消息插入队列头部,最先处理 + // when小于队列头的when说明此消息应该被处理的时间比队列中第一个要处理的时间还早 + // 以上情况满足任意一种直接将消息插入队列头部 + if (p == null || when == 0 || when < p.when) { + // 将消息插入队列头部 + msg.next = p; + mMessages = msg; + needWake = mBlocked; + } else { + + // 线程已经被阻塞&&消息存在宿主Handler&&消息是异步的 + needWake = mBlocked && p.target == null && msg.isAsynchronous(); + + + //如果上述条件都不满足就要按照消息应该被处理的时间插入队列中 + Message prev; + for (;;) { + // 两根相邻的引用一前一后从队列头开始依次向后移动 + prev = p; + p = p.next; + // 如果队列到尾部了或者找到了处理时间早于自身的消息就结束循环 + if (p == null || when < p.when) { + break; + } + + // 如果入队的消息是异步的而排在它前面的消息有异步的就不需要唤醒 + if (needWake && p.isAsynchronous()) { + needWake = false; + } + } + // 将新消息插在这一前一后两个引用中间,完成入队 + msg.next = p; // invariant: p == prev.next + prev.next = msg; + } + + // 判断是否需要唤醒线程 + if (needWake) { + nativeWake(mPtr); + } + } + return true; + } +``` + +总结一下消息入队的逻辑大致分为如下几步: + +1. 检查消息合法性,包括宿主`target`是否为空,是否为in-use状态,队列是否还存活。 +2. 如果满足条件【队列为空、when等于0、此消息应被处理的时间比队列中第一个要处理的时间还早】中的任意一个直接将此消息插在队列头部最先被处理。 +3. 如果以上三个条件均不满足,那么就从头遍历队列根据被处理时间找到它的位置。 + +### 同步消息拦截器 + +除了`enqueueMessage()`方法可以向队列中添加消息外,还有一个`postSyncBarrier()`方法也可以向队列添加消息,但它不是添加普通的消息,我们将它添加的特殊Message称为**同步消息拦截器**。 + +顾名思义,该拦截器只会影响同步消息。复习一下上节中分析到的东西,我们默认发送的消息都是同步的,只有某个Message被调用了`setAsynchronous(true)`后才是异步消息。同步消息受队列限制依次有序的等待处理,异步消息也不受限制。 + +消息拦截器与普通消息的差异在于拦截器的`target`是空的,正常我们通过`enqueueMessage()`方法入队的消息由于限制`target`是不能为空的。 + + + +```csharp + // 标识拦截器的token + private int mNextBarrierToken; + + private int postSyncBarrier(long when) { + + synchronized (this) { + // 得到拦截器token + final int token = mNextBarrierToken++; + // 实例化一个消息对象 + final Message msg = Message.obtain(); + // 将对象设置为in-use状态 + msg.markInUse(); + // 设置时间 + msg.when = when; + // 将token存于消息的常用属性arg1中 + msg.arg1 = token; + + Message prev = null; + Message p = mMessages; + + // 如果when不等于0就在队列中按时间找到它的位置 + if (when != 0) { + while (p != null && p.when <= when) { + prev = p; + p = p.next; + } + } + // 如果prev不等于空就把拦截器插入 + // 如果prev等于空直接插入队列头部 + if (prev != null) { // invariant: p == prev.next + msg.next = p; + prev.next = msg; + } else { + msg.next = p; + mMessages = msg; + } + + // 拦截器入队成功,返回对应token + return token; + } + } +``` + +总体来说添加拦截器的方法跟正常消息入队差不多,值得一提的就是Message的`target`是空的,然后`arg1`保存着拦截器的唯一标识`token`。 + +`token`的作用是找到对应的拦截器删除,看看删除拦截器的方法。 + + + +```java + public void removeSyncBarrier(int token) { + synchronized (this) { + Message prev = null; + Message p = mMessages; + // 遍历队列找到指定拦截器 + // 查找条件:target为空,arg1等于指定token值 + while (p != null && (p.target != null || p.arg1 != token)) { + prev = p; + p = p.next; + } + // 如果p等于空说明没找到 + if (p == null) { + throw new IllegalStateException("The specified message queue synchronization " + + " barrier token has not been posted or has already been removed."); + } + + // 是否需要唤醒线程 + final boolean needWake; + + // 在队列中移除掉拦截器 + if (prev != null) { + prev.next = p.next; + // 如果prev不等于空说明拦截器前面还有别的消息,就不需要唤醒 + needWake = false; + } else { + mMessages = p.next; + // 拦截器在队列头部,移除它之后如果队列空了或者它的下一个消息是个正常消息就需要唤醒 + needWake = mMessages == null || mMessages.target != null; + } + + // 回收 + p.recycleUnchecked(); + + // 判断是否需要唤醒 + if (needWake && !mQuitting) { + nativeWake(mPtr); + } + } + } +``` + +### 队列空闲处理器IdleHandler + +由于在从队列中取出消息时队里可能是空的,这时候就会阻塞线程等待消息到来。每次队列中没有消息而进入的阻塞状态,我们叫它为“空闲状态”。 + +讲道理实际使用中队列空闲状态的情况还是很常见的,为了更好的利用资源,也为了更好的掌握线程的状态,开发人员就设计了这么一个“队列空闲处理器IdleHandler”。 + +`IdleHandler`是MessageQueue类下的一个子接口,只包含了一个方法: + + + +```csharp + public static interface IdleHandler { + /** + * 当线程的MessageQueue等待更多消息时会调用该方法。 + * + * 返回值:true代表只执行一次,false代表会一直执行它 + */ + boolean queueIdle(); + } +``` + +MessageQueue为我们提供了添加和删除IdleHandler的方法: + + + +```java + //使用一个ArrayList存储IdleHandler + private final ArrayList mIdleHandlers = new ArrayList(); + + // 添加一个IdleHandler + public void addIdleHandler(@NonNull IdleHandler handler) { + if (handler == null) { + throw new NullPointerException("Can't add a null IdleHandler"); + } + synchronized (this) { + mIdleHandlers.add(handler); + } + } + + // 删除一个IdleHandler + public void removeIdleHandler(@NonNull IdleHandler handler) { + synchronized (this) { + mIdleHandlers.remove(handler); + } + } +``` + +### 消息出队管理next()方法 + +`next()`方法很长,先大致看一下源码: + + + +```java + private IdleHandler[] mPendingIdleHandlers; + + Message next() { + + // mPtr是从native方法中得到的NativeMessageQueue地址 + // 如果mPtr等于0说明队列不存在或被清除掉了 + final long ptr = mPtr; + if (ptr == 0) { + return null; + } + + // 待处理的IdleHandler数量,因为代表数量,所以只有第一次初始化时为-1 + int pendingIdleHandlerCount = -1; + + + // 线程将被阻塞的时间 + // -1:一直阻塞 + // 0:不阻塞 + // >0:阻塞nextPollTimeoutMillis 毫秒 + int nextPollTimeoutMillis = 0; + + + // 开始死循环,下面的代码都是在循环中,贼长! + for (;;) { + + // 如果nextPollTimeoutMillis 不等于0说明要阻塞线程了 + if (nextPollTimeoutMillis != 0) { + // 为即将长时间阻塞做准备把该释放的对象都释放了 + Binder.flushPendingCommands(); + } + + // 阻塞线程操作 + nativePollOnce(ptr, nextPollTimeoutMillis); + + synchronized (this) { + // 得到当前时间 + final long now = SystemClock.uptimeMillis(); + + Message prevMsg = null; + Message msg = mMessages; + + // 判断队列头是不是同步拦截器 + if (msg != null && msg.target == null) { + // 如果是拦截器就向后找一个异步消息 + do { + prevMsg = msg; + msg = msg.next; + } while (msg != null && !msg.isAsynchronous()); + } + + // 判断队列是否有可以取出的消息 + if (msg != null) { + + if (now < msg.when) { + // 如果待取出的消息还没有到应该被处理的时间就让线程阻塞到应该被处理的时间 + nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); + } else { + // 直接就能取出消息,所以不用阻塞线程 + mBlocked = false; + + 将消息从队列中剥离出来 + if (prevMsg != null) { + prevMsg.next = msg.next; + } else { + mMessages = msg.next; + } + // 让消息脱离队列 + msg.next = null; + + if (DEBUG) Log.v(TAG, "Returning message: " + msg); + + // 设置为in-use状态 + msg.markInUse(); + // 返回取出的消息,结束循环,结束next()方法 + return msg; + } + } else { + // 队列中没有可取出的消息,nextPollTimeoutMillis 等于-1让线程一直阻塞 + nextPollTimeoutMillis = -1; + } + + // 如果队列已经退出了直接注销和结束方法 + if (mQuitting) { + dispose(); + return null; + } + + // IdleHandler初始化为-1,所以在本循环中该条件成立的次数 <= 1 + if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { + // 得到IdleHandler的数量 + pendingIdleHandlerCount = mIdleHandlers.size(); + } + + // pendingIdleHandlerCount 小于或等于0说明既没有合适的消息也没有合适的闲时处理 + if (pendingIdleHandlerCount <= 0) { + // 直接进入下次循环阻塞线程 + mBlocked = true; + continue; + } + + // 代码执行到此处就说明线程中有待处理的IdleHandler + // 那么就从IdleHandler集合列表中取出待处理的IdleHandler + if (mPendingIdleHandlers == null) { + // 初始化待处理IdleHandler数组,最小长度为4 + mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; + } + + // 从IdleHandler集合中获取待处理的IdleHandler + mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); + } + + // ==========到此处同步代码块已经结束========== + + + for (int i = 0; i < pendingIdleHandlerCount; i++) { + // 取出一个IdleHandler + final IdleHandler idler = mPendingIdleHandlers[i]; + // 释放掉引用 + mPendingIdleHandlers[i] = null; + + // IdleHandler的执行模式,true=执行一次,false=总是执行 + boolean keep = false; + + try { + // 执行IdleHandler的queueIdle()代码,得到执行模式 + keep = idler.queueIdle(); + } catch (Throwable t) { + Log.wtf(TAG, "IdleHandler threw exception", t); + } + + // 通过执行模式判断是否需要移除掉对应的IdleHandler + if (!keep) { + synchronized (this) { + mIdleHandlers.remove(idler); + } + } + } + + // 处理完了所有IdleHandler把数量清0 + pendingIdleHandlerCount = 0; + + // 因为执行了IdleHandler的代码块,有可能已经有新的消息入队了 + // 所以到这里就不阻塞线程,直接去查看有没有新消息 + nextPollTimeoutMillis = 0; + } + } +``` + +消息出队的核心代码的逻辑都在一个庞大的死循环`for(;;)`中,其流程如下: + +0,循环开始。 + +1,根据nextPollTimeoutMillis值阻塞线程,初始值为0:不阻塞线程。 + +2,将【待取出消息指针】指向队列头。 + +3,如果队列头是同步拦截器的话就将【待取出消息指针】指向队列头后面最近的一个异步消息。 + +4,如果【待取出消息指针】不可用(msg == null)说明队列中没有可取出的消息,让nextPollTimeoutMillis 等于-1让线程一直阻塞,等待新消息到来时唤醒它。 + +5,如果【待取出消息指针】可用(msg != null)再判断一下消息的待处理时间。 + +- 如果消息的待处理时间大于当前时间(now < msg.when)说明当前消息还没到要处理的时间,让线程阻塞到消息待处理的指定时间。 +- 如果消息的待处理时间小于当前时间(now > msg.when)就直接从队列中取出消息返回给调用处。(此处会直接结束整个循环,结束next()方法。) + +6,如果队列已经退出了直接结束next()方法。 + +7,如果是第一次循环就初始化IdleHandler数量的局部变量pendingIdleHandlerCount 。 + +8,如果IdleHandler数量小于等于0说明没有合适的IdleHandler,直接进入下次循环阻塞线程。(此处会直接结束本次循环。) + +9,初始化IdleHandler数组,里面保存着本地待处理的IdleHandler。 + +10,遍历IdleHandler数组,执行对应的queueIdle()方法。 + +11,执行完所有IdleHandler之后,将IdleHandler数量清0。 + +12,因为执行了IdleHandler的代码块,有可能已经有新的消息入队了, 所以让nextPollTimeoutMillis 等于0不阻塞线程,直接去查看有没有新消息。 + +13,本次循环结束,开始新一轮循环。 + +### 总结 + +1. MessageQueue队列消息是有序的,按消息待处理时间依次排序。 +2. 同步拦截器可以拦截它之后的所有同步消息,直到这个拦截器被移除。 +3. 取出消息时如果没有合适的消息线程会阻塞 + +## Handler机制实现原理(三)Looper的源码分析 + +> 刚看源码的时候:“这TM写的是啥?那写的又TM是啥?” +> 研究明白了之后:“奥,原来就这点玩意儿啊,太简单了。” + +Looper的职责很单一,就是单纯的从MessageQueue中取出消息分发给消息对应的宿主Handler,因此它的代码不多(300行左右)。 + +Looper是线程独立的且每个线程只能存在一个Looper。 + +Looper会根据自己的存活情况来创建和退出属于它自己的MessageQueue。 + +### 创建与退出Looper + +上面的结论中提到了Looper是线程独立的且每个线程只能存在一个Looper。所以构造Looper实例的方法类似于单例模式。隐藏构造方法,对外提供了两个指定的获取实例方法`prepare()`和`prepareMainLooper()`。 + + + +```java + // 应用主线程(UI线程)Looper实例 + private static Looper sMainLooper; + + // Worker线程Looper实例,用ThreadLocal保存的对象都是线程独立的 + static final ThreadLocal sThreadLocal = new ThreadLocal(); + + // 与当前Looper对应的消息队列 + final MessageQueue mQueue; + + // 当前Looper所以的线程 + final Thread mThread; + + /** + * 对外公开初始化方法 + * + * 在普通线程中初始化Looper调用此方法 + */ + public static void prepare() { + // 初始化一个可以退出的Looper + prepare(true); + } + + /** + * 对外公开初始化方法 + * + * 在应用主线程(UI线程)中初始化Looper调用此方法 + */ + public static void prepareMainLooper() { + + // 因为是主线程,初始化一个不允许退出的Looper + prepare(false); + + synchronized (Looper.class) { + // 如果sMainLooper不等于空说明已经创建过主线程Looper了,不应该重复创建 + if (sMainLooper != null) { + throw new IllegalStateException("The main Looper has already been prepared."); + } + sMainLooper = myLooper(); + } + } + + /** + * 内部私有初始化方法 + * @param quitAllowed 是否允许退出Looper + */ + private static void prepare(boolean quitAllowed) { + // 每个线程只能有一个Looper + if (sThreadLocal.get() != null) { + throw new RuntimeException("Only one Looper may be created per thread"); + } + // 保存实例 + sThreadLocal.set(new Looper(quitAllowed)); + } + + /** + * 私有构造方法 + * @param quitAllowed 是否允许退出Looper + */ + private Looper(boolean quitAllowed) { + // 初始化MessageQueue + mQueue = new MessageQueue(quitAllowed); + // 得到当前线程实例 + mThread = Thread.currentThread(); + } +``` + +真正创建Looper实例的构造方法中其实很简单,就是创建了对应的MessageQueue实例,然后得到当前线程,值得注意的是MessageQueue和线程实例都是被`final`关键字修饰的,只能被赋值一次。 + +对外公开初始化方法`prepareMainLooper()`是为应用主线程(UI线程)准备的,应用刚被创建就会调用该方法,所以我们不该再去调用它。 + +开发者可以通过调用对外公开初始化方法`prepare()`对自己的worker线程创建Looper,但是要注意只能初始化一次。 + +调用`Looper.prepare()`方法初始化完成后,可以调用`myLooper()`和`myQueue()`方法得到当前线程对应的实例。 + + + +```java + public static @Nullable Looper myLooper() { + return sThreadLocal.get(); + } + + public static @NonNull MessageQueue myQueue() { + return myLooper().mQueue; + } +``` + +**退出Looper** + +退出Looper有安全与不安全两种退出方法,其实对应的就是MessageQueue的安全与不安全方法: + + + +```cpp + public void quit() { + mQueue.quit(false); + } + + public void quitSafely() { + mQueue.quit(true); + } +``` + +什么安全退出,什么是不安全退出,在MessageQueue源码中分析过。 + +### 运行Looper处理消息 + +调用`Looper.prepare()`方法初始化完成Looper后就可以让Looper去工作了,只需要调用`Looper.loop()`方法即可。 + + + +```java + public static void loop() { + // 得到当前线程下的Looper + final Looper me = myLooper(); + + // 如果还没初始化过抛异常 + if (me == null) { + throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); + } + + // 得到当前线程下与Looper对应的消息队列 + final MessageQueue queue = me.mQueue; + + // 得到当前线程的唯一标识(uid+pid),作用是下面每次循环都判断一下线程有没有被切换 + // 不知道为什么要调用两次该方法 + Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); + + // 进入死循环不断取出消息 + for (;;) { + + // 从队列中取出一个消息,这可能会阻塞线程 + Message msg = queue.next(); + + // 如果消息是空的,说明队列已经退出了,直接结束循环,结束方法 + if (msg == null) { + return; + } + + // 打印日志 + final Printer logging = me.mLogging; + if (logging != null) { + logging.println(">>>>> Dispatching to " + msg.target + " " + + msg.callback + ": " + msg.what); + } + + // 性能分析相关的东西 + final long traceTag = me.mTraceTag; + if (traceTag != 0 && Trace.isTagEnabled(traceTag)) { + Trace.traceBegin(traceTag, msg.target.getTraceName(msg)); + } + + try { + //尝试将消息分发给宿主(Handler) + //dispatchMessage为宿主Handler的接收消息方法 + msg.target.dispatchMessage(msg); + } finally { + // 性能分析相关的东西 + if (traceTag != 0) { + Trace.traceEnd(traceTag); + } + } + + //打印日志 + if (logging != null) { + logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); + } + + + //得到当前线程的唯一标识 + final long newIdent = Binder.clearCallingIdentity(); + + //如果本次循环所在的线程与最开始不一样,打印日志记录 + if (ident != newIdent) { + Log.wtf(TAG, "Thread identity changed from 0x" + + Long.toHexString(ident) + " to 0x" + + Long.toHexString(newIdent) + " while dispatching to " + + msg.target.getClass().getName() + " " + + msg.callback + " what=" + msg.what); + } + + //消息分发完毕,回收消息到缓存池 + msg.recycleUnchecked(); + } + } +``` + +### 总结 + +Looper的功能很简单,核心方法`Looper.loop()`就是不断的从消息队列中取出消息分发给对应的宿主Handler,它与对应MessageQueue息息相关,一起创建,一起退出。 + +Looper更想强调的是线程的独立性与唯一性,利用`ThreadLocal`保证每个线程只有一个Looper实例的存在。利用静态构造实例方法保证不能重复创建Looper。 + +`Looper.prepareMainLooper()`是比较特殊的方法,它是给UI线程准备,理论上开发者在任何情况下都不应该调用它。 + +## Handler机制实现原理(四)handler的源码分析 + +Handler本身可在多线程之间调用,不管它在哪个线程发送消息,都会回到它被初始化的哪个线程中接收到消息。 + +### 初始化 + +Handler有**7个**构造方法,分别对应不同的参数来初始化不同的Handler属性,但是真正完成初始化操作的只有两个构造方法: + + + +```java + // 是否需要查找潜在的漏洞 + private static final boolean FIND_POTENTIAL_LEAKS = false; + + /** + * 将Callback接口作为构造方法参数,可以用作接收消息的回调 + * 这样就可以省去自己重写Handler自身的handleMessage方法 + * + * @param msg 接收到的消息 + * @return 是否需要进一步处理,即调用Handler自身的handleMessage方法 + */ + public interface Callback { + public boolean handleMessage(Message msg); + } + + final Looper mLooper; + final MessageQueue mQueue; + final Callback mCallback; + + // 发送的消息是否为异步的,默认是false + final boolean mAsynchronous; + + /** + * @hide 隐藏的构造方法,外部不可见 + */ + public Handler(Callback callback, boolean async) { + + // 检查是否存在内存泄漏的可能 + if (FIND_POTENTIAL_LEAKS) { + final Class klass = getClass(); + if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && + (klass.getModifiers() & Modifier.STATIC) == 0) { + Log.w(TAG, "The following Handler class should be static or leaks might occur: " + + klass.getCanonicalName()); + } + } + + // 得到当前线程的Looper + mLooper = Looper.myLooper(); + + // 如果Looper还没初始化抛出异常 + if (mLooper == null) { + throw new RuntimeException( + "Can't create handler inside thread that has not called Looper.prepare()"); + } + + // 得到当前线程的MessageQueue + mQueue = mLooper.mQueue; + + + mCallback = callback; + mAsynchronous = async; + } + + + public Handler(Looper looper, Callback callback, boolean async) { + mLooper = looper; + mQueue = looper.mQueue; + mCallback = callback; + mAsynchronous = async; + } +``` + +从上面代码的得知,构造方法初始化的工作就是给`mLooper`,`mQueue`,`mCallback`和`mAsynchronous`这几个关键的属性赋值.`mLooper`和`mQueue`自然就是当前线程下的Looper和MessageQueue了,如果传递了Looper参数就直接赋值,如果没传递就调用`Looper.myLooper();`得到当前线程的Looper。 + +`mCallback`是Handler内部定义的一个简单接口,其目的是为了**替代传统的接收消息方法**。当然使用`mCallback`的同时并不会影响正常的Handler消息分发。此处解释从后面接收消息时的逻辑就可以看到。 + +`mAsynchronous`的意思是**该Handler发送的消息是否是异步的**,从前面Message源码的文章中我们知道Message中有一个设置消息是否为异步消息的方法,MessageQueue对异步消息的处理也与同步消息不同。此处如果设置了`mAsynchronous`为`true`,那么这个Handler发送的所有消息就都是异步消息。 + +在有两个参数的构造方法中我们会发现有一段检查是否存在内存泄漏的代码,为什么会这样呢?在分析完发送消息和接收消息后再说这个。 + +当然,除了上面两个构造方法外还有其它几个构造方法,但均是调用上面两个方法: + + + +```csharp + public Handler() { + this(null, false); + } + + public Handler(Callback callback) { + this(callback, false); + } + + public Handler(Looper looper) { + this(looper, null, false); + } + + public Handler(Looper looper, Callback callback) { + this(looper, callback, false); + } + + /** + * @hide 隐藏的构造方法,外部不可见 + */ + public Handler(boolean async) { + this(null, async); + } +``` + +### 发送消息 + +使用Handler发送消息时我们知道它分为两类: + +- `postXXX()`方法切换回原线程。 +- `sendMessageXXX()`方法发送消息到原线程。 + +其实这两种方法本质都是发送一个Message对象到原线程,只不过`PostXXX()`方法是发送了一个只有`Runnable callback`属性的Message对象。 + +先来看一下`sendMessageXXX()`类的方法: + + + +```java + // 发送一条普通消息 + public final boolean sendMessage(Message msg){ + return sendMessageDelayed(msg, 0); + } + + // 发送一条空消息 + public final boolean sendEmptyMessage(int what){ + return sendEmptyMessageDelayed(what, 0); + } + + // 发送一条空的延时消息 + public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { + Message msg = Message.obtain(); + msg.what = what; + return sendMessageDelayed(msg, delayMillis); + } + + // 发送一条空的定时消息 + public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) { + Message msg = Message.obtain(); + msg.what = what; + return sendMessageAtTime(msg, uptimeMillis); + } + + // 发送一个普通的延时消息 + public final boolean sendMessageDelayed(Message msg, long delayMillis) { + if (delayMillis < 0) { + delayMillis = 0; + } + return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); + } + + // 发送一个普通的定时消息 + public boolean sendMessageAtTime(Message msg, long uptimeMillis) { + //得到消息队列 + MessageQueue queue = mQueue; + // 如果消息队列是空的记录日志然后结束方法 + if (queue == null) { + RuntimeException e = new RuntimeException( + this + " sendMessageAtTime() called with no mQueue"); + Log.w("Looper", e.getMessage(), e); + return false; + } + // 执行消息入队操作 + return enqueueMessage(queue, msg, uptimeMillis); + } +``` + +从上面的代码中发现,不管调用何种发送消息的方法,最后真正调用的都是`sendMessageAtTime()`方法。而真正发送的核心方法也就是入队方法是Handler的`enqueueMessage()`方法。 + + + +```cpp + private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { + // 将消息的宿主设置为当前Handler自身 + msg.target = this; + + //如果Handler被设置成了异步就把消息也设置成异步的 + if (mAsynchronous) { + msg.setAsynchronous(true); + } + + // 执行消息队列的入队操作 + return queue.enqueueMessage(msg, uptimeMillis); + } +``` + +看到这里终于明白了为啥Looper和MessageQueue一直在使用Message的`target`属性而我们却从来没有给它赋值过,是Handler在发送消息前自己赋值上去的。 + +看完了发送消息类的方法在看看切换线程类的方法干了什么: + + + +```java + + private static Message getPostMessage(Runnable r) { + Message m = Message.obtain(); + m.callback = r; + return m; + } + + private static Message getPostMessage(Runnable r, Object token) { + Message m = Message.obtain(); + m.obj = token; + m.callback = r; + return m; + } + + public final boolean post(Runnable r){ + return sendMessageDelayed(getPostMessage(r), 0); + } + + public final boolean postAtTime(Runnable r, long uptimeMillis) { + return sendMessageAtTime(getPostMessage(r), uptimeMillis); + } + + public final boolean postAtTime(Runnable r, Object token, long uptimeMillis){ + return sendMessageAtTime(getPostMessage(r, token), uptimeMillis); + } + + public final boolean postDelayed(Runnable r, long delayMillis){ + return sendMessageDelayed(getPostMessage(r), delayMillis); + } + + public final boolean postAtFrontOfQueue(Runnable r){ + return sendMessageAtFrontOfQueue(getPostMessage(r)); + } +``` + +上本节开头讲的一样,`postXXX()`类方法就是构造了一个只有`Runnable callback`的Message对象,然后走正常发送消息的方法。唯一有一个特例就是`postAtFrontOfQueue()`方法,它调用了`sendMessageAtFrontOfQueue()`方法是之前发送消息没有用到过的: + + + +```cpp + public final boolean sendMessageAtFrontOfQueue(Message msg) { + MessageQueue queue = mQueue; + if (queue == null) { + RuntimeException e = new RuntimeException( + this + " sendMessageAtTime() called with no mQueue"); + Log.w("Looper", e.getMessage(), e); + return false; + } + return enqueueMessage(queue, msg, 0); + } +``` + +原来这个方法特殊的地方就是在入队的时候时间参数为0,我们在MessageQueue源码知道如果入队消息的时间参数为0那么这个消息会被直接放在队列头。所以,`postAtFrontOfQueue()`方法就是直接在消息队列头部插入了一个消息。 + +### 接收消息 + +在Looper 的源码中我们知道每当从MessageQueue中取出一个消息时就会调用这个消息的宿主`target`中分发消息的方法: + + + +```cpp +// Looper分发消息 +msg.target.dispatchMessage(msg); +``` + +而这个宿主`target`也就是我们的Handler,所有Handler接收消息就是在这个`dispatchMessage()`方法中了: + + + +```csharp + public void dispatchMessage(Message msg) { + // 如果Message的callback不为空,说明它是一个通过postXXX()方法发送的消息 + if (msg.callback != null) { + // 直接运行这个callback + handleCallback(msg); + } else { + //如果mCallback 不为空说明Handler设置了Callback接口 + // 先执行接口处理消息的方法 + if (mCallback != null) { + // 如果callback接口处理完消息返回true说明它将消息拦截 + // 不再执行Handler自身的处理消息方法,直接结束方法 + if (mCallback.handleMessage(msg)) { + return; + } + } + // 调用Handler自身处理消息的方法 + handleMessage(msg); + } + } + + private static void handleCallback(Message message) { + // 运行callback,也就是这个Runnable接口 + message.callback.run(); + } + + // Handler自身处理消息的方法,开发者需要重新该方法来实现接收消息 + public void handleMessage(Message msg) { + } +``` + +由此可见,如果是单纯的`PostXXX()`方法发送的消息,Handler接收到了之后直接运行Message对象的Runnable接口,不会将它当做一个消息进行处理。 + +而我们的`mCallback`接口是完全可以替代Handler自身接收消息的方法,因为其高优先处理等级,它甚至可以选择拦截掉Handler自身的接收消息方法。 + +### 内存泄漏的可能 + +我们在使用Handler的时候写法一般如下: + + + +```java + private final Handler handler = new Handler(){ + + @Override + public void handleMessage(Message msg) { + + } + }; +``` + +由于重写了`handleMessage()`方法相当于生成了一个匿名内部类,也就相当于如下代码: + + + +```java + private final Handler handler = new MyHandler (); + + class MyHandler extends Handler{ + + @Override + public void handleMessage(Message msg) { + + } + } +``` + +可是你有没有想过内部类凭什么能够调用外部类的属性和方法呢?答案就是内部类隐式的持有着外部类的引用,编译器在创建内部类时把外部类的引用传入了其中,只不过是你看不到而已。 + +既然Handler作为内部类持有着外部类(多数情况为Activity)的引用,而Handler对应的一般都是耗时操作。当我们在子线程执行一项耗时操作时,用户退出程序,Activity需要被销毁,而Handler还在持有Activity的引用导致无法回收,就会引发内存泄漏。 + +**解决方法分为两步** + +1. 生成内部类时把内部类声明为静态的。 +2. 使用弱引用来持有外部类引用。 + +静态内部类不会持有外部类的引用,且弱引用不会阻止JVM回收对象。 + + + +```java + static class MyHandler extends Handler{ + + WeakReference mActivity ; + + public MyHandler(Activity activity){ + mActivity = new WeakReference(activity); + } + + @Override + public void handleMessage(Message msg) { + Activity activity = mActivity.get(); + + if (activity == null){ + return; + } + + // do something + + } + } +``` + +所以,在文章刚开始初始化方法中检查漏洞的代码其实就是检查这个内存泄漏的可能性: + + + +```dart + // 检查是否存在内存泄漏的可能 + if (FIND_POTENTIAL_LEAKS) { + final Class klass = getClass(); + // 是否为匿名类,内部类以及是否为静态类 + if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && + (klass.getModifiers() & Modifier.STATIC) == 0) { + Log.w(TAG, "The following Handler class should be static or leaks might occur: " + + klass.getCanonicalName()); + } + } +``` + +大功告成,完美! + +## Handler机制实现原理(五)总结 + +### Message缓存池 + +Android 的工程师们充分利用了Java的高级语言特性,即类中持有着一个类自身的属性作为经典数据结构中的链表`next`指针,以静态属性属于类本身的特性实现了链表的表头。这种模式给我了很大的启发,让我这种渣渣每逢想起都会惊讶“还有这种操作?”。 + +**为什么要有缓存池** + +了解完Handler整体机制后我猜测,Message功能十分单一且状态很少,它只是一个具体发送消息的载体,但是使用数量十分庞大,回收用过的Message不仅可以有效的减少重复消耗系统资源且回收它的成本很低,所以何乐而不为呢? + +**谁负责回收Message** + +我们使用Message时候知道调用`Message.obtain();`方法可以从缓存池中取出一个Message,有存才能有取,我们什么时候回收它呢?从源码中发现,Looper在分发Message给宿主Handler之后,确定了Message已经完成了它的使命直接就会将它回收。所以我们完全不用担心这个,我们发送的每个消息最后都会被回收。 + +### 真正的阻塞发生在MessageQueue + +MessageQueue维持的消息队列也是靠跟Message缓存池同样的原理生成的,每次消息出队时如果没有合适的待取出消息就会阻塞线程等待有合适的消息。 + +非常奇怪的是,MessageQueue线程的方式不是传统使用java实现的,而是通过JNI调用native层的C++代码实现的,C++代码中也实现了一套Looper+MessageQueue+Handler,阻塞线程的方式是调用Linux的监听文件描述符ePoll实现的。 + +我的猜测是因为Java代码需要经过JVM的帮助才能跟系统接触,这一过程会消耗性能,而C++代码则直接可以绕过这一个环节。所以,使用C++代码实现线程阻塞可能是性能上的需求。 + +### 为什么推荐使用Handler实现线程间通信 + +在没有真正了解Handler的时候以为Google的工程师们在Handler上使用了什么了不起的技术呢,所以才推荐开发者们使用Handler来实现线程间通信。 + +其实呢?Android是事件型驱动的系统,刚创建一个应用程序的主线程里就会被创建一个Looper来不断接受各种事件,所以说如果我们打开一个程序什么都不操作,这个程序就有可能是阻塞状态的,因为他没有任何事件需要去处理。反之,我们在自己的UI线程里执行一项耗时操作,主线程Looper一直在处理这个任务而无法分身处理其它的事件这时候就有可能ANR了。 + +所以,不是Handler的技术多牛逼,是主线程用了Handler来通信,你是用别的方法通信有可能会影响主线程Looper的正常工作。 + + + +# Binder + +## Binder原理(一)学习Binder前必须要了解的知识点 + +Binder原理是掌握系统底层原理的基石,也是进阶高级工程师的必备知识点,这篇文章不会过多介绍Binder原理,而是讲解学习Binder前需要的掌握的知识点。 + +### Linux和Android的IPC机制种类 + +IPC全名为inter-Process Communication,含义为进程间通信,是指两个进程之间进行数据交换的过程。在Android和Linux中都有各自的IPC机制,这里分别来介绍下。 + +#### Linux中的IPC机制种类 + +Linux中提供了很多进程间通信机制,主要有管道(pipe)、信号(sinal)、信号量(semophore)、消息队列(Message)、共享内存(Share Memory)、套接字(Socket)等。 + +**管道** +管道是Linux由Unix那里继承过来的进程间的通信机制,它是Unix早期的一个重要通信机制。管道的主要思想是,在内存中创建一个共享文件,从而使通信双方利用这个共享文件来传递信息。这个共享文件比较特殊,它不属于文件系统并且只存在于内存中。另外还有一点,管道采用的是半双工通信方式的,数据只能在一个方向上流动。 +简单的模型如下所示。 +![nbXJ2T.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMC8yNC8xNmRmOTZhOWE3YzEwMTM5?x-oss-process=image/format,png) + +**信号** +信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,进程不必通过任何操作来等待信号的到达。信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件。信号不适用于信息交换,比较适用于进程中断控制。 +**信号量** +信号量是一个计数器,用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。主要作为进程间以及同一进程内不同线程之间的同步手段。 +**消息队列** +消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识,并且允许一个或多个进程向它写入与读取消息。信息会复制两次,因此对于频繁或者信息量大的通信不宜使用消息队列。 + +**共享内存** +多个进程可以直接读写的一块内存空间,是针对其他通信机制运行效率较低而设计的。 +为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大的提高效率。 + +**套接字** +套接字是更为基础的进程间通信机制,与其他方式不同的是,套接字可用于不同机器之间的进程间通信。 + +#### Android中的IPC机制 + +Android系统是基于Linux内核的,在Linux内核基础上,又拓展出了一些IPC机制。Android系统除了支持套接字,还支持序列化、Messenger、AIDL、Bundle、文件共享、ContentProvider、Binder等。Binder会在后面介绍,先来了解前面的IPC机制。 +**序列化** +序列化指的是Serializable/Parcelable,Serializable是Java提供的一个序列化接口,是一个空接口,为对象提供标准的序列化和反序列化操作。Parcelable接口是Android中的序列化方式,更适合在Android平台上使用,用起来比较麻烦,效率很高。 +**Messenger** +Messenger在Android应用开发中的使用频率不高,可以在不同进程中传递Message对象,在Message中加入我们想要传的数据就可以在进程间的进行数据传递了。Messenger是一种轻量级的IPC方案并对AIDL进行了封装。 + +**AIDL** +AIDL全名为Android interface definition Language,即Android接口定义语言。Messenger是以串行的方式来处理客户端发来的信息,如果有大量的消息发到服务端,服务端仍然一个一个的处理再响应客户端显然是不合适的。另外还有一点,Messenger用来进程间进行数据传递但是却不能满足跨进程的方法调用,这个时候就需要使用AIDL了。 + +**Bundle** +Bundle实现了Parcelable接口,所以它可以方便的在不同的进程间传输。Acitivity、Service、Receiver都是在Intent中通过Bundle来进行数据传递。 + +**文件共享** +两个进程通过读写同一个文件来进行数据共享,共享的文件可以是文本、XML、JOSN。文件共享适用于对数据同步要求不高的进程间通信。 + +**ContentProvider** +ContentProvider为存储和获取数据了提供统一的接口,它可以在不同的应用程序之间共享数据,本身就是适合进程间通信的。ContentProvider底层实现也是Binder,但是使用起来比AIDL要容易许多。系统中很多操作都采用了ContentProvider,例如通讯录,音视频等,这些操作本身就是跨进程进行通信。 + +### Linux和Binder的IPC通信原理 + +在讲到Linux的进程通信原理之前,我们需要先了解Liunx中的几个概念。 + +![njr0qU.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMC8yNC8xNmRmOTZhOTkzODU1MzY2?x-oss-process=image/format,png) + +**内核空间和用户空间** +当我们接触到Liunx时,免不了听到两个词,User space(用户空间)和 Kernel space(内核空间),那么它们的含义是什么呢? +为了保护用户进程不能直接操作内核,保证内核的安全,操作系统从逻辑上将虚拟空间划分为用户空间和内核空间。Linux 操作系统将最高的1GB字节供内核使用,称为内核空间,较低的3GB 字节供各进程使用,称为用户空间。 + +内核空间是Linux内核的运行空间,用户空间是用户程序的运行空间。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不会受到影响。内核空间的数据是可以进程间共享的,而用户空间则不可以。比如在上图进程A的用户空间是不能和进程B的用户空间共享的。 + +**进程隔离** +进程隔离指的是,一个进程不能直接操作或者访问另一个进程。也就是进程A不可以直接访问进程B的数据。 + +**系统调用** +用户空间需要访问内核空间,就需要借助系统调用来实现。系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。 + +进程A和进程B的用户空间可以通过如下系统函数和内核空间进行交互。 + +- copy_from_user:将用户空间的数据拷贝到内核空间。 +- copy_to_user:将内核空间的数据拷贝到用户空间。 + +**内存映射** +由于应用程序不能直接操作设备硬件地址,所以操作系统提供了一种机制:内存映射,把设备地址映射到进程虚拟内存区。 +举个例子,如果用户空间需要读取磁盘的文件,如果不采用内存映射,那么就需要在内核空间建立一个页缓存,页缓存去拷贝磁盘上的文件,然后用户空间拷贝页缓存的文件,这就需要两次拷贝。 +采用内存映射,如下图所示。 + +![nzlnaV.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMC8yNC8xNmRmOTZhOTZjOWFjOGY5?x-oss-process=image/format,png) +由于新建了虚拟内存区域,那么磁盘文件和虚拟内存区域就可以直接映射,少了一次拷贝。 + +内存映射全名为Memory Map,在Linux中通过系统调用函数mmap来实现内存映射。将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间,反之亦然。内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。 + +#### Linux的IPC通信原理 + +了解Liunx中的几个概念后,就可以学习Linux的IPC通信原理了,如下图所示。 +![nzaypq.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMC8yNC8xNmRmOTZhOTkzMDVhNTZm?x-oss-process=image/format,png) +内核程序在内核空间分配内存并开辟一块内核缓存区,发送进程通过copy_from_user函数将数据拷贝到到内核空间的缓冲区中。同样的,接收进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy_to_user() 函数将数据从内核缓存区拷贝到接收进程。这样数据发送进程和数据接收进程完成了一次数据传输,也就是一次进程间通信。 + +Linux的IPC通信原理有两个问题: + +1. 一次数据传递需要经历:用户空间 --> 内核缓存区 --> 用户空间,需要2次数据拷贝,这样效率不高。 +2. 接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用API接收消息头来获取消息体的大小,浪费了空间或者时间。 + +#### Binder的通信原理 + +Binder是基于开源的OpenBinder实现的,OpenBinder最早并不是由Google公司开发的,而是Be Inc公司开发的,接着由Palm, Inc.公司负责开发。后来OpenBinder的作者Dianne Hackborn加入了Google公司,并负责Android平台的开发工作,顺便把这项技术也带进了Android。 + +Binder是基于内存映射来实现的,在前面我们知道内存映射通常是用在有物理介质的文件系统上的,Binder没有物理介质,它使用内存映射是为了跨进程传递数据。 +![nzNJUA.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMC8yNC8xNmRmOTZhOTc5NTU4NjEy?x-oss-process=image/format,png) + +Binder通信的步骤如下所示。 +1.Binder驱动在内核空间创建一个数据接收缓存区。 +2.在内核空间开辟一块内核缓存区,建立内核缓存区和数据接收缓存区之间的映射关系,以及数据接收缓存区和接收进程用户空间地址的映射关系。 +3.发送方进程通过copy_from_user()函数将数据拷贝 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。 + +整个过程只使用了1次拷贝,不会因为不知道数据的大小而浪费空间或者时间,效率更高。 + +### 为什么要使用Binder + +Android是基于Linux内核的 ,Linux提供了很多IPC机制,而Android却自己设计了Binder来进行通信,主要是因为以下几点。 +**性能方面** +性能方面主要影响的因素是拷贝次数,管道、消息队列、Socket的拷贝次书都是两次,性能不是很好,共享内存不需要拷贝,性能最好,Binder的拷贝次书为1次,性能仅次于内存拷贝。 +**稳定性方面** +Binder是基于C/S架构的,这个架构通常采用两层结构,在技术上已经很成熟了,稳定性是没有问题的。共享内存没有分层,难以控制,并发同步访问临界资源时,可能还会产生死锁。从稳定性的角度讲,Binder是优于共享内存的。 +**安全方面** +Android是一个开源的系统,并且拥有开放性的平台,市场上应用来源很广,因此安全性对于Android 平台而言极其重要。 +传统的IPC接收方无法获得对方可靠的进程用户ID/进程ID(UID/PID),无法鉴别对方身份。Android 为每个安装好的APP分配了自己的UID,通过进程的UID来鉴别进程身份。另外,Android系统中的Server端会判断UID/PID是否满足访问权限,而对外只暴露Client端,加强了系统的安全性。 +**语言方面** +Linux是基于C语言,C语言是面向过程的,Android应用层和Java Framework是基于Java语言,Java语言是面向对象的。Binder本身符合面向对象的思想,因此作为Android的通信机制更合适不过。 + +从这四方面来看,Linux提供的大部分IPC机制根本无法和Binder相比较,而共享内存只在性能方面优于Binder,其他方面都劣于Binder,这些就是为什么Android要使用Binder来进行进程间通信,当然系统中并不是所有的进程通信都是采用了Binder,而是根据场景选择最合适的,比如Zygote进程与AMS通信使用的是Socket,Kill Process采用的是信号。 + +### 为什么要学习Binder? + +Binder机制在Android中的地位举足轻重,我们需要掌握的很多原理都和Binder有关: + +1. 系统中的各个进程是如何通信的? +2. Android系统启动过程 +3. AMS、PMS的原理 +4. 四大组件的原理,比如Activity是如何启动的? +5. 插件化原理 +6. 系统服务的Client端和Server端是如何通信的?(比如MediaPlayer和MeidaPlayerService) + +上面只是列了一小部分,简单来说说,比如系统在启动时,SystemServer进程启动后会创建Binder线程池,目的是通过Binder,使得在SystemServer进程中的服务可以和其他进程进行通信了。再比如我们常说的AMS、PMS都是基于Binder来实现的,拿PMS来说,PMS运行在SystemServer进程,如果它想要和DefaultContainerService通信(是用于检查和复制可移动文件的系统服务),就需要通过Binder,因为DefaultContainerService运行在com.android.defcontainer进程。 +还有一个比较常见的C/S架构间通信的问题,Client端的MediaPlayer和Server端的MeidaPlayerService不是运行在一个进程中的,同样需要Binder来实现通信。 + +可以说Binder机制是掌握系统底层原理的基石。根据Android系统的分层,Binder机制主要分为以下几个部分。 + +![n5i5PP.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMC8yNC8xNmRmOTZhOTgwOTk0NjVj?x-oss-process=image/format,png) + +上图并没有给出Binder机制的具体的细节,而是先给出了一个概念,根据系统的Android系统的分层,我将Binder机制分为了Java Binder、Native Binder、Kernel Binder,实际上Binder的内容非常多,完全可以写一本来介绍,但是对于应用开发来说,并不需要掌握那么多的知识点,因此本系列主要会讲解Java Binder和Native Binder。 + +## Binder原理(二)ServiceManager中的Binder机制 + +在上一部分中,我们了解了学习Binder前必须要了解的知识点,其中有一点就是Binder机制的三个部分:Java Binder、Native Binder、Kernel Binder,其中Java Binder和Native Binder都是应用开发需要掌握的。Java Binder是需要借助Native Binder来工作的,因此需要先了解Native Binder,Native Binder架构的原型就是基于Binder通信的C/S架构,因此我们先从它开始入手。源码是基于Android 9.0。 + +### 基于Binder通信的C/S架构 + +在Android系统中,Binder进程间的通信的使用是很普遍的,在Android进阶三部曲第一部的最后一章,我讲解了MediaPlayer框架,这个框架基于C/S架构,并采用Binder来进行进程间通信,如下图所示。 +![uZgTgJ.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9zMi5heDF4LmNvbS8yMDE5LzA5LzI1L3VaZ1RnSi5wbmc?x-oss-process=image/format,png) + +从图中可以看出,除了常规C/S架构的Client端和Server端,还包括了ServiceManager,它用于管理系统中的服务。 +首先Server进程会注册一些Service到ServiceManager中,Client要使用某个Service,则需要先到ServiceManager查询Service的相关信息,然后根据Service的相关信息与Service所在的Server进程建立通信通路,这样Client就可以使用Service了。 + +### MediaServer的main函数 + +Client、Server、ServiceManager三者的交互都是基于Binder通信的,那么任意两者的交互都可以说明Binder的通信的原理,可以说Native Binder的原理的核心就是ServiceManager的原理,为了更好的了解ServiceManager,这里拿MediaPlayer框架来举例,它也是学习多媒体时必须要掌握的知识点。 + +MediaPlayer框架的简单框架图如下所示。 +![uMegMj.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9zMi5heDF4LmNvbS8yMDE5LzA5LzI3L3VNZWdNai5wbmc?x-oss-process=image/format,png) +可以看到,MediaPlayer和MediaPlayerService是通过Binder来进行通信的,MediaPlayer是Client端,MediaPlayerService是Server端,MediaPlayerService是系统多媒体服务的一种,系统多媒体服务是由一个叫做MediaServer的服务进程提供的,它是一个可执行程序,在Android系统启动时,MediaServer也被启动,它的入口函数如下所示。 +**frameworks/av/media/mediaserver/main_mediaserver.cpp** + +```cpp +int main(int argc __unused, char **argv __unused) +{ + signal(SIGPIPE, SIG_IGN); + //获取ProcessState实例 + sp proc(ProcessState::self());//1 + sp sm(defaultServiceManager());//2 + ALOGI("ServiceManager: %p", sm.get()); + InitializeIcuOrDie(); + //注册MediaPlayerService + MediaPlayerService::instantiate();//3 + ResourceManagerService::instantiate(); + registerExtensions(); + //启动Binder线程池 + ProcessState::self()->startThreadPool(); + //当前线程加入到线程池 + IPCThreadState::self()->joinThreadPool(); +} +``` + +注释1处用于获取ProcessState实例,在这一过程中会打开/dev/binder设备,并使用mmap为Binder驱动分配一个虚拟地址空间用来接收数据。 +注释2处用来得到一个IServiceManager,通过这个IServiceManager,其他进程就可以和当前的ServiceManager进行交互,这里就用到了Binder通信。 +注释3处用来注册MediaPlayerService。 +除了注释3处的知识点在下一篇文章进行介绍,注释1和注释2处的内容,本篇文章会分别来进行介绍,先看ProcessState实例。 + +### 每个进程唯一的ProcessState + +ProcessState从名称就可以看出来,用于代表进程的状态,先来查看上一小节的ProcessState的self函数。 +**frameworks/native/libs/binder/ProcessState.cpp** + +```cpp +sp ProcessState::self() +{ + Mutex::Autolock _l(gProcessMutex); + if (gProcess != NULL) { + return gProcess; + } + gProcess = new ProcessState("/dev/binder");//1 + return gProcess; +} +``` + +这里采用了单例模式,确保每个进程只有一个ProcessState实例。注释1处用于创建一个ProcessState实例,参数为/dev/binder。接着来查看ProcessState的构造函数,代码如下所示。 +**frameworks/native/libs/binder/ProcessState.cpp** + +```cpp +ProcessState::ProcessState(const char *driver) + : mDriverName(String8(driver)) + , mDriverFD(open_driver(driver))//1 + , mVMStart(MAP_FAILED) + , mThreadCountLock(PTHREAD_MUTEX_INITIALIZER) + , mThreadCountDecrement(PTHREAD_COND_INITIALIZER) + , mExecutingThreadsCount(0) + , mMaxThreads(DEFAULT_MAX_BINDER_THREADS) + , mStarvationStartTimeMs(0) + , mManagesContexts(false) + , mBinderContextCheckFunc(NULL) + , mBinderContextUserData(NULL) + , mThreadPoolStarted(false) + , mThreadPoolSeq(1) +{ + if (mDriverFD >= 0) { + mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);//2 + if (mVMStart == MAP_FAILED) { + // *sigh* + ALOGE("Using %s failed: unable to mmap transaction memory.\n", mDriverName.c_str()); + close(mDriverFD); + mDriverFD = -1; + mDriverName.clear(); + } + } + LOG_ALWAYS_FATAL_IF(mDriverFD < 0, "Binder driver could not be opened. Terminating."); +} +``` + +ProcessState的构造函数中调用了很多函数,需要注意的是注释1处,它用来打开/dev/binder设备。 +注释2处的mmap函数,它会在内核虚拟地址空间中申请一块与用户虚拟内存相同大小的内存,然后再申请物理内存,将同一块物理内存分别映射到内核虚拟地址空间和用户虚拟内存空间,实现了内核虚拟地址空间和用户虚拟内存空间的数据同步操作,也就是内存映射。 +mmap函数用于对Binder设备进行内存映射,除了它还有open、ioctl函数,来看看它们做了什么。 +注释1处的open_driver函数的代码如下所示。 +**frameworks/native/libs/binder/ProcessState.cpp** + +```cpp +static int open_driver(const char *driver) +{ + int fd = open(driver, O_RDWR | O_CLOEXEC);//1 + if (fd >= 0) { + ... + size_t maxThreads = DEFAULT_MAX_BINDER_THREADS; + result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);//2 + if (result == -1) { + ALOGE("Binder ioctl to set max threads failed: %s", strerror(errno)); + } + } else { + ALOGW("Opening '%s' failed: %s\n", driver, strerror(errno)); + } + return fd; +} +``` + +注释1处用于打开/dev/binder设备并返回文件操作符fd,这样就可以操作内核的Binder驱动了。注释2处的ioctl函数的作用就是和Binder设备进行参数的传递,这里的ioctl函数用于设定binder支持的最大线程数为15(maxThreads的值为15)。最终open_driver函数返回文件操作符fd。 + +ProcessState就分析倒这里,总的来说它做了以下几个重要的事: +1.打开/dev/binder设备并设定Binder最大的支持线程数。 +2.通过mmap为binder分配一块虚拟地址空间,达到内存映射的目的。 + +### ServiceManager中的Binder机制 + +回到第一小节的MediaServer的入口函数,在注释2处调用了defaultServiceManager函数。 +**frameworks/native/libs/binder/IServiceManager.cpp** + +```cpp +sp defaultServiceManager() +{ + if (gDefaultServiceManager != NULL) return gDefaultServiceManager; + + { + AutoMutex _l(gDefaultServiceManagerLock); + while (gDefaultServiceManager == NULL) { + gDefaultServiceManager = interface_cast( + ProcessState::self()->getContextObject(NULL));//1 + if (gDefaultServiceManager == NULL) + sleep(1); + } + } + + return gDefaultServiceManager; +} +``` + +从IServiceManager所在的文件路径就可以知道,ServiceManager中不仅仅使用了Binder通信,它自身也是属于Binder体系的。defaultServiceManager中同样使用了单例,注释1处的interface_cast函数生成了gDefaultServiceManager,其内部调用了ProcessState的getContextObject函数,代码如下所示。 +**frameworks/native/libs/binder/ProcessState.cpp** + +```cpp +sp ProcessState::getContextObject(const sp& /*caller*/) +{ + return getStrongProxyForHandle(0); +} + +sp ProcessState::getStrongProxyForHandle(int32_t handle) +{ + sp result; + AutoMutex _l(mLock); + handle_entry* e = lookupHandleLocked(handle);//1 + if (e != NULL) { + IBinder* b = e->binder; + if (b == NULL || !e->refs->attemptIncWeak(this)) { + if (handle == 0) { + Parcel data; + status_t status = IPCThreadState::self()->transact( + 0, IBinder::PING_TRANSACTION, data, NULL, 0); + if (status == DEAD_OBJECT) + return NULL; + } + b = BpBinder::create(handle);//2 + e->binder = b; + if (b) e->refs = b->getWeakRefs(); + result = b; + } else { + result.force_set(b); + e->refs->decWeak(this); + } + } + + return result; +} +``` + +getContextObject函数中直接调用了getStrongProxyForHandle函数,注意它的参数的值为0,那么handle的值就为0,handle是一个资源标识。注释1处查询这个资源标识对应的资源(handle_entry)是否存在,如果不存在就会在注释2处新建BpBinder,并在注释3处赋值给 handle_entry的binder。最终返回的result的值为BpBinder。 + +#### BpBinder和BBinder + +说到BpBinder,不得不提到BBinder,它们是Binder通信的“双子星”,都继承了IBinder。BpBinder是Client端与Server交互的代理类,而BBinder则代表了Server端。BpBinder和BBinder是一一对应的,BpBinder会通过handle来找到对应的BBinder。 +我们知道在ServiceManager中创建了BpBinder,通过handle(值为0)可以找到对应的BBinder。 +![usIeXQ.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9zMi5heDF4LmNvbS8yMDE5LzEwLzA1L3VzSWVYUS5wbmc?x-oss-process=image/format,png) + +分析完了ProcessState的getContextObject函数,回到interface_cast函数: + +```cpp + gDefaultServiceManager = interface_cast( + ProcessState::self()->getContextObject(NULL)); +``` + +interface_cast具体实现如下所示。 +**frameworks/native/libs/binder/include/binder/IInterface.h** + +```cpp +template +inline sp interface_cast(const sp& obj) +{ + return INTERFACE::asInterface(obj); +} +``` + +当前的场景中,INTERFACE的值为IServiceManager,那么替换后代码如下所示。 + +```cpp +inline sp interface_cast(const sp& obj){ return IServiceManager::asInterface(obj);} +``` + +我们接着来分析IServiceManager。 + +#### 解密IServiceManager + +BpBinder和BBinder负责Binder的通信,而IServiceManager用于处理ServiceManager的业务,IServiceManager是C++代码,因此它的定义在IServiceManager.h中。 +**frameworks/native/libs/binder/include/binder/IServiceManager.h** + +```cpp +class IServiceManager : public IInterface +{ +public: + DECLARE_META_INTERFACE(ServiceManager)//1 + ... + //一些操作Service的函数 + virtual sp getService( const String16& name) const = 0; + virtual sp checkService( const String16& name) const = 0; + virtual status_t addService(const String16& name, const sp& service, + bool allowIsolated = false, + int dumpsysFlags = DUMP_FLAG_PRIORITY_DEFAULT) = 0; + virtual Vector listServices(int dumpsysFlags = DUMP_FLAG_PRIORITY_ALL) = 0; + enum { + GET_SERVICE_TRANSACTION = IBinder::FIRST_CALL_TRANSACTION, + CHECK_SERVICE_TRANSACTION, + ADD_SERVICE_TRANSACTION, + LIST_SERVICES_TRANSACTION, + }; +}; +``` + +可以看到IServiceManager继承了IInterface,其内部定义了一些常量和一些操作Service的函数,在注释1处调用了DECLARE_META_INTERFACE宏,它的定义在IInterface.h中。 +**frameworks/native/libs/binder/include/binder/IInterface.h** + +```cpp +#define DECLARE_META_INTERFACE(INTERFACE) \ + static const ::android::String16 descriptor; \ + static ::android::sp asInterface( \ + const ::android::sp<::android::IBinder>& obj); \ + virtual const ::android::String16& getInterfaceDescriptor() const; \ + I##INTERFACE(); \ + virtual ~I##INTERFACE(); +``` + +其中INTERFACE的值为ServiceManager,那么经过替换后的代码如下所示。 + +```cpp + static const ::android::String16 descriptor; + //定义asInterface函数 + static ::android::sp asInterface( + const ::android::sp<::android::IBinder>& obj); + virtual const ::android::String16& getInterfaceDescriptor() const; + //定义IServiceManager构造函数 + IServiceManager(); + //定义IServiceManager析构函数 + virtual ~IServiceManager(); +``` + +从DECLARE_META_INTERFACE宏的名称和上面的代码中,可以发现它主要声明了一些函数和一个变量。那么这些函数和变量的实现在哪呢?答案还是在IInterface.h中,叫做IMPLEMENT_META_INTERFACE宏,代码如下所示/ +**frameworks/native/libs/binder/include/binder/IInterface.h** + +```cpp +#define IMPLEMENT_META_INTERFACE(INTERFACE, NAME) \ + const ::android::String16 I##INTERFACE::descriptor(NAME); \ + const ::android::String16& \ + I##INTERFACE::getInterfaceDescriptor() const { \ + return I##INTERFACE::descriptor; \ + } \ + ::android::sp I##INTERFACE::asInterface( \ + const ::android::sp<::android::IBinder>& obj) \ + { \ + ::android::sp intr; \ + if (obj != NULL) { \ + intr = static_cast( \ + obj->queryLocalInterface( \ + I##INTERFACE::descriptor).get()); \ + if (intr == NULL) { \ + intr = new Bp##INTERFACE(obj); \ + } \ + } \ + return intr; \ + } \ + I##INTERFACE::I##INTERFACE() { } \ + I##INTERFACE::~I##INTERFACE() { } \ +``` + +DECLARE_META_INTERFACE宏和IMPLEMENT_META_INTERFACE宏是配合使用的,很多系统服务都使用了它们,IServiceManager使用IMPLEMENT_META_INTERFACE宏只有一行代码,如下所示。 +**frameworks/native/libs/binder/IServiceManager.cpp** + +```cpp +IMPLEMENT_META_INTERFACE(ServiceManager, "android.os.IServiceManager"); +``` + +IMPLEMENT_META_INTERFACE宏的INTERFACE值为ServiceManager,NAME值为"android.os.IServiceManager",进行替换后的代码如下所示。 + +```cpp +const ::android::String16 IServiceManager::descriptor("android.os.IServiceManager"); +const ::android::String16& + IServiceManager::getInterfaceDescriptor() const { + return IServiceManager::descriptor; +} + //实现了asInterface函数 +::android::sp IServiceManager::asInterface( + const ::android::sp<::android::IBinder>& obj) +{ + ::android::sp intr; + if (obj != NULL) { + intr = static_cast( + obj->queryLocalInterface( + IServiceManager::descriptor).get()); + if (intr == NULL) { + intr = new BpServiceManager(obj);//1 + } + } + return intr; +} +IServiceManager::IServiceManager() { } +IServiceManager::~IServiceManager() { } +``` + +关键的点就在于注释1处,新建了一个BpServiceManager,传入的参数obj的值为BpBinder。看到这里,我们也就明白了,asInterface函数就是用BpBinder为参数创建了BpServiceManager,从而推断出interface_cast函数创建了BpServiceManager,再往上推断,IServiceManager的defaultServiceManager函数返回的就是BpServiceManager。 +BpServiceManager有什么作用呢,先从BpServiceManager的构造函数看起。 +**frameworks/native/libs/binder/IServiceManager.cpp** + +```cpp +class BpServiceManager : public BpInterface +{ +public: + explicit BpServiceManager(const sp& impl) + : BpInterface(impl) + { + } +... +} +``` + +impl的值其实就是BpBinder,BpServiceManager的构造函数调用了基类BpInterface的构造函数。 +**frameworks/native/libs/binder/include/binder/IInterface.h** + +```cpp +template +class BpInterface : public INTERFACE, public BpRefBase +{ +... +}; +``` + +BpInterface继承了BpRefBase,BpRefBase的实现如下所示。 +**frameworks/native/libs/binder/Binder.cpp** + +```cpp +BpRefBase::BpRefBase(const sp& o) + : mRemote(o.get()), mRefs(NULL), mState(0) +{ + extendObjectLifetime(OBJECT_LIFETIME_WEAK); + + if (mRemote) { + mRemote->incStrong(this); + mRefs = mRemote->createWeak(this); + } +} +``` + +mRemote是一个IBinder* 指针,它最终的指向为BpBinder,也就是说BpServiceManager的mRemote指向了BpBinder。那么BpServiceManager的作用也就知道了,就是它实现了IServiceManager,并且通过BpBinder来实现通信。 + +#### IServiceManager家族 + +可能上面讲的会让你有些头晕,这是因为对各个类的关系不大明确,通过下图也许你就会豁然开朗。 +![ufWhRI.png](https://s2.ax1x.com/2019/10/08/ufWhRI.png) + +1.BpBinder和BBinder都和通信有关,它们都继承自IBinder。 +2.BpServiceManager派生自IServiceManager,它们都和业务有关。 +3.BpRefBase包含了mRemote,通过不断的派生,BpServiceManager也同样包含mRemote,它指向了BpBinder,通过BpBinder来实现通信。 + +### 小结 + +本篇我们学到了Binder通信的C/S架构,也知道了Native Binder的原理的核心其实就是ServiceManager的原理,为了讲解ServiceManager的原理,我们需要一个框架来举例,那就是MediaPlayer框架。在讲解MediaServer的入口函数时,我们遇到了三个问题,其中前两个问题相关的知识点ProcessState和IServiceManager都讲解到了,下一篇文章会讲解第三个问题,MediaPlayerService是如何注册的。 + +## Binder原理(三)系统服务的注册过程 + +在上一部分中,我们学习了ServiceManager中的Binder机制,有一个问题由于篇幅问题没有讲完,那就是MediaPlayerService是如何注册的。通过了解MediaPlayerService是如何注册的,可以得知系统服务的注册过程。 + +### 从调用链角度说明MediaPlayerService是如何注册的 + +我们先来看MediaServer的入口函数,代码如下所示。 +**frameworks/av/media/mediaserver/main_mediaserver.cpp** + +```cpp +int main(int argc __unused, char **argv __unused) +{ + signal(SIGPIPE, SIG_IGN); + //获取ProcessState实例 + sp proc(ProcessState::self()); + sp sm(defaultServiceManager()); + ALOGI("ServiceManager: %p", sm.get()); + InitializeIcuOrDie(); + //注册MediaPlayerService + MediaPlayerService::instantiate();//1 + ResourceManagerService::instantiate(); + registerExtensions(); + //启动Binder线程池 + ProcessState::self()->startThreadPool(); + //当前线程加入到线程池 + IPCThreadState::self()->joinThreadPool(); +} +``` + +这段代码中的很多内容都在上一篇文章介绍过了,接着分析注释1处的代码。 + +**frameworks/av/media/libmediaplayerservice/MediaPlayerService.cpp** + +```cpp +void MediaPlayerService::instantiate() { + defaultServiceManager()->addService( + String16("media.player"), new MediaPlayerService,()); +} +``` + +defaultServiceManager返回的是BpServiceManager,不清楚的看[Android Binder原理(二)ServiceManager中的Binder机制][1]这篇文章。参数是一个字符串和MediaPlayerService,看起来像是Key/Value的形式来完成注册,接着看addService函数。 + +**frameworks/native/libs/binder/IServiceManager.cpp** + +```cpp +virtual status_t addService(const String16& name, const sp& service, + bool allowIsolated, int dumpsysPriority) { + Parcel data, reply;//数据包 + data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor()); + data.writeString16(name); //name值为"media.player" + data.writeStrongBinder(service); //service值为MediaPlayerService + data.writeInt32(allowIsolated ? 1 : 0); + data.writeInt32(dumpsysPriority); + status_t err = remote()->transact(ADD_SERVICE_TRANSACTION, data, &reply);//1 + return err == NO_ERROR ? reply.readExceptionCode() : err; + } +``` + +data是一个数据包,后面会不断的将数据写入到data中, 注释1处的remote()指的是mRemote,也就是BpBinder。addService函数的作用就是将请求数据打包成data,然后传给BpBinder的transact函数,代码如下所示。 +**frameworks/native/libs/binder/BpBinder.cpp** + +```cpp +status_t BpBinder::transact( + uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) +{ + if (mAlive) { + status_t status = IPCThreadState::self()->transact( + mHandle, code, data, reply, flags); + if (status == DEAD_OBJECT) mAlive = 0; + return status; + } + + return DEAD_OBJECT; +} +``` + +BpBinder将逻辑处理交给IPCThreadState,先来看IPCThreadState::self()干了什么? +**frameworks/native/libs/binder/IPCThreadState.cpp** + +```cpp +IPCThreadState* IPCThreadState::self() +{ + //首次进来gHaveTLS的值为false + if (gHaveTLS) { +restart: + const pthread_key_t k = gTLS;//1 + IPCThreadState* st = (IPCThreadState*)pthread_getspecific(k);//2 + if (st) return st; + return new IPCThreadState;//3 + } + ... + pthread_mutex_unlock(&gTLSMutex); + goto restart; +} +``` + +注释1处的TLS的全称为Thread local storage,指的是线程本地存储空间,在每个线程中都有TLS,并且线程间不共享。注释2处用于获取TLS中的内容并赋值给IPCThreadState*指针。注释3处会新建一个IPCThreadState,这里可以得知IPCThreadState::self()实际上是为了创建IPCThreadState,它的构造函数如下所示。 +**frameworks/native/libs/binder/IPCThreadState.cpp** + +```cpp +IPCThreadState::IPCThreadState() + : mProcess(ProcessState::self()), + mStrictModePolicy(0), + mLastTransactionBinderFlags(0) +{ + pthread_setspecific(gTLS, this);//1 + clearCaller(); + mIn.setDataCapacity(256); + mOut.setDataCapacity(256); +} +``` + +注释1处的pthread_setspecific函数用于设置TLS,将IPCThreadState::self()获得的TLS和自身传进去。IPCThreadState中还包含mIn、一个mOut,其中mIn用来接收来自Binder驱动的数据,mOut用来存储发往Binder驱动的数据,它们默认大小都为256字节。 +知道了IPCThreadState的构造函数,再回来查看IPCThreadState的transact函数。 +**frameworks/native/libs/binder/IPCThreadState.cpp** + +```cpp +status_t IPCThreadState::transact(int32_t handle, + uint32_t code, const Parcel& data, + Parcel* reply, uint32_t flags) +{ + status_t err; + + flags |= TF_ACCEPT_FDS; + ... + err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);//1 + + if (err != NO_ERROR) { + if (reply) reply->setError(err); + return (mLastError = err); + } + + if ((flags & TF_ONE_WAY) == 0) { + ... + if (reply) { + err = waitForResponse(reply);//2 + } else { + Parcel fakeReply; + err = waitForResponse(&fakeReply); + } + ... + } else { + //不需要等待reply的分支 + err = waitForResponse(NULL, NULL); + } + + return err; +} +``` + +调用BpBinder的transact函数实际上就是调用IPCThreadState的transact函数。注释1处的writeTransactionData函数用于传输数据,其中第一个参数BC_TRANSACTION代表向Binder驱动发送命令协议,向Binder设备发送的命令协议都以BC_开头,而Binder驱动返回的命令协议以BR_开头。这个命令协议我们先记住,后面会再次提到他。 + +现在分别来分析注释1的writeTransactionData函数和注释2处的waitForResponse函数。 + +#### writeTransactionData函数分析 + +**frameworks/native/libs/binder/IPCThreadState.cpp** + +```cpp +status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags, + int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer) +{ + binder_transaction_data tr;//1 + + tr.target.ptr = 0; + tr.target.handle = handle;//2 + tr.code = code; //code=ADD_SERVICE_TRANSACTION + tr.flags = binderFlags; + tr.cookie = 0; + tr.sender_pid = 0; + tr.sender_euid = 0; + + const status_t err = data.errorCheck();//3 + if (err == NO_ERROR) { + tr.data_size = data.ipcDataSize(); + tr.data.ptr.buffer = data.ipcData(); + tr.offsets_size = data.ipcObjectsCount()*sizeof(binder_size_t); + tr.data.ptr.offsets = data.ipcObjects(); + } else if (statusBuffer) { + tr.flags |= TF_STATUS_CODE; + *statusBuffer = err; + tr.data_size = sizeof(status_t); + tr.data.ptr.buffer = reinterpret_cast(statusBuffer); + tr.offsets_size = 0; + tr.data.ptr.offsets = 0; + } else { + return (mLastError = err); + } + + mOut.writeInt32(cmd); //cmd=BC_TRANSACTION + mOut.write(&tr, sizeof(tr)); + + return NO_ERROR; +} +``` + +注释1处的binder_transaction_data结构体(tr结构体)是向Binder驱动通信的数据结构,注释2处将handle传递给target的handle,用于标识目标,这里的handle的值为0,代表了ServiceManager。 +注释3处对数据data进行错误检查,如果没有错误就将数据赋值给对应的tr结构体。最后会将BC_TRANSACTION和tr结构体写入到mOut中。 +上面代码调用链的时序图如下所示。 + +![](https://upload-images.jianshu.io/upload_images/11474088-0e4cec13a8f7c169.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +#### waitForResponse函数分析 + +接着回过头来查看waitForResponse函数做了什么,waitForResponse函数中的case语句很多,这里截取部分代码。 +**frameworks/native/libs/binder/IPCThreadState.cpp** + +```cpp +status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult) +{ + uint32_t cmd; + int32_t err; + while (1) { + if ((err=talkWithDriver()) < NO_ERROR) break;//1 + err = mIn.errorCheck(); + if (err < NO_ERROR) break; + if (mIn.dataAvail() == 0) continue; + cmd = (uint32_t)mIn.readInt32(); + IF_LOG_COMMANDS() { + alog << "Processing waitForResponse Command: " + << getReturnString(cmd) << endl; + } + switch (cmd) { + case BR_TRANSACTION_COMPLETE: + if (!reply && !acquireResult) goto finish; + break; + + case BR_DEAD_REPLY: + err = DEAD_OBJECT; + goto finish; + ... + default: + //处理各种命令协议 + err = executeCommand(cmd); + if (err != NO_ERROR) goto finish; + break; + } +} +finish: + ... + return err; +} +``` + +注释1处的talkWithDriver函数的内部通过ioctl与Binder驱动进行通信,代码如下所示。 +**frameworks/native/libs/binder/IPCThreadState.cpp** + +```cpp +status_t IPCThreadState::talkWithDriver(bool doReceive) +{ + if (mProcess->mDriverFD <= 0) { + return -EBADF; + } + //和Binder驱动通信的结构体 + binder_write_read bwr; //1 + //mIn是否有可读的数据,接收的数据存储在mIn + const bool needRead = mIn.dataPosition() >= mIn.dataSize(); + const size_t outAvail = (!doReceive || needRead) ? mOut.dataSize() : 0; + bwr.write_size = outAvail; + bwr.write_buffer = (uintptr_t)mOut.data();//2 + //这时doReceive的值为true + if (doReceive && needRead) { + bwr.read_size = mIn.dataCapacity(); + bwr.read_buffer = (uintptr_t)mIn.data();//3 + } else { + bwr.read_size = 0; + bwr.read_buffer = 0; + } + ... + if ((bwr.write_size == 0) && (bwr.read_size == 0)) return NO_ERROR; + bwr.write_consumed = 0; + bwr.read_consumed = 0; + status_t err; + do { + IF_LOG_COMMANDS() { + alog << "About to read/write, write size = " << mOut.dataSize() << endl; + } +#if defined(__ANDROID__) + if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)//4 + err = NO_ERROR; + else + err = -errno; +#else + err = INVALID_OPERATION; +#endif + ... + } while (err == -EINTR); + ... + return err; +} +``` + +注释1处的 binder_write_read是和Binder驱动通信的结构体,在注释2和3处将mOut、mIn赋值给binder_write_read的相应字段,最终通过注释4处的ioctl函数和Binder驱动进行通信,这一部分涉及到Kernel Binder的内容 +了,就不再详细介绍了,只需要知道在Kernel Binder中会记录服务名和handle,用于后续的服务查询。 + +#### 小结 + +从调用链的角度来看,MediaPlayerService是如何注册的貌似并不复杂,因为这里只是简单的介绍了一个调用链分支,可以简单的总结为以下几个步骤: + +1. addService函数将数据打包发送给BpBinder来进行处理。 +2. BpBinder新建一个IPCThreadState对象,并将通信的任务交给IPCThreadState。 +3. IPCThreadState的writeTransactionData函数用于将命令协议和数据写入到mOut中。 +4. IPCThreadState的waitForResponse函数主要做了两件事,一件事是通过ioctl函数操作mOut和mIn来与Binder驱动进行数据交互,另一件事是处理各种命令协议。 + +### 从进程角度说明MediaPlayerService是如何注册的 + +实际上MediaPlayerService的注册还涉及到了进程,如下图所示。 + +![Ka0Dx0.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMS8xOS8xNmU4MTFjYmUxMzFiMTdi?x-oss-process=image/format,png) + +从图中看出是以C/S架构为基础,addService是在MediaPlayerService进行的,它是Client端,用于请求添加系统服务。而Server端则是指的是ServiceManager,用于完成系统服务的添加。 +Client端和Server端分别运行在两个进程中,通过向Binder来进行通信。更详细点描述,就是两端通过向Binder驱动发送命令协议来完成系统服务的添加。这其中命令协议非常多,过程也比较复杂,这里对命令协议进行了简化,只涉及到了四个命令协议,其中 +BC_TRANSACTION和BR_TRANSACTION过程是一个完整的事务,BC_REPLY和BR_REPLY是一个完整的事务。 +Client端和Server端向Binder驱动发送命令协议以BC开头,而Binder驱动向Client端和Server端返回的命令协议以BR_开头。 + +步骤如下所示: +1.Client端向Binder驱动发送BC_TRANSACTION命令。 +2.Binder驱动接收到请求后生成BR_TRANSACTION命令,唤醒Server端的线程后将BR_TRANSACTION命令发送给ServiceManager。 +3.Server端中的服务注册完成后,生成BC_REPLY命令发送给Binder驱动。 +4.Binder驱动生成BR_REPLY命令,唤醒Client端的线程后将BR_REPLY命令发送个Client端。 + +通过这些协议命令来驱动并完成系统服务的注册。 + +### 总结 + +本文分别从调用链角度和进程角度来讲解MediaPlayerService是如何注册的,间接的得出了服务是如何注册的 +。这两个角度都比较复杂,因此这里分别对这两个角度做了简化,作为应用开发,我们不需要注重太多的过程和细节,只需要了解大概的步骤即可。 + +## Binder原理(四)ServiceManager的启动过程 + +在上一部分中,我们以MediaPlayerService为例,讲解了系统服务是如何注册的(addService),既然有注册就势必要有获取,但是在了解获取服务前,我们最好先了解ServiceManager的启动过程,这样更有助于理解系统服务的注册和获取的过程。 + +另外还有一点需要说明的是,要想了解ServiceManager的启动过程,需要查看Kernel Binder部分的源码,这部分代码在内核源码中,AOSP源码是不包括内核源码的 + +### ServiceManager的入口函数 + +ServiceManager是init进程负责启动的,具体是在解析init.rc配置文件时启动的,init进程是在系统启动时启动的,因此ServiceManager亦是如此。 + +rc文件内部由Android初始化语言编写(Android Init Language)编写的脚本,它主要包含五种类型语句:Action、Commands、Services、Options和Import。 +在Android 7.0中对init.rc文件进行了拆分,每个服务一个rc文件。ServiceManager的启动脚本在servicemanager.rc中: +frameworks/native/cmds/servicemanager/servicemanager.rc + +```cpp +service servicemanager /system/bin/servicemanager + class core animation + user system //1 + group system readproc + critical //2 + onrestart restart healthd + onrestart restart zygote + onrestart restart audioserver + onrestart restart media + onrestart restart surfaceflinger + onrestart restart inputflinger + onrestart restart drm + onrestart restart cameraserver + onrestart restart keystore + onrestart restart gatekeeperd + writepid /dev/cpuset/system-background/tasks + shutdown critical +``` + +service用于通知init进程创建名为servicemanager的进程,这个servicemanager进程执行程序的路径为/system/bin/servicemanager。 +注释1的关键字user说明servicemanager是以用户system的身份运行的,注释2处的critical说明servicemanager是系统中的关键服务,关键服务是不会退出的,如果退出了,系统就会重启,当系统重启时就会启动用onrestart关键字修饰的进程,比如zygote、media、surfaceflinger等等。 + +servicemanager的入口函数在service_manager.c中: +**frameworks/native/cmds/servicemanager/service_manager.c** + +```cpp +int main(int argc, char** argv) +{ + struct binder_state *bs;//1 + union selinux_callback cb; + char *driver; + + if (argc > 1) { + driver = argv[1]; + } else { + driver = "/dev/binder"; + } + + bs = binder_open(driver, 128*1024);//2 + ... + if (binder_become_context_manager(bs)) {//3 + ALOGE("cannot become context manager (%s)\n", strerror(errno)); + return -1; + } + ... + if (getcon(&service_manager_context) != 0) { + ALOGE("SELinux: Failed to acquire service_manager context. Aborting.\n"); + abort(); + } + binder_loop(bs, svcmgr_handler);//4 + + return 0; +} +``` + +注释1处的binder_state结构体用来存储binder的三个信息: + +```cpp +struct binder_state +{ + int fd; //binder设备的文件描述符 + void *mapped; //binder设备文件映射到进程的地址空间 + size_t mapsize; //内存映射后,系统分配的地址空间的大小,默认为128KB +}; +``` + +main函数主要做了三件事: +1.注释2处调用binder_open函数用于打开binder设备文件,并申请128k字节大小的内存空间。 +2.注释3处调用binder_become_context_manager函数,将servicemanager注册成为Binder机制的上下文管理者。 +3.注释4处调用binder_loop函数,循环等待和处理client端发来的请求。 + +现在对这三件事分别进行讲解。 + +#### 打开binder设备 + +binder_open函数用于打开binder设备文件,并且将它映射到进程的地址空间,如下所示。 + +**frameworks/native/cmds/servicemanager/binder.c** + +```cpp +struct binder_state *binder_open(const char* driver, size_t mapsize) +{ + struct binder_state *bs; + struct binder_version vers; + + bs = malloc(sizeof(*bs)); + if (!bs) { + errno = ENOMEM; + return NULL; + } + + bs->fd = open(driver, O_RDWR | O_CLOEXEC);//1 + if (bs->fd < 0) { + fprintf(stderr,"binder: cannot open %s (%s)\n", + driver, strerror(errno)); + goto fail_open; + } + //获取Binder的version + if ((ioctl(bs->fd, BINDER_VERSION, &vers) == -1) || + (vers.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION)) {//2 + fprintf(stderr, + "binder: kernel driver version (%d) differs from user space version (%d)\n", + vers.protocol_version, BINDER_CURRENT_PROTOCOL_VERSION); + goto fail_open; + } + + bs->mapsize = mapsize; + bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);//3 + if (bs->mapped == MAP_FAILED) { + fprintf(stderr,"binder: cannot map device (%s)\n", + strerror(errno)); + goto fail_map; + } + return bs; + +fail_map: + close(bs->fd); +fail_open: + free(bs); + return NULL; +} +``` + +注释1处用于打开binder设备文件,后面会进行分析。 +注释2处的ioctl函数用于获取Binder的版本,如果获取不到或者内核空间和用户空间的binder不是同一个版本就会直接goto到fail_open标签,释放binder的内存空间。 +注释3处调用mmap函数进行内存映射,通俗来讲就是将binder设备文件映射到进程的地址空间,地址空间的大小为mapsize,也就是128K。映射完毕后会将地址空间的起始地址和大小保存在binder_state结构体中的mapped和mapsize变量中。 + +这里着重说一下open函数,它会调用Kernel Binder部分的binder_open函数,这部分源码位于内核源码中,这里展示的代码版本为goldfish3.4。 + +**用户态和内核态** +临时插入一个知识点:用户态和内核态 +Intel的X86架构的CPU提供了0到3四个特权级,数字越小,权限越高,Linux操作系统中主要采用了0和3两个特权级,分别对应的就是内核态与用户态。用户态的特权级别低,因此进程在用户态下不经过系统调用是无法主动访问到内核空间中的数据的,这样用户无法随意的进入所有进程共享的内核空间,起到了保护的作用。下面来介绍下什么是用户态和内核态。 +当一个进程在执行用户自己的代码时处于用户态,比如open函数,它运行在用户空间,当前的进程处于用户态。 +当一个进程因为系统调用进入内核代码中执行时就处于内核态,比如open函数通过系统调用(__open()函数),查找到了open函数在Kernel Binder对应的函数为binder_open,这时binder_open运行在内核空间,当前的进程由用户态切换到内核态。 + +**kernel/goldfish/drivers/staging/android/binder.c** + +```cpp +static int binder_open(struct inode *nodp, struct file *filp) +{ //代表Binder进程 + struct binder_proc *proc;//1 + binder_debug(BINDER_DEBUG_OPEN_CLOSE, "binder_open: %d:%d\n", + current->group_leader->pid, current->pid); + //分配内存空间 + proc = kzalloc(sizeof(*proc), GFP_KERNEL);//2 + if (proc == NULL) + return -ENOMEM; + get_task_struct(current); + proc->tsk = current; + INIT_LIST_HEAD(&proc->todo); + init_waitqueue_head(&proc->wait); + proc->default_priority = task_nice(current); + //binder同步锁 + binder_lock(__func__); + + binder_stats_created(BINDER_STAT_PROC); + hlist_add_head(&proc->proc_node, &binder_procs); + proc->pid = current->group_leader->pid; + INIT_LIST_HEAD(&proc->delivered_death); + filp->private_data = proc;//3 + //binder同步锁释放 + binder_unlock(__func__); + ... + return 0; +} +``` + +注释1处的binder_proc结构体代表binder进程,用于管理binder的各种信息。注释2处用于为binder_proc分配内存空间。注释3处将binder_proc赋值给file指针的private_data变量,后面的1.2小节会再次提到这个private_data变量。 + +#### 注册成为Binder机制的上下文管理者 + +binder_become_context_manager函数用于将servicemanager注册成为Binder机制的上下文管理者,这个管理者在整个系统只有一个,代码如下所示。 +**frameworks/native/cmds/servicemanager/binder.c** + +```cpp +int binder_become_context_manager(struct binder_state *bs) +{ + return ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, 0); +} +``` + +ioctl函数会调用Binder驱动的binder_ioctl函数,binder_ioctl函数代码比较多,这里截取BINDER_SET_CONTEXT_MGR的处理部分,代码如下所示。 +**kernel/goldfish/drivers/staging/android/binder.c** + +```cpp +static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) +{ + int ret; + struct binder_proc *proc = filp->private_data; //1 + struct binder_thread *thread; + unsigned int size = _IOC_SIZE(cmd); + void __user *ubuf = (void __user *)arg; + trace_binder_ioctl(cmd, arg); + + ret = wait_event_interruptible(binder_user_error_wait, binder_stop_on_user_error < 2); + if (ret) + goto err_unlocked; + + binder_lock(__func__); + thread = binder_get_thread(proc);//2 + if (thread == NULL) { + ret = -ENOMEM; + goto err; + } + + switch (cmd) { + ... + case BINDER_SET_CONTEXT_MGR: + if (binder_context_mgr_node != NULL) {//3 + printk(KERN_ERR "binder: BINDER_SET_CONTEXT_MGR already set\n"); + ret = -EBUSY; + goto err; + } + ret = security_binder_set_context_mgr(proc->tsk); + if (ret < 0) + goto err; + if (binder_context_mgr_uid != -1) {//4 + if (binder_context_mgr_uid != current->cred->euid) {//5 + printk(KERN_ERR "binder: BINDER_SET_" + "CONTEXT_MGR bad uid %d != %d\n", + current->cred->euid, + binder_context_mgr_uid); + ret = -EPERM; + goto err; + } + } else + binder_context_mgr_uid = current->cred->euid;//6 + binder_context_mgr_node = binder_new_node(proc, NULL, NULL);//7 + if (binder_context_mgr_node == NULL) { + ret = -ENOMEM; + goto err; + } + binder_context_mgr_node->local_weak_refs++; + binder_context_mgr_node->local_strong_refs++; + binder_context_mgr_node->has_strong_ref = 1; + binder_context_mgr_node->has_weak_ref = 1; + break; + ... +err_unlocked: + trace_binder_ioctl_done(ret); + return ret; +} +``` + +注释1处将file指针中的private_data变量赋值给binder_proc,这个private_data变量在binder_open函数中讲过,是一个binder_proc结构体。注释2处的binder_get_thread函数用于获取binder_thread,binder_thread结构体指的是binder线程,binder_get_thread函数内部会从传入的参数binder_proc中查找binder_thread,如果查询到直接返回,如果查询不到会创建一个新的binder_thread并返回。 +注释3处的全局变量binder_context_mgr_node代表的是Binder机制的上下文管理者对应的一个Binder对象,如果它不为NULL,说明此前自身已经被注册为Binder的上下文管理者了,Binder的上下文管理者是不能重复注册的,因此会goto到err标签。 +注释4处的全局变量binder_context_mgr_uid代表注册了Binder机制上下文管理者的进程的有效用户ID,如果它的值不为-1,说明此前已经有进程注册Binder的上下文管理者了,因此在注释5处判断当前进程的有效用户ID是否等于binder_context_mgr_uid,不等于就goto到err标签。 +如果不满足注释4的条件,说明此前没有进程注册Binder机制的上下文管理者,就会在注释6处将当前进程的有效用户ID赋值给全局变量binder_context_mgr_uid,另外还会在注释7处调用binder_new_node函数创建一个Binder对象并赋值给全局变量binder_context_mgr_node。 + +#### 循环等待和处理client端发来的请求 + +servicemanager成功注册成为Binder机制的上下文管理者后,servicemanager就是Binder机制的“总管”了,它需要在系统运行期间处理client端的请求,由于client端的请求不确定何时发送,因此需要通过无限循环来实现,实现这一需求的函数就是binder_loop。 +**frameworks/native/cmds/servicemanager/binder.c** + +```cpp +void binder_loop(struct binder_state *bs, binder_handler func) +{ + int res; + struct binder_write_read bwr; + uint32_t readbuf[32]; + + bwr.write_size = 0; + bwr.write_consumed = 0; + bwr.write_buffer = 0; + + readbuf[0] = BC_ENTER_LOOPER; + binder_write(bs, readbuf, sizeof(uint32_t));//1 + + for (;;) { + bwr.read_size = sizeof(readbuf); + bwr.read_consumed = 0; + bwr.read_buffer = (uintptr_t) readbuf; + + res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);//2 + + if (res < 0) { + ALOGE("binder_loop: ioctl failed (%s)\n", strerror(errno)); + break; + } + + res = binder_parse(bs, 0, (uintptr_t) readbuf, bwr.read_consumed, func);//3 + if (res == 0) { + ALOGE("binder_loop: unexpected reply?!\n"); + break; + } + if (res < 0) { + ALOGE("binder_loop: io error %d %s\n", res, strerror(errno)); + break; + } + } +} +``` + +注释1处将BC_ENTER_LOOPER指令通过binder_write函数写入到Binder驱动中,这样当前线程(ServiceManager的主线程)就成为了一个Binder线程,这样就可以处理进程间的请求了。 +在无限循环中不断的调用注释2处的ioctl函数,它不断的使用BINDER_WRITE_READ指令查询Binder驱动中是否有新的请求,如果有就交给注释3处的binder_parse函数处理。如果没有,当前线程就会在Binder驱动中睡眠,等待新的进程间请求。 + +由于binder_write函数的调用链中涉及到了内核空间和用户空间的交互,因此这里着重讲解下。 + +**frameworks/native/cmds/servicemanager/binder.c** + +```cpp +int binder_write(struct binder_state *bs, void *data, size_t len) +{ + struct binder_write_read bwr;//1 + int res; + + bwr.write_size = len; + bwr.write_consumed = 0; + bwr.write_buffer = (uintptr_t) data;//2 + bwr.read_size = 0; + bwr.read_consumed = 0; + bwr.read_buffer = 0; + res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);//3 + if (res < 0) { + fprintf(stderr,"binder_write: ioctl failed (%s)\n", + strerror(errno)); + } + return res; +} +``` + +注释1处定义binder_write_read结构体,接下来的代码对bwr进行赋值,其中需要注意的是,注释2处的data的值为BC_ENTER_LOOPER。注释3处的ioctl函数将会bwr中的数据发送给binder驱动,我们已经知道了ioctl函数在Kernel Binder中对应的函数为binder_ioctl,此前分析过这个函数,这里截取BINDER_WRITE_READ命令处理部分。 + +**kernel/goldfish/drivers/staging/android/binder.c** + +```cpp +static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) +{ + ... + void __user *ubuf = (void __user *)arg; + ... + switch (cmd) { + case BINDER_WRITE_READ: { + struct binder_write_read bwr; + if (size != sizeof(struct binder_write_read)) { + ret = -EINVAL; + goto err; + } + if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {//1 + ret = -EFAULT; + goto err; + } + binder_debug(BINDER_DEBUG_READ_WRITE, + "binder: %d:%d write %ld at %08lx, read %ld at %08lx\n", + proc->pid, thread->pid, bwr.write_size, bwr.write_buffer, + bwr.read_size, bwr.read_buffer); + + if (bwr.write_size > 0) {//2 + ret = binder_thread_write(proc, thread, (void __user *)bwr.write_buffer, bwr.write_size, &bwr.write_consumed);//3 + trace_binder_write_done(ret); + if (ret < 0) { + bwr.read_consumed = 0; + if (copy_to_user(ubuf, &bwr, sizeof(bwr))) + ret = -EFAULT; + goto err; + } + } + ... + binder_debug(BINDER_DEBUG_READ_WRITE, + "binder: %d:%d wrote %ld of %ld, read return %ld of %ld\n", + proc->pid, thread->pid, bwr.write_consumed, bwr.write_size, + bwr.read_consumed, bwr.read_size); + if (copy_to_user(ubuf, &bwr, sizeof(bwr))) {//4 + ret = -EFAULT; + goto err; + } + break; + } + ... + return ret; +} +``` + +注释1处的copy_from_user函数,在本系列的第一篇**[Android Binder原理(一)学习Binder前必须要了解的知识点](# Android Binder原理(一)学习Binder前必须要了解的知识点)**提过。在这里,它用于将把用户空间数据ubuf拷贝出来保存到内核数据bwr(binder_write_read结构体)中。 +注释2处,bwr的输入缓存区有数据时,会调用注释3处的binder_thread_write函数来处理BC_ENTER_LOOPER协议,其内部会将目标线程的状态设置为BINDER_LOOPER_STATE_ENTERED,这样目标线程就是一个Binder线程。 +注释4处通过copy_to_user函数将内核空间数据bwr拷贝到用户空间。 + +### 总结 + +ServiceManager的启动过程实际上就是分析ServiceManager的入口函数,在入口函数中主要做了三件事,本篇深入到内核源码来对这三件逐一进行分析,由于涉及的函数比较多,这篇文章只介绍了我们需要掌握的,剩余大家可以自行阅读源码,比如binder_thread_write、copy_to_user函数。 + +## Binder原理(五)系统服务的获取过程 + +在本系列的此前文章中,以MediaPlayerService为例,讲解了系统服务是如何注册的(addService),既然有注册那肯定也要有获取,本篇文章仍旧以MediaPlayerService为例,来讲解系统服务的获取过程(getService)。文章会分为两个部分进行讲解,分别是客户端MediaPlayerService请求获取服务和服务端ServiceManager处理请求,先来学习第一部分。 + +### 客户端MediaPlayerService请求获取服务 + +要想获取MediaPlayerService,需要先调用getMediaPlayerService函数,如下所示。 +frameworks/av/media/libmedia/IMediaDeathNotifier.cpp + +```cpp +IMediaDeathNotifier::getMediaPlayerService() +{ + ALOGV("getMediaPlayerService"); + Mutex::Autolock _l(sServiceLock); + if (sMediaPlayerService == 0) { + sp sm = defaultServiceManager();//1 + sp binder; + do { + binder = sm->getService(String16("media.player"));//2 + if (binder != 0) {//3 + break; + } + ALOGW("Media player service not published, waiting..."); + usleep(500000); //4 + } while (true); + + if (sDeathNotifier == NULL) { + sDeathNotifier = new DeathNotifier(); + } + binder->linkToDeath(sDeathNotifier); + sMediaPlayerService = interface_cast(binder);//5 + } + ALOGE_IF(sMediaPlayerService == 0, "no media player service!?"); + return sMediaPlayerService; +} +``` + +注释1处的defaultServiceManager返回的是BpServiceManager,注释2处获取名为”media.player”的系统服务(MediaPlayerService),返回的值为BpBinder。由于这个时候MediaPlayerService可能还没有向ServiceManager注册,那么就不能满足注释3的条件,在注释4处休眠0.5s后继续调用getService函数,直到获取服务对应的为止。 +注释5处的interface_cast函数用于将BpBinder转换成BpMediaPlayerService,其原理就是通过BpBinder的handle来找到对应的服务,即BpMediaPlayerService。 + +注释2处的获取服务是本文的重点,BpServiceManager的getService函数如下所示。 +**frameworks/native/libs/binder/IServiceManager.cpp::BpServiceManager** + +```cpp +virtual sp getService(const String16& name) const + { + ... + int n = 0; + while (uptimeMillis() < timeout) { + n++; + if (isVendorService) { + ALOGI("Waiting for vendor service %s...", String8(name).string()); + CallStack stack(LOG_TAG); + } else if (n%10 == 0) { + ALOGI("Waiting for service %s...", String8(name).string()); + } + usleep(1000*sleepTime); + + sp svc = checkService(name);//1 + if (svc != NULL) return svc; + } + ALOGW("Service %s didn't start. Returning NULL", String8(name).string()); + return NULL; + } +``` + +getService函数中主要做的事就是循环的查询服务是否存在,如果不存在就继续查询,查询服务用到了注释1处的checkService函数,代码如下所示。 +**frameworks/native/libs/binder/IServiceManager.cpp::BpServiceManager** + +```cpp +virtual sp checkService( const String16& name) const +{ + Parcel data, reply;//1 + data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor()); + data.writeString16(name);//2 + remote()->transact(CHECK_SERVICE_TRANSACTION, data, &reply);//3 + return reply.readStrongBinder(); +} +``` + +注释1处的data,看过上一篇文章的同学应该很熟悉,它出现在BpServiceManager的addService函数中,data是一个数据包,后面会不断的将数据写入到data中。注释2处将字符串"media.player"写入到data中。 +注释3处的remote()指的是mRemote,也就是BpBinder,BpBinder的transact函数如下所示。 + +**frameworks/native/libs/binder/BpBinder.cpp** + +```cpp +status_t BpBinder::transact( + uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) +{ + if (mAlive) { + status_t status = IPCThreadState::self()->transact( + mHandle, code, data, reply, flags); + if (status == DEAD_OBJECT) mAlive = 0; + return status; + } + + return DEAD_OBJECT; +} +``` + +BpBinder将逻辑处理交给IPCThreadState,后面的调用链在=[Android Binder原理(三)系统服务的注册过程](http://liuwangshu.cn/framework/binder/3-addservice.html)中讲过,这里再次简单的过一遍,IPCThreadState::self()会创建创建IPCThreadState,IPCThreadState的transact函数如下所示。 +**frameworks/native/libs/binder/IPCThreadState.cpp** + +```cpp +status_t IPCThreadState::transact(int32_t handle, + uint32_t code, const Parcel& data, + Parcel* reply, uint32_t flags) +{ + status_t err; + + flags |= TF_ACCEPT_FDS; + ... + err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);//1 + + if (err != NO_ERROR) { + if (reply) reply->setError(err); + return (mLastError = err); + } + + if ((flags & TF_ONE_WAY) == 0) { + ... + if (reply) { + err = waitForResponse(reply);//2 + } else { + Parcel fakeReply; + err = waitForResponse(&fakeReply); + } + ... + } else { + //不需要等待reply的分支 + err = waitForResponse(NULL, NULL); + } + + return err; +} +``` + +调用BpBinder的transact函数实际上就是调用IPCThreadState的transact函数。注释1处的writeTransactionData函数用于传输数据,其中第一个参数BC_TRANSACTION代表向Binder驱动发送命令协议。 +注释1处的writeTransactionData用于准备发送的数据,其内部会将BC_TRANSACTION和binder_transaction_data结构体写入到mOut中。 +接着查看waitForResponse函数做了什么,代码如下所示。 +**frameworks/native/libs/binder/IPCThreadState.cpp** + +```cpp +status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult) +{ + uint32_t cmd; + int32_t err; + while (1) { + if ((err=talkWithDriver()) < NO_ERROR) break;//1 + err = mIn.errorCheck(); + if (err < NO_ERROR) break; + if (mIn.dataAvail() == 0) continue; + cmd = (uint32_t)mIn.readInt32(); + IF_LOG_COMMANDS() { + alog << "Processing waitForResponse Command: " + << getReturnString(cmd) << endl; + } + switch (cmd) { + case BR_TRANSACTION_COMPLETE: + if (!reply && !acquireResult) goto finish; + break; + ... + default: + //处理各种命令协议 + err = executeCommand(cmd); + if (err != NO_ERROR) goto finish; + break; + } +} +finish: + ... + return err; +} +``` + +注释1处的talkWithDriver函数的内部通过ioctl与Binder驱动进行通信,代码如下所示。 +**frameworks/native/libs/binder/IPCThreadState.cpp** + +```cpp +status_t IPCThreadState::talkWithDriver(bool doReceive) +{ + if (mProcess->mDriverFD <= 0) { + return -EBADF; + } + //和Binder驱动通信的结构体 + binder_write_read bwr; //1 + //mIn是否有可读的数据,接收的数据存储在mIn + const bool needRead = mIn.dataPosition() >= mIn.dataSize(); + const size_t outAvail = (!doReceive || needRead) ? mOut.dataSize() : 0; + bwr.write_size = outAvail; + bwr.write_buffer = (uintptr_t)mOut.data();//2 + //这时doReceive的值为true + if (doReceive && needRead) { + bwr.read_size = mIn.dataCapacity(); + bwr.read_buffer = (uintptr_t)mIn.data();//3 + } else { + bwr.read_size = 0; + bwr.read_buffer = 0; + } + ... + if ((bwr.write_size == 0) && (bwr.read_size == 0)) return NO_ERROR; + bwr.write_consumed = 0; + bwr.read_consumed = 0; + status_t err; + do { + IF_LOG_COMMANDS() { + alog << "About to read/write, write size = " << mOut.dataSize() << endl; + } +#if defined(__ANDROID__) + if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)//4 + err = NO_ERROR; + else + err = -errno; +#else + err = INVALID_OPERATION; +#endif + ... + } while (err == -EINTR); + ... + return err; +} +``` + +注释1处的 binder_write_read是和Binder驱动通信的结构体,在注释2和3处将mOut、mIn赋值给binder_write_read的相应字段,最终通过注释4处的ioctl函数和Binder驱动进行通信。这一过程的时序图如下所示。 +[![MUr7w9.md.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMS8yNy8xNmVhYjNiMjRiZTViZmU5?x-oss-process=image/format,png)](https://imgchr.com/i/MUr7w9) + +这时我们需要再次查看[Android Binder原理(三)系统服务的注册过程](#Android Binder原理(三)系统服务的注册过程)这篇第2小节给出的图。 +![Ka0Dx0.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMS8yNy8xNmVhYjNiMjRiYmExZDRh?x-oss-process=image/format,png) + +从这张简化的流程图可以看出,我们当前分析的是客户端进程的流程,当MediaPlayerService向Binder驱动发送BC_TRANSACTION命令后,Binder驱动会向ServiceManager发送BR_TRANSACTION命令,接下来我们来查看服务端ServiceManager是如何处理获取服务这一请求的。 + +### 服务端ServiceManager处理请求 + +说到服务端ServiceManager处理请求,不得不说到ServiceManager的启动过程,具体的请看[Android Binder原理(四)ServiceManager的启动过程](#Android Binder原理(四)ServiceManager的启动过程) 这篇。 +这里简单回顾servicemanager的入口函数,如下所示。 + +**frameworks/native/cmds/servicemanager/service_manager.c** + +```cpp +int main(int argc, char** argv) +{ + ... + bs = binder_open(driver, 128*1024); + ... + if (binder_become_context_manager(bs)) { + ALOGE("cannot become context manager (%s)\n", strerror(errno)); + return -1; + } + ... + if (getcon(&service_manager_context) != 0) { + ALOGE("SELinux: Failed to acquire service_manager context. Aborting.\n"); + abort(); + } + binder_loop(bs, svcmgr_handler);//1 + return 0; +} +``` + +main函数主要做了三件事,其中最后一件事就是调用binder_loop函数,这里需要注意,它的第二个参数为svcmgr_handler,后面会再次提到svcmgr_handler。 +binder_loop函数如下所示。 +**frameworks/native/cmds/servicemanager/binder.c** + +```cpp +void binder_loop(struct binder_state *bs, binder_handler func) +{ +... + for (;;) { + bwr.read_size = sizeof(readbuf); + bwr.read_consumed = 0; + bwr.read_buffer = (uintptr_t) readbuf; + + res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr); + + if (res < 0) { + ALOGE("binder_loop: ioctl failed (%s)\n", strerror(errno)); + break; + } + res = binder_parse(bs, 0, (uintptr_t) readbuf, bwr.read_consumed, func); + if (res == 0) { + ALOGE("binder_loop: unexpected reply?!\n"); + break; + } + if (res < 0) { + ALOGE("binder_loop: io error %d %s\n", res, strerror(errno)); + break; + } + } +} +``` + +在无限循环中不断的调用ioctl函数,它不断的使用BINDER_WRITE_READ指令查询Binder驱动中是否有新的请求,如果有就交给binder_parse函数处理。如果没有,当前线程就会在Binder驱动中睡眠,等待新的进程间通信请求。 +binder_parse函数如下所示。 +**frameworks/native/cmds/servicemanager/binder.c** + +```cpp +int binder_parse(struct binder_state *bs, struct binder_io *bio, + uintptr_t ptr, size_t size, binder_handler func) +{ + int r = 1; + uintptr_t end = ptr + (uintptr_t) size; + + while (ptr < end) { + uint32_t cmd = *(uint32_t *) ptr; + ptr += sizeof(uint32_t); +#if TRACE + fprintf(stderr,"%s:\n", cmd_name(cmd)); +#endif + switch(cmd) { + ... + case BR_TRANSACTION: { + struct binder_transaction_data *txn = (struct binder_transaction_data *) ptr; + if ((end - ptr) < sizeof(*txn)) { + ALOGE("parse: txn too small!\n"); + return -1; + } + binder_dump_txn(txn); + if (func) { + unsigned rdata[256/4]; + struct binder_io msg; + struct binder_io reply; + int res; + + bio_init(&reply, rdata, sizeof(rdata), 4); + bio_init_from_txn(&msg, txn); + res = func(bs, txn, &msg, &reply);//1 + if (txn->flags & TF_ONE_WAY) { + binder_free_buffer(bs, txn->data.ptr.buffer); + } else { + binder_send_reply(bs, &reply, txn->data.ptr.buffer, res); + } + } + ptr += sizeof(*txn); + break; + } + ... + } + + return r; +} +``` + +这里截取了BR_TRANSACTION命令的处理部分,注释1出的func通过一路传递指向的是svcmgr_handler,svcmgr_handler函数如下所示。 +**frameworks/native/cmds/servicemanager/service_manager.c** + +```cpp +int svcmgr_handler(struct binder_state *bs, + struct binder_transaction_data *txn, + struct binder_io *msg, + struct binder_io *reply) +{ + ... + switch(txn->code) { + case SVC_MGR_GET_SERVICE: + case SVC_MGR_CHECK_SERVICE: + s = bio_get_string16(msg, &len); + if (s == NULL) { + return -1; + } + handle = do_find_service(s, len, txn->sender_euid, txn->sender_pid); + if (!handle) + break; + bio_put_ref(reply, handle); + return 0; + + ... + default: + ALOGE("unknown code %d\n", txn->code); + return -1; + } + + bio_put_uint32(reply, 0); + return 0; +} +``` + +当要获取服务时,会调用do_find_service函数,代码如下所示。 +**frameworks/native/cmds/servicemanager/service_manager.c** + +```cpp +uint32_t do_find_service(const uint16_t *s, size_t len, uid_t uid, pid_t spid) +{ + struct svcinfo *si = find_svc(s, len);//1 + + if (!si || !si->handle) { + return 0; + } + + if (!si->allow_isolated) { + uid_t appid = uid % AID_USER; + if (appid >= AID_ISOLATED_START && appid <= AID_ISOLATED_END) { + return 0; + } + } + if (!svc_can_find(s, len, spid, uid)) { + return 0; + } + + return si->handle; +} +``` + +注释1处的find_svc函数用于查询服务,返回的svcinfo是一个结构体,其内部包含了服务的handle值,最终会返回服务的handle值。接着来看find_svc函数: +**frameworks/native/cmds/servicemanager/service_manager.c** + +```cpp +struct svcinfo *find_svc(const uint16_t *s16, size_t len) +{ + struct svcinfo *si; + + for (si = svclist; si; si = si->next) { + if ((len == si->len) && + !memcmp(s16, si->name, len * sizeof(uint16_t))) { + return si; + } + } + return NULL; +} +``` + +系统服务的注册流程中,在Kernel Binder中会调用do_add_service函数,其内部会将包含服务名和handle值的svcinfo保存到svclist列表中。同样的,在获取服务的流程中,find_svc函数中会遍历svclist列表,根据服务名查找对应服务是否已经注册,如果已经注册就会返回对应的svcinfo,如果没有注册就返回NULL。 + +### 总结 + +这篇将系统服务的获取过程分为两个部分,代码涉及到了Native Binder和Kernel Binder。在下一篇文章中会继续学习Java Binder相关的内容。 + +## Binder原理(六)Java Binder的初始化 + +在[Android Binder原理(一)学习Binder前必须要了解的知识点](#Android Binder原理(一)学习Binder前必须要了解的知识点)这篇中,我根据Android系统的分层,将Binder机制分为了三层: + +1. Java Binder (对应Framework层的Binder) +2. Native Binder(对应Native层的Binder) +3. Kernel Binder(对应Kernel层的Binder) + +在此前,我一直都在介绍Native Binder和Kernel Binder的内容,它们的架构简单总结为下图。 + +[![MgRMbF.png](https://s2.ax1x.com/2019/11/19/MgRMbF.png)](https://s2.ax1x.com/2019/11/19/MgRMbF.png) + +在[Android Binder原理(二)ServiceManager中的Binder机制](#Android Binder原理(二)ServiceManager中的Binder机制)这篇中,我讲过BpBinder是Client端与Server交互的代理类,而BBinder则代表了Server端,那么上图就可以改为: +[![MgWuRI.png](https://s2.ax1x.com/2019/11/19/MgWuRI.png)](https://s2.ax1x.com/2019/11/19/MgWuRI.png) +从上图可以看到,Native Binder实际是基于C/S架构,Bpinder是Client端,BBinder是Server端,在[Android Binder原理(四)ServiceManager的启动过程](#Android Binder原理(四)ServiceManager的启动过程)这篇中,我们得知Native Binder通过ioctl函数和Binder驱动进行数据交互。 +Java Binder是需要借助Native Binder来进行工作的,因此Java Binder在设计上也是一个C/S架构,可以说Java Binder是Native Binder的一个镜像,所以在学习Java Binder前,最好先要学习此前文章讲解的Native Binder的内容。本篇文章先来讲解Java Binder是如何初始化的,即Java Binder的JNI注册。 + +### Java Binder的JNI注册 + +Java Binder要想和Native Binder进行通信,需要通过JNI,JNI的注册是在Zygote进程启动过程中注册的,代码如下所示。 +**frameworks/base/core/jni/AndroidRuntime.cpp** + +```cpp +void AndroidRuntime::start(const char* className, const Vector& options, bool zygote) +{ + ... + JniInvocation jni_invocation; + jni_invocation.Init(NULL); + JNIEnv* env; + if (startVm(&mJavaVM, &env, zygote) != 0) {//1 + return; + } + onVmCreated(env); + if (startReg(env) < 0) { + ALOGE("Unable to register all android natives\n"); + return; + } + ... +} +``` + +注释1处用于启动Java虚拟机,注释2处startReg函数用于完成虚拟机的JNI注册,关于AndroidRuntime的start函数的具体分析见[Android系统启动流程(二)解析Zygote进程启动过程](# Android系统启动流程(二)解析Zygote进程启动过程)这篇。 +startReg函数如下所示。 +**frameworks/base/core/jni/AndroidRuntime.cpp** + +```cpp +/*static*/ int AndroidRuntime::startReg(JNIEnv* env) +{ + ATRACE_NAME("RegisterAndroidNatives"); + androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc); + + ALOGV("--- registering native functions ---\n"); + env->PushLocalFrame(200); + + if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {//1 + env->PopLocalFrame(NULL); + return -1; + } + env->PopLocalFrame(NULL); + return 0; +} +``` + +注释1处的register_jni_procs函数的作用就是循环调用gRegJNI数组的成员所对应的方法,如下所示。 + +```cpp +static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env) +{ + for (size_t i = 0; i < count; i++) { + if (array[i].mProc(env) < 0) { +#ifndef NDEBUG + ALOGD("----------!!! %s failed to load\n", array[i].mName); +#endif + return -1; + } + } + return 0; +} +``` + +gRegJNI数组中有100多个成员变量: + +```cpp +static const RegJNIRec gRegJNI[] = { + REG_JNI(register_com_android_internal_os_RuntimeInit), + REG_JNI(register_com_android_internal_os_ZygoteInit_nativeZygoteInit), + REG_JNI(register_android_os_SystemClock), + ... + REG_JNI(register_android_os_Binder),//1 + ... +}; +``` + +其中REG_JNI是一个宏定义: + +```cpp +#define REG_JNI(name) { name } +struct RegJNIRec { + int (*mProc)(JNIEnv*); +}; +``` + +实际上就是调用参数名所对应的函数。负责Java Binder和Native Binder通信的函数为注释1处的register_android_os_Binder,代码如下所示。 +**frameworks/base/core/jni/android_util_Binder.cpp** + +```cpp +int register_android_os_Binder(JNIEnv* env) +{ + //注册Binder类 + if (int_register_android_os_Binder(env) < 0) + return -1; + //注册BinderInternal类 + if (int_register_android_os_BinderInternal(env) < 0) + return -1; + //注册BinderProxy类 + if (int_register_android_os_BinderProxy(env) < 0) + return -1; + ... + return 0; +} +``` + +register_android_os_Binder函数做了三件事,分别是: +1.注册Binder类 +2.注册BinderInternal类 +3.注册BinderProxy类 + +它们是Java Binder关联类的一小部分,它们的关系如下图所示。 + +[![MTmhzd.png](https://s2.ax1x.com/2019/11/22/MTmhzd.png)](https://s2.ax1x.com/2019/11/22/MTmhzd.png) + +- IBinder接口中定义了很多整型的变量,其中定义一个叫做`FLAG_ONEWAY`的整形变量。客户端发起调用时,客户端一般会阻塞,直到服务端返回结果。设置`FLAG_ONEWAY`后,客户端只需要把请求发送到服务端就可以立即返回,而不需要等待服务端的结果,这是一种非阻塞方式。 +- Binder和BinderProxy实现了IBinder接口,Binder是服务端的代表,而BinderProxy是客户端的代表。 +- BinderInternal只是在Binder框架中被使用,其内部类GcWatcher用于处理和Binder的垃圾回收。 +- Parcel是一个数据包装器,它可以在进程间进行传递,Parcel既可以传递基本数据类型也可以传递Binder对象,Binder通信就是通过Parcel来进行客户端与服务端数据交互。Parcel的实现既有Java部分,也有Native部分,具体实现在Native部分中。 + +下面分别对Binder、BinderInternal这两个类的注册进行分析。 + +#### Binder类的注册 + +调用int_register_android_os_Binder函数来完成Binder类的注册,代码如下所示。 +**frameworks/base/core/jni/android_util_Binder.cpp** + +```cpp +static const JNINativeMethod gBinderMethods[] = { + /* name, signature, funcPtr */ + { "getCallingPid", "()I", (void*)android_os_Binder_getCallingPid }, + { "getCallingUid", "()I", (void*)android_os_Binder_getCallingUid }, + { "clearCallingIdentity", "()J", (void*)android_os_Binder_clearCallingIdentity }, + { "restoreCallingIdentity", "(J)V", (void*)android_os_Binder_restoreCallingIdentity }, + { "setThreadStrictModePolicy", "(I)V", (void*)android_os_Binder_setThreadStrictModePolicy }, + { "getThreadStrictModePolicy", "()I", (void*)android_os_Binder_getThreadStrictModePolicy }, + { "flushPendingCommands", "()V", (void*)android_os_Binder_flushPendingCommands }, + { "getNativeBBinderHolder", "()J", (void*)android_os_Binder_getNativeBBinderHolder }, + { "getNativeFinalizer", "()J", (void*)android_os_Binder_getNativeFinalizer }, + { "blockUntilThreadAvailable", "()V", (void*)android_os_Binder_blockUntilThreadAvailable } +}; +const char* const kBinderPathName = "android/os/Binder";//1 +static int int_register_android_os_Binder(JNIEnv* env) +{ + jclass clazz = FindClassOrDie(env, kBinderPathName);//2 + + gBinderOffsets.mClass = MakeGlobalRefOrDie(env, clazz);//3 + gBinderOffsets.mExecTransact = GetMethodIDOrDie(env, clazz, "execTransact", "(IJJI)Z");//4 + gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J"); + + return RegisterMethodsOrDie( + env, kBinderPathName, + gBinderMethods, NELEM(gBinderMethods)); +} +``` + +注释1处的kBinderPathName的值为”android/os/Binder”,这是Binder在Java Binder中的全路径名。 +注释2处根据这个路径名获取Binder的Class对象,并赋值给jclass类型的变量clazz,clazz是Java层Binder在JNI层的代表。 +注释3处通过MakeGlobalRefOrDie函数将本地引用clazz转变为全局引用并赋值给gBinderOffsets.mClass。 +注释4处用于找到Java层的Binder的成员方法execTransact并赋值给gBinderOffsets.mExecTransact。 +注释5处用于找到Java层的Binder的成员变量mObject并赋值给gBinderOffsets.mObject。 +最后一行通过RegisterMethodsOrDie函数注册gBinderMethods中定义的函数,其中gBinderMethods是JNINativeMethod类型的数组,里面存储的是Binder的Native方法(Java层)与JNI层函数的对应关系。 + +gBinderMethods的定义如下所示。 + +```cpp +static struct bindernative_offsets_t +{ + jclass mClass; + jmethodID mExecTransact; + jfieldID mObject; + +} gBinderOffsets; +``` + +使用gBinderMethods来保存变量和方法有两个原因: +1.为了效率考虑,如果每次调用相关的方法时都需要查询方法和变量,显然效率比较低。 +2.这些成员变量和方法都是本地引用,在int int_register_android_os_Binder函数返回时,这些本地引用会被自动释放,因此用gBinderOffsets来保存,以便于后续使用。 + +对于JNI不大熟悉的同学可以看[Android深入理解JNI(二)类型转换、方法签名和JNIEnv](http://liuwangshu.cn/framework/jni/2-signature-jnienv)这篇文章。 + +#### BinderInternal类的注册 + +调用int_register_android_os_BinderInternal函数来完成BinderInternal类的注册,代码如下所示。 +**frameworks/base/core/jni/android_util_Binder.cpp** + +```cpp +const char* const kBinderInternalPathName = "com/android/internal/os/BinderInternal"; +static int int_register_android_os_BinderInternal(JNIEnv* env) +{ + jclass clazz = FindClassOrDie(env, kBinderInternalPathName); + + gBinderInternalOffsets.mClass = MakeGlobalRefOrDie(env, clazz); + gBinderInternalOffsets.mForceGc = GetStaticMethodIDOrDie(env, clazz, "forceBinderGc", "()V"); + gBinderInternalOffsets.mProxyLimitCallback = GetStaticMethodIDOrDie(env, clazz, "binderProxyLimitCallbackFromNative", "(I)V"); + + jclass SparseIntArrayClass = FindClassOrDie(env, "android/util/SparseIntArray"); + gSparseIntArrayOffsets.classObject = MakeGlobalRefOrDie(env, SparseIntArrayClass); + gSparseIntArrayOffsets.constructor = GetMethodIDOrDie(env, gSparseIntArrayOffsets.classObject, + "", "()V"); + gSparseIntArrayOffsets.put = GetMethodIDOrDie(env, gSparseIntArrayOffsets.classObject, "put", + "(II)V"); + + BpBinder::setLimitCallback(android_os_BinderInternal_proxyLimitcallback); + + return RegisterMethodsOrDie( + env, kBinderInternalPathName, + gBinderInternalMethods, NELEM(gBinderInternalMethods)); +} +``` + +和int_register_android_os_Binder函数的实现类似,主要做了三件事: +1.获取BinderInternal在JNI层的代表clazz。 +2.将BinderInternal类中有用的成员变量和方法存储到gBinderInternalOffsets中。 +3.注册BinderInternal类的Native方法对应的JNI函数。 + +还有一个BinderProxy类的注册,它和Binder、BinderInternal的注册过程差不多,这里就不再赘述了,有兴趣的读者可以自行去看源码。 + +## Binder原理(七)Java Binder中系统服务的注册过程 + +在[Android Binder原理(三)系统服务的注册过程](#Android Binder原理(三)系统服务的注册过程)这篇文章中,我介绍的是Native Binder中的系统服务的注册过程,这一过程的核心是ServiceManager,而在Java Binder中,也有一个ServiceManager,只不过这个ServiceManager是Java文件。 +既然要将系统服务注册到ServiceManager,那么需要选择一个系统服务为例,这里以常见的AMS为例。 + +### 将AMS注册到ServiceManager + +在AMS的setSystemProcess方法中,会调用ServiceManager的addService方法,如下所示。 +**frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java** + +```cpp +public void setSystemProcess() { + try { + ServiceManager.addService(Context.ACTIVITY_SERVICE, this, /* allowIsolated= */ true, + DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO);//1 + .... + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException( + "Unable to find android system package", e); + } + ... +} +``` + +注释1处的`Context.ACTIVITY_SERVICE`的值为”activity”,作用就是将AMS注册到ServiceManager中。接着来看 +ServiceManager的addService方法。 +**frameworks/base/core/java/android/os/ServiceManager.java** + +```cpp +public static void addService(String name, IBinder service, boolean allowIsolated, + int dumpPriority) { + try { + getIServiceManager().addService(name, service, allowIsolated, dumpPriority); + } catch (RemoteException e) { + Log.e(TAG, "error in addService", e); + } +} +``` + +主要分析getIServiceManager方法返回的是什么,代码如下所示。 +**frameworks/base/core/java/android/os/ServiceManager.java** + +```cpp +private static IServiceManager getIServiceManager() { + if (sServiceManager != null) { + return sServiceManager; + } + sServiceManager = ServiceManagerNative + .asInterface(Binder.allowBlocking(BinderInternal.getContextObject())); + return sServiceManager; + } +``` + +讲到这里,已经积累了几个点需要分析,分别是: + +- BinderInternal.getContextObject() +- ServiceManagerNative.asInterface() +- getIServiceManager().addService() + +现在我们来各个击破它们。 + +#### BinderInternal.getContextObject() + +Binder.allowBlocking的作用是将BinderProxy的sWarnOnBlocking值置为false。主要来分析BinderInternal.getContextObject()做了什么,这个方法是一个Native方法,找到它对应的函数: +**frameworks/base/core/jni/android_util_Binder.cpp** + +```cpp +static const JNINativeMethod gBinderInternalMethods[] = { + { "getContextObject", "()Landroid/os/IBinder;", (void*)android_os_BinderInternal_getContextObject }, + ... +}; +``` + +对应的函数为android_os_BinderInternal_getContextObject: +**frameworks/base/core/jni/android_util_Binder.cpp** + +```cpp +static jobject android_os_BinderInternal_getContextObject(JNIEnv* env, jobject clazz) +{ + sp b = ProcessState::self()->getContextObject(NULL);//1 + return javaObjectForIBinder(env, b); +} +``` + +ProcessState::self()的作用是创建ProcessState,注释1处最终返回的是BpBinder,不理解的可以查看[Android Binder原理(二)ServiceManager中的Binder机制](#Android Binder原理(二)ServiceManager中的Binder机制)这篇文章。 + +BpBinder是Native Binder中的Client端,这说明Java层的ServiceManager需要Native层的BpBinder,但是这个BpBinder在Java层是无法直接使用,那么就需要传入javaObjectForIBinder函数来做处理,其内部会创建一个BinderProxy对象,这样我们得知 BinderInternal.getContextObject()最终得到的是BinderProxy。 +在[Android Binder原理(六)Java Binder的初始化](#Android Binder原理(六)Java Binder的初始化)这篇文章我们讲过,BinderProxy是Java Binder的客户端的代表。 +需要注意的一点是,这个传入的BpBinder会保存到BinderProxy的成员变量mObject中,后续会再次提到这个点。 + +#### ServiceManagerNative.asInterface() + +说到asInterface方法,在Native Binder中也有一个asInterface函数。在[Android Binder原理(二)ServiceManager中的Binder机制](#Android Binder原理(二)ServiceManager中的Binder机制)这篇文章中讲过IServiceManager的asInterface函数,它的作用是用BpBinder做为参数创建BpServiceManager。那么在Java Binder中的asInterface方法的作用又是什么? +**frameworks/base/core/java/android/os/ServiceManagerNative.java** + +```cpp +static public IServiceManager asInterface(IBinder obj) + { + if (obj == null) { + return null; + } + IServiceManager in = + (IServiceManager)obj.queryLocalInterface(descriptor); + if (in != null) { + return in; + } + + return new ServiceManagerProxy(obj); + } +``` + +根据1.1小节,我们得知obj的值为BinderProxy,那么asInterface方法的作用就是用BinderProxy作为参数创建ServiceManagerProxy。 +BinderProxy和BpBinder分别在Jave Binder和Native Binder作为客户端的代表,BpServiceManager通过BpBinder来实现通信,同样的,ServiceManagerProxy也会将业务的请求交给BinderProxy来处理。 +分析到这里,那么: + +```cpp +sServiceManager = ServiceManagerNative + .asInterface(Binder.allowBlocking(BinderInternal.getContextObject())); +``` + +可以理解为: + +```cpp + sServiceManager = new ServiceManagerProxy(BinderProxy); +} +``` + +#### getIServiceManager().addService() + +根据1.2节的讲解,getIServiceManager()返回的是ServiceManagerProxy,ServiceManagerProxy是ServiceManagerNative的内部类,它实现了IServiceManager接口。 + +来查看ServiceManagerProxy的addService方法, +**frameworks/base/core/java/android/os/ServiceManagerNative.java::ServiceManagerProxy** + +```cpp +public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IServiceManager.descriptor); + data.writeString(name); + data.writeStrongBinder(service);//1 + data.writeInt(allowIsolated ? 1 : 0); + data.writeInt(dumpPriority); + mRemote.transact(ADD_SERVICE_TRANSACTION, data, reply, 0);//2 + reply.recycle(); + data.recycle(); +} +``` + +注释1处的data.writeStrongBinder很关键,后续会进行分析。这里又看到了Parcel,它是一个数据包装器,将请求数据写入到Parcel类型的对象data中,通过注释1处的mRemote.transact发送出去,mRemote实际上是BinderProxy,BinderProxy.transact是native函数,实现的函数如下所示。 +**frameworks/base/core/jni/android_util_Binder.cpp** + +```cpp +static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj, + jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException +{ + if (dataObj == NULL) { + jniThrowNullPointerException(env, NULL); + return JNI_FALSE; + } + Parcel* data = parcelForJavaObject(env, dataObj);//1 + if (data == NULL) { + return JNI_FALSE; + } + Parcel* reply = parcelForJavaObject(env, replyObj);//2 + if (reply == NULL && replyObj != NULL) { + return JNI_FALSE; + } + IBinder* target = getBPNativeData(env, obj)->mObject.get();//3 + if (target == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", "Binder has been finalized!"); + return JNI_FALSE; + } + ... + status_t err = target->transact(code, *data, reply, flags);//4 + return JNI_FALSE; +} +``` + +注释1和注释2处,将Java层的Parcel对象转化成为Native层的Parcel对象。在1.1小节中,我们得知BpBinder会保存到BinderProxy的成员变量mObject中,因此在注释3处,从BinderProxy的成员变量mObject中获取BpBinder。最终会在注释4处调用BpBinder的transact函数,向Binder驱动发送数据,可以看出Java Binder是需要Native Binder支持的,最终的目的就是向Binder驱动发送和接收数据。 + +### 引出JavaBBinder + +接着回过头来分析1.3小节遗留下来的data.writeStrongBinder(service),代码如下所示。 +**frameworks/base/core/java/android/os/Parcel.java** + +```cpp +public final void writeStrongBinder(IBinder ll) { + nativeWriteStrongBinder(mNativePtr, val); + } +``` + +nativeWriteStrongBinder是Native方法,实现的函数为android_os_Parcel_writeStrongBinder: +**frameworks/base/core/jni/android_os_Parcel.cpp** + +```cpp +static void android_os_Parcel_writeStrongBinder(JNIEnv* env, jclass clazz, jlong nativePtr, jobject object) +{ + Parcel* parcel = reinterpret_cast(nativePtr); + if (parcel != NULL) { + const status_t err = parcel->writeStrongBinder(ibinderForJavaObject(env, object));//1 + if (err != NO_ERROR) { + signalExceptionForError(env, clazz, err); + } + } +} +``` + +接着查看注释1处ibinderForJavaObject函数: +**frameworks/base/core/jni/android_util_Binder.cpp** + +```cpp +sp ibinderForJavaObject(JNIEnv* env, jobject obj) +{ + if (obj == NULL) return NULL; + if (env->IsInstanceOf(obj, gBinderOffsets.mClass)) {//1 + JavaBBinderHolder* jbh = (JavaBBinderHolder*) + env->GetLongField(obj, gBinderOffsets.mObject); + return jbh->get(env, obj);//2 + } + if (env->IsInstanceOf(obj, gBinderProxyOffsets.mClass)) { + return getBPNativeData(env, obj)->mObject; + } + + ALOGW("ibinderForJavaObject: %p is not a Binder object", obj); + return NULL; +} +``` + +注释2处,如果obj是Java层的BinderProxy类,则返回BpBinder。 +注释1处,如果obj是Java层的Binder类,那么先获取JavaBBinderHolder对象,然后在注释2处调用JavaBBinderHolder的get函数,代码如下所示。 +**frameworks/base/core/jni/android_util_Binder.cpp::JavaBBinderHolder** + +```cpp +class JavaBBinderHolder +{ +public: + sp get(JNIEnv* env, jobject obj) + { + AutoMutex _l(mLock); + sp b = mBinder.promote();//1 + if (b == NULL) { + //obj是一个Java层Binder对象 + b = new JavaBBinder(env, obj);//2 + mBinder = b; + ALOGV("Creating JavaBinder %p (refs %p) for Object %p, weakCount=%" PRId32 "\n", + b.get(), b->getWeakRefs(), obj, b->getWeakRefs()->getWeakCount()); + } + return b; + } + sp getExisting() + { + AutoMutex _l(mLock); + return mBinder.promote(); + } +private: + Mutex mLock; + wp mBinder; +}; +``` + +成员变量mBinder是`wp`类型的弱引用,在注释1处得到`sp`类型的强引用b,在注释2处创建JavaBBinder并赋值给b。那么,JavaBBinderHolder的get函数返回的是JavaBBinder。 + +data.writeStrongBinder(service)在本文中等价于: + +```code +data.writeStrongBinder(new JavaBBinder(env,Binder))。 +``` + +讲到这里可以得知ServiceManager.addService()传入的并不是AMS本身,而是JavaBBinder。 + +### 解析JavaBBinder + +接着来分析JavaBBinder,查看它的构造函数: +**frameworks/base/core/jni/android_util_Binder.cpp::JavaBBinderHolder::JavaBBinder** + +```cpp +class JavaBBinder : public BBinder +{ +public: + JavaBBinder(JNIEnv* env, jobject /* Java Binder */ c) + : mVM(jnienv_to_javavm(env)), mObject(env->NewGlobalRef(object)) + { + ALOGV("Creating JavaBBinder %p\n", this); + gNumLocalRefsCreated.fetch_add(1, std::memory_order_relaxed); + gcIfManyNewRefs(env); + } +... +``` + +可以发现JavaBBinder继承了BBinder,那么JavaBBinder的作用是什么呢?当Binder驱动得到客户端的请求,紧接着会将响应发送给JavaBBinder,这时会调用JavaBBinder的onTransact函数,代码如下所示。 +**frameworks/base/core/jni/android_util_Binder.cpp::JavaBBinderHolder::JavaBBinder** + +```cpp +virtual status_t onTransact( + uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0) + { + JNIEnv* env = javavm_to_jnienv(mVM); + ALOGV("onTransact() on %p calling object %p in env %p vm %p\n", this, mObject, env, mVM); + IPCThreadState* thread_state = IPCThreadState::self(); + const int32_t strict_policy_before = thread_state->getStrictModePolicy(); + jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact, + code, reinterpret_cast(&data), reinterpret_cast(reply), flags);//1 + + ... + return res != JNI_FALSE ? NO_ERROR : UNKNOWN_TRANSACTION; + } +``` + +在注释1处会调用Java层Binder的execTransact函数: +**frameworks/base/core/java/android/os/Binder.java** + +```cpp + private boolean execTransact(int code, long dataObj, long replyObj, + int flags) { +... + try { + if (tracingEnabled) { + Trace.traceBegin(Trace.TRACE_TAG_ALWAYS, getClass().getName() + ":" + code); + } + res = onTransact(code, data, reply, flags);//1 + } catch (RemoteException|RuntimeException e) { + ... + } + ... + return res; + } +``` + +关键点是注释1处的onTransact函数,AMS实现了onTransact函数,从而完成业务实现。 +从这里可有看出,JavaBBinder并没有实现什么业务,当它接收到请求时,会调用Binder类的execTransact函数,execTransact函数内部又调用了onTransact函数,系统服务会重写onTransact函数来实现自身的业务功能。 + +### Java Binder架构 + +Binder架构如下图所示。 +[![Qud4yt.png](https://s2.ax1x.com/2019/12/02/Qud4yt.png)](https://s2.ax1x.com/2019/12/02/Qud4yt.png) + +Native Binder的部分在此前的文章已经讲过,这里主要来说说Java Binder部分,从图中可以看到: +1.Binder是服务端的代表,JavaBBinder继承BBinder,JavaBBinder通过mObject变量指向Binder。 +2.BinderProxy是客户端的代表,ServiceManager的addService等方法会交由ServiceManagerProxy处理。 +3.ServiceManagerProxy的成员变量mRemote指向BinderProxy对象,所以ServiceManagerProxy的addService等方法会交由BinderProxy来处理。 +4.BinderProxy的成员变量mObject指向BpBinder对象,因此BinderProxy可以通过BpBinder和Binder驱动发送数据。 + + + +# Zygote + +## Zygote(一):Android系统的启动过程及Zygote的启动过程 + +### init进程 + +作为Android开发者和Android使用者,相信每个人都接触过多种Android设备,不管是哪种品牌、哪种类型的Android设置,在使用之前都要完成开机操作,对于普通用户来说开机只是一个操作过程,但对于开发者有没有想过Android是如何开机的?是如何从断电状态启动到可操作交互的?开发者都听过init进程、孵化器进程已经开发中使用的各种服务,那么它们又是如何启动如何工作的呢?带着这些问题进入本篇文章的主题Androdi系统的启动过程; + +- Android系统的启动过程总结 + +1. 启动电源:按下电源后,程序从固定地方开始加载引导程序到RAM中 +2. 引导程序BootLoader:Android系统开始执行引导程序,并同时拉起并运行系统的OS +3. Linux内核启动:当内核启动完成后,首先寻找init.rc配置文件并启动init进程 +4. init进程启动:在init进程中完成属性服务的初始、Zygote进程的启动 + +由上面的程序启动过程知道,程序在执行完引导程序并启动内核后,首先会查找init文件,在init文件中首先执行main()方法 + +- init.main() + +```java +...... +property_init();//1 +...... +sigchld_handler_init();//2 +...... +start_property_service(); +...... +LoadBootScripts(am, sm); + +static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) { + Parser parser = CreateParser(action_manager, service_list); + parser.ParseConfig("/init.rc"); //3 +} +``` + +在main()函数中主要执行一下操作: + +1. 初始化和启动属性服务 +2. 设置进程信号处理 +3. 解析init.rc配置文件 + +#### 属性服务初始化与启动 + +- 属性服务的初始化 + +1. 创建非阻塞的Socket +2. 调用listen函数对对属性进行监听 +3. 当有数据更新时,init进程会调用handle_property_set_fd函数进行处理 + +- 处理客户端请求 + +1. 服务属性接收到客户端请求时调用handle_property_set_fd()处理数据 +2. 根据属性分类处理:普通属性、控制属性 + +#### 设置进程信号处理 + +- 僵尸进程:父进程通过Fork创建子进程,当子进程终止之后,如果父进程不知道此时子进程已结束,此时系统中会仍然保存着进程的信息,那么子进程就会成为僵尸进程 + +1. 僵尸进程危害:系统资源有限,僵尸进程会占用系统资源,当资源耗尽时系统将无法创建新的进程 + +由僵尸进程的定义知道,出现僵尸进程的原因就是父进程与子进程之间通信中断,signal_handler_init函数就是在父进程中监听子进程的状态,在子进程暂停或终止时会发送SIGCHLD信号,signal_handler_init会接收和处理信号,当接收到子进程终止时及时的释放资源 + +#### 解析init配置文件 + +配置文件的解析和处理也是init进程中最主要的部分,安卓中将系统的配置文件保存在init.rc文件中,而Android 8.0之后对init.rc文件浸信会拆分,将每个服务以启动脚本的形式单独存在,然后在init.rc中引入所需要的服务脚本,在启动的时候就可以实现所有服务的启动,这里以接下来要分析的Zygote的启动脚本为例,看看系统是如何定义和处理脚本的 + +- 启动脚本——init.zygote64.rc + +```java +service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server + class main + priority -20 + user root + group root readproc reserved_disk + socket zygote stream 660 root system + onrestart write /sys/android_power/request_state wake + onrestart write /sys/power/state on + onrestart restart audioserver + onrestart restart cameraserver + onrestart restart media + onrestart restart netd + onrestart restart wificond + writepid /dev/cpuset/foreground/tasks +``` + +启动脚本中参数介绍: + +1. zygote:创建的进程名称 +2. /system/bin/app_process64 :执行的文件路径 +3. class main:表示Zygote的classname为main,后面会根据main查找Zygote服务 +4. onrestart:当服务启动时需要重启的服务 + +上面启动脚本文件的名称为init.zygote64.rc,脚本文件名称表示只支持64系统,不过有的启动过脚本会同时支持32为和64为系统,如init.zygote64_32.rc + +```java +service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote + class main + ...... + writepid /dev/cpuset/foreground/tasks + +service zygote_secondary /system/bin/app_process32 -Xzygote /system/bin --zygote --socket-name=zygote_secondary --enable-lazy-preload + class main + ....... + writepid /dev/cpuset/foreground/tasks +``` + +init.zygote64_32.rc脚本文件中有有两个Zygote服务,一个主模式支持64位的名为zygote进程,另一个辅模式支持32为名为zygote_secondary进程,系统会根据设备的属性决定启动的服务; + +- 解析启动脚本 + +init进程中会使用ServerParse对Service的启动脚本进行解析,最终会针对启动脚本中的每个服务创建对应的实例,然后将所有的对象实例缓存在Service裢表中,在启动服务时就会从此列表中查找对应的服务对象; + +### Zygote进程启动 + +在init.rc文件中引入Zygote的启动脚本,所以在解析init.rc配置文件的服务时,就会将Zygote启动脚本中的服务解析保存在Service的裢表中 + +```java +import /init.${ro.zygote}.rc +``` + +- init启动Zygote进程 + +```java +on nonencrypted + class_start main + class_start late_start +``` + +在解析服务后,会继续init.rc配置文件中的程序,程序执行class_start main,由前面的服务脚本可知classnam为main代表的时Zygote服务,所以此处代表启动Zygote进程,首先会遍历前面保存解析Service的链表,查找classname为main()的服务,然后执行Service中的start()方法; + +```java +Result Service::Start() { +pid_t pid = -1; + if (namespace_flags_) { + pid = clone(nullptr, nullptr, namespace_flags_ | SIGCHLD, nullptr); + } else { + pid = fork(); //1、 + } + if (pid == 0) { //2、 + if (!ExpandArgsAndExecv(args_)) { + PLOG(ERROR) << "cannot execve('" << args_[0] << "')"; + } + } +} + +static bool ExpandArgsAndExecv(const std::vector& args) { + return execv(c_strings[0], c_strings.data()) == 0; //3、 +} +``` + +在statr()方法中,首先判断进程是否已经运行,对未运行的进程通过fork()创建子进程,创建成功后调用ExpandArgsAndExecv()方法,在ExpandArgsAndExecv()中调用执行execv()后Service进程就被启动并进入Service的main()方法,Zygote进程对应的程序路径为app_main.cpp,在app_main.cpp的main()方法中调用runtime.start()启动进程 + +```java +int main(int argc, char* const argv[]){ + if (strcmp(arg, "--zygote") == 0) { //1 + zygote = true; + niceName = ZYGOTE_NICE_NAME; + } else if (strcmp(arg, "--start-system-server") == 0) { + startSystemServer = true; + } else if (strcmp(arg, "--application") == 0) { + application = true; + } else if (strncmp(arg, "--nice-name=", 12) == 0) { + niceName.setTo(arg + 12); + } else if (strncmp(arg, "--", 2) != 0) { + className.setTo(arg); + break; + } + } +} + +if (zygote) { + runtime.start("com.android.internal.os.ZygoteInit", args, zygote); + } +``` + +在app_main文件的main()方法中,首先根据进程的名称判断当前是否为Zyote进程,并赋值zygote为true,然后调用runtime.start()启动进程,注意这里的参数传入的是ZygoteInit类的全路径,这里先猜测下最后是根据全路径反射执行ZygoteInit方法,接着看runtime,这里的runtime指的是AndroidRuntime + +- AndroidRuntime.start() + +```java +void AndroidRuntime::start(const char* className, const Vector& options, bool zygote){ + JniInvocation jni_invocation; + jni_invocation.Init(NULL); + JNIEnv* env; + if (startVm(&mJavaVM, &env, zygote) != 0) { //1 + return; + } + onVmCreated(env); + if (startReg(env) < 0) { // 2 + return; + } + +char* slashClassName = toSlashClassName(className != NULL ? className : "");//3 +jclass startClass = env->FindClass(slashClassName);// 4 +jmethodID startMeth = env->GetStaticMethodID(startClass, "main", + "([Ljava/lang/String;)V"); // 5 + if (startMeth == NULL) { + ALOGE("JavaVM unable to find main() in '%s'\n", className); + } else { + env->CallStaticVoidMethod(startClass, startMeth, strArray); //6 + if (env->ExceptionCheck()) + threadExitUncaughtException(env); + } +} +``` + +在AndroidRuntime的start()方法中,执行了Zygote进程的主要逻辑: + +1. 启动Java虚拟机 +2. 为Java虚拟机注册JNI方法 +3. 通过JNI调用Java层ZygoteInit类中的方法完成进程的启动,此时程序由native进入Java层 + +- ZygoteInit + +```java +public static void main(String argv[]) { + ZygoteServer zygoteServer = new ZygoteServer();//1 + zygoteServer.registerServerSocketFromEnv(socketName); + + preload(bootTimingsTraceLog);//2 + + if (startSystemServer) { + Runnable r = forkSystemServer(abiList, socketName, zygoteServer);//3 + if (r != null) { + r.run(); + return; + } + } + caller = zygoteServer.runSelectLoop(abiList); //4 +} +``` + +程序进入Java层执行ZygoteInit.main()方法,在main()中主要执行: + +1. 首先创建并注册Service端的Socket,此Socket用于相应AMS请求创建进程 +2. 预加载类和资源 +3. 调用forkSystemServer()启动SystemServer进程 +4. 执行zygoteServer.runSelectLoop()循环等待AMS请求创建新的应用进程 + +关于ZygoteServer的注册和循环等待AMS创建进程的部分之后在[应用进程的启动过程](https://blog.csdn.net/Alexwll/article/details/100133553)中介绍,这里先来看看startSystemServer()启动SystemServer进程部分; + +#### SystemServer启动过程 + +SystemServer进程主要用于创建系统服务,如:AMS、WMS、PMS等都由SystemServer启动,由上面知道系统会调用forkSystemServer() + +```java +private static Runnable forkSystemServer(String abiList, String socketName, + ZygoteServer zygoteServer) { + String args[] = { //1 + "--setuid=1000", + "--setgid=1000", + "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1023,1024,1032,1065,3001,3002,3003,3006,3007,3009,3010", + "--capabilities=" + capabilities + "," + capabilities, + "--nice-name=system_server", + "--runtime-args", + "--target-sdk-version=" + VMRuntime.SDK_VERSION_CUR_DEVELOPMENT, + "com.android.server.SystemServer", + }; + parsedArgs = new ZygoteConnection.Arguments(args); //2 + pid = Zygote.forkSystemServer( //3 + parsedArgs.uid, parsedArgs.gid, + parsedArgs.gids, + parsedArgs.runtimeFlags, + null, + parsedArgs.permittedCapabilities, + parsedArgs.effectiveCapabilities); +} + if (pid == 0) { + return handleSystemServerProcess(parsedArgs);//4 + } +``` + +在forkSystemServer()方法中首先将启动参数封装在数组中,然后使用数组创建ZygoteConnection.Arguments对象,最后调用Zygote.forkSystemServer方法fok SystemServer进程,在forkSystemServer()中调用nativeForkSystemServer()方法实现进程创建,fork进程成功后调用handleSystemServerProcess()处理进程中的工作; + +```java +private static Runnable handleSystemServerProcess(ZygoteConnection.Arguments parsedArgs) { + ClassLoader cl = null; + if (systemServerClasspath != null) { + cl = createPathClassLoader(systemServerClasspath, parsedArgs.targetSdkVersion); + Thread.currentThread().setContextClassLoader(cl); + } + return ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs, cl); + } +``` + +在handleSystemServerProcess()中首先创建PathClassLoader对象,然后调用ZygoteInit.zygoteInit()方法 + +- ZygoteInit.zygoteInit + +```java + public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) { + ZygoteInit.nativeZygoteInit(); //1、 + return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);//2、 +} +``` + +在zygoteInit()中调用nativeZygoteInit()方法,从名字上看出调用的是native方法,在内部通过JNI方法完成Binder线程池的创建,在方法的最后调用RuntimeInit.applicationInit()方法传入ClassLoader,applicationInit()方法相对比较特殊,下面一起看下源码 + +```java +protected static Runnable applicationInit(int targetSdkVersion, String[] argv, + ClassLoader classLoader) { + final Arguments args = new Arguments(argv); + return findStaticMain(args.startClass, args.startArgs, classLoader);//1 + } + + protected static Runnable findStaticMain(String className, String[] argv, + ClassLoader classLoader) { + Class cl; + + try { + cl = Class.forName(className, true, classLoader);//2 + } catch (ClassNotFoundException ex) { + throw new RuntimeException( + "Missing class when invoking static main " + className, + ex); + } + + Method m; + try { + m = cl.getMethod("main", new Class[] { String[].class });//3 + } catch (NoSuchMethodException ex) { + throw new RuntimeException( + "Missing static main on " + className, ex); + } catch (SecurityException ex) { + throw new RuntimeException( + "Problem getting static main on " + className, ex); + } + + int modifiers = m.getModifiers(); + if (! (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))) { + throw new RuntimeException( + "Main method is not public and static on " + className); + } + return new MethodAndArgsCaller(m, argv); //4 + } +``` + +applicationInit()中调用了findStaticMain()方法,findStaticMain()并没有直接调用SystemServer.main()方法,而是通过反射获取SystemServer的Class,然后获取main()方法,并将main方法和参数封装在MethodAndArgsCaller中,这一点Android P中做了修改,之前的版本中将反射main方法封装在异常中抛出,然后捕捉异常执行,Android P返回了MethodAndArgsCaller对象,MethodAndArgsCaller继承实现Runnable,其实在前面ZygoteInit类中,有一段代码如下 + +```java +if (startSystemServer) { + Runnable r = forkSystemServer(abiList, socketName, zygoteServer); + if (r != null) { + r.run(); + return; + } + } +``` + +整个SystemServer继承的启动是从调用forkSystemServer()开始的,forkSystemServer返回了Runnable对象,这里的Runnable对象就是上面创建的MethodAndArgsCaller对象,然后调用run()方法执行MethodAndArgsCaller对象; + +```java +static class MethodAndArgsCaller implements Runnable { + private final Method mMethod; + private final String[] mArgs; + public MethodAndArgsCaller(Method method, String[] args) { + mMethod = method; + mArgs = args; + } + public void run() { + try { + mMethod.invoke(null, new Object[] { mArgs }); + } catch (IllegalAccessException ex) { + } + } +``` + +在MethodAndArgsCaller中保存了反射获取的Method,这里的Method就是SystemServer.main()方法,在run方法中调用method.invoke()执行main方法,反射执行之后程序进入SystemServer.main(),main中创建SystemServer对象,并执行run()方法; + +```java +public static void main(String[] args) { + new SystemServer().run(); + } +``` + +- SystemServer.run() + +```java +mSystemServiceManager = new SystemServiceManager(mSystemContext); //1 +// Start services. + try { + startBootstrapServices();//2 + startCoreServices();//3 + startOtherServices();//4 + SystemServerInitThreadPool.shutdown(); + } catch (Throwable ex) { + throw ex; + } +``` + +main()方法中直接调用SystemServer.run()方法,在run()方法中,首先创建系统的SystemServiceManager对象,然后依次调用方法启动引导服务、启动核心服务、启动其他服务 + +- 启动服务过程 + +```java +mSystemServiceManager.startService(PowerManagerService.class); + + public SystemService startService(String className) { + final Class serviceClass; + try { + serviceClass = (Class)Class.forName(className); + } catch (ClassNotFoundException ex) { + } + return startService(serviceClass); + } +``` + +系统调用SystemServerManager.startService()传入对应的服务,在startService()中根据传入的类名加载类文件,然后执行startService(serviceClass)方法,startService中使用加载的Class获取构造函数并创建对象,然后调用startService(service); + +```java +// startService中 +Constructor constructor = serviceClass.getConstructor(Context.class); +service = constructor.newInstance(mContext); +startService(service); +return service; + +public void startService(@NonNull final SystemService service) { + mServices.add(service); //1 + try { + service.onStart();//2 + } catch (RuntimeException ex) { + } +``` + +在startService()中,先将service注册到mServices中,然后调用service.onStart()方法启动服务; + +Android系统指执行到此后会使用ServiceManager启动一系列的服务,这些都是位置Android系统运行的核心功能,之后有时间会逐个分析,本次Android系统的启动过程及Zygote的启动过程就介绍完毕了! + +## Zygote(二):应用进程的启动过程 + +程序的启动是从进程启动开始的,换句话说只有程序进程启动后,程序才会加载和执行,在AMS启动程序时首先会判断当前进程是否启动,对未启动的进程会发送请求,Zygote在收到请求后创建新的进程; + +### Zygote监听客户端请求 + +由Zygote(一)—Android系统的启动过程知道,系统的启动会执行到ZygoteInit.main()方法; + +```java +public static void main(String argv[]) { + ZygoteServer zygoteServer = new ZygoteServer();//1 + zygoteServer.registerServerSocketFromEnv(socketName); + caller = zygoteServer.runSelectLoop(abiList); //2 +} +``` + +在main中创建并注册了服务端的Socket,然后执行runSelectLoop()循环等待AMS的请求创建进程,前一篇文章中跳过了这个部分,本片文章来分析下系统如何实现Socket通信的 + +- registerServerSocketFromEnv() + +```java +void registerServerSocketFromEnv(String socketName) { + if (mServerSocket == null) { + int fileDesc; + final String fullSocketName = ANDROID_SOCKET_PREFIX + socketName;//1 + try { + String env = System.getenv(fullSocketName);//2 + fileDesc = Integer.parseInt(env); + } + try { + FileDescriptor fd = new FileDescriptor();//3 + fd.setInt$(fileDesc); + mServerSocket = new LocalServerSocket(fd);//4 + mCloseSocketFd = true; + } catch (IOException ex) { + } + } +``` + +在registerServerSocketFromEnv中()出入的参数为进程名称,由上一篇文章知道传入的为ztgote,然后使用ANDROID_SOCKET_PREFIX拼接Socket名称,最终的名称为ANDROID_SOCKET_zygote,然后将fullSocketName转换为环境变量的值,注释3处创建文件描述符,然后根据文件描述符fd创建LocalServerSocket对象; + +```java +public LocalServerSocket(FileDescriptor fd) throws IOException + { + impl = new LocalSocketImpl(fd); + impl.listen(LISTEN_BACKLOG); + localAddress = impl.getSockAddress(); + } +``` + +LocalService使用select监听文件描述符,当文件描述符上出现新内容时会自动触发中断,然后中断处理函数中再次读取文件描述福符上的数据,从而获取信息; + +- zygoteServer.runSelectLoop() + +```java + Runnable runSelectLoop(String abiList) { + ArrayList fds = new ArrayList(); + ArrayList peers = new ArrayList(); +177 fds.add(mServerSocket.getFileDescriptor());//1 +178 peers.add(null); +179 +180 while (true) { //2 +181 StructPollfd[] pollFds = new StructPollfd[fds.size()]; +182 for (int i = 0; i < pollFds.length; ++i) { +183 pollFds[i] = new StructPollfd(); +184 pollFds[i].fd = fds.get(i); +185 pollFds[i].events = (short) POLLIN; +186 } +187 try { +188 Os.poll(pollFds, -1); +189 } catch (ErrnoException ex) { +190 throw new RuntimeException("poll failed", ex); +191 } +192 for (int i = pollFds.length - 1; i >= 0; --i) { +193 if ((pollFds[i].revents & POLLIN) == 0) { +194 continue; +195 } +197 if (i == 0) { // 3 +198 ZygoteConnection newPeer = acceptCommandPeer(abiList); +199 peers.add(newPeer); +200 fds.add(newPeer.getFileDesciptor()); +201 } else { +202 try { +203 ZygoteConnection connection = peers.get(i); +204 final Runnable command = connection.processOneCommand(this); // 4 +205 +206 if (mIsForkChild) { +213 return command; +214 } else { +223 if (connection.isClosedByPeer()) { +224 connection.closeSocket(); +225 peers.remove(i); +226 fds.remove(i); +227 } +228 } +229 } catch (Exception e) { +230 if (!mIsForkChild) { +241 ZygoteConnection conn = peers.remove(i); +242 conn.closeSocket(); +244 fds.remove(i); +245 } else { +250 throw e; +251 } +252 } finally { +256 mIsForkChild = false; +257 } +258 } +259 } +260 } +261 } +``` + +在runSelectLoop方法中,首先获取注册Socket的文件描述符fd,并将其保存在fds集合中,然后开启循环监听AMS的请求,在注释3处判断i ==0,若i=0表示Socket没有可用的链接需要重连,否则表示接收到AMS的请求信号,调用connection.processOneCommand(this)创建新的进程,创建完成后清除对应的peers集合和fds集合; + +### AMS发送创建进程请求 + +AMS会检查目标程序进程,如果进程未启动则调用startProcessLocked()方法启动进程 + +```java + int uid = app.uid; //1 + ...... +if (ArrayUtils.isEmpty(permGids)) { //2 + gids = new int[3]; + } else { + gids = new int[permGids.length + 3]; + System.arraycopy(permGids, 0, gids, 3, permGids.length); + } + gids[0] = UserHandle.getSharedAppGid(UserHandle.getAppId(uid)); + gids[1] = UserHandle.getCacheAppGid(UserHandle.getAppId(uid)); + gids[2] = UserHandle.getUserGid(UserHandle.getUserId(uid)); +..... +final String entryPoint = "android.app.ActivityThread"; //3 +return startProcessLocked(hostingType, hostingNameStr, entryPoint, app, uid, gids,runtimeFlags, mountExternal, seInfo, requiredAbi, instructionSet, invokeWith,startTime); //4 +``` + +startProcessLocked执行一下逻辑: + +1. 获取当前程序的uid +2. 对用户组gids创建并赋值 +3. entryPoint赋值为ActivityThread的路径,ActivityThread就是进程启动时要初始化的类 +4. 调用startProcessLocked()启动进程 + +在startProcessLocked()中又调用startProcess(),startProcess()中调用Process.start()方法,start中调用ZygoteProcess.startViaZygote()启动进程, + +- ZygoteProcess.startViaZygote() + +```java +private Process.ProcessStartResult startViaZygote(final String processClass, + ......String[] extraArgs)throws ZygoteStartFailedEx { + ArrayList argsForZygote = new ArrayList(); + argsForZygote.add("--runtime-args"); + argsForZygote.add("--setuid=" + uid); + argsForZygote.add("--setgid=" + gid); + argsForZygote.add("--runtime-flags=" + runtimeFlags); + +synchronized(mLock) { +return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote); +} +``` + +在startViaZygote中首先创建ArrayList集合,然后添加创建进程的启动参数,最后调用zygoteSendArgsAndGetResult()执行启动进程,在zygoteSendArgsAndGetResult的第一个参数中首先调用了openZygoteSocketIfNeeded()方法,它的作用其实就是连接Zygote 中服务端的Socket + +```java +private ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx { + if (primaryZygoteState == null || primaryZygoteState.isClosed()) { + try { + primaryZygoteState = ZygoteState.connect(mSocket); //1 + } catch (IOException ioe) { + throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe); + } + maybeSetApiBlacklistExemptions(primaryZygoteState, false); + maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState); + } + if (primaryZygoteState.matches(abi)) { //2 + return primaryZygoteState; + } + if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) { + try { + secondaryZygoteState = ZygoteState.connect(mSecondarySocket);//3 + } + maybeSetApiBlacklistExemptions(secondaryZygoteState, false); + maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState); + } + if (secondaryZygoteState.matches(abi)) { //4 + return secondaryZygoteState; + } + } +``` + +在openZygoteSocketIfNeeded()中ZygoteState.connect()连接Zygote进程中的Socket,连接成功后返回连接的主模式,用此主模式和传入的abi比较是否匹配,如果匹配则直接返回ZygoteState,否则连接zygote辅模式; + +- zygoteSendArgsAndGetResult() + +```java + private static Process.ProcessStartResult zygoteSendArgsAndGetResult( +281 ZygoteState zygoteState, ArrayList args) +282 throws ZygoteStartFailedEx { +283 try { +286 int sz = args.size(); +287 for (int i = 0; i < sz; i++) { +288 if (args.get(i).indexOf('\n') >= 0) { +289 throw new ZygoteStartFailedEx("embedded newlines not allowed"); +290 } +291 } +303 final BufferedWriter writer = zygoteState.writer; +304 final DataInputStream inputStream = zygoteState.inputStream; +306 writer.write(Integer.toString(args.size())); +307 writer.newLine(); +309 for (int i = 0; i < sz; i++) { +310 String arg = args.get(i); +311 writer.write(arg); +312 writer.newLine(); +313 } +315 writer.flush(); +323 result.pid = inputStream.readInt(); +324 result.usingWrapper = inputStream.readBoolean(); +326 if (result.pid < 0) { +327 throw new ZygoteStartFailedEx("fork() failed"); +328 } +329 return result; +330 } catch (IOException ex) { +331 zygoteState.close(); +332 throw new ZygoteStartFailedEx(ex); +333 } +334 } +``` + +在zygoteSendArgsAndGetResult中获取连接Socket返回的ZygoteState,利用ZygoteState内部的BufferedWriter将请求参数写入Zygote进程中; + +### Zygote接收信息并创建进程 + +由第一部分知道,在AMS发送请求后zygote进程会接收请求,并调用ZygoteConnection.processOneCommand()方法处理请求,在ZygoteInit.main()方法中有以下代码 + +```java + final Runnable caller; + caller = zygoteServer.runSelectLoop(abiList); + if (caller != null) { + caller.run(); + } +``` + +在runSelectLoop()中接收到AMS请求信息后,然后执行处理并返回Runnable对象并执行run()方法, + +```java + Runnable processOneCommand(ZygoteServer zygoteServer) { + String args[]; + Arguments parsedArgs = null; + FileDescriptor[] descriptors; + try { + args = readArgumentList(); //1 + descriptors = mSocket.getAncillaryFileDescriptors(); + } + parsedArgs = new Arguments(args); //2 + pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,parsedArgs.runtimeFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,parsedArgs.niceName, fdsToClose, fdsToIgnore, parsedArgs.startChildZygote,parsedArgs.instructionSet, parsedArgs.appDataDir); + if (pid == 0) { + zygoteServer.setForkChild(); + zygoteServer.closeServerSocket(); + IoUtils.closeQuietly(serverPipeFd); + serverPipeFd = null; + return handleChildProc(parsedArgs, descriptors, childPipeFd, + parsedArgs.startChildZygote); //3 + } else { + IoUtils.closeQuietly(childPipeFd); + childPipeFd = null; + handleParentProc(pid, descriptors, serverPipeFd); + return null; + } + + } +``` + +在processOneCommand()中首先调用readArgumentList读取参数数组,然后将数组封装在Arguments对象中,调用Zygote.forkAndSpecialize()方法fork子进程,子进程创建成功后调用handleChildProc()初始化进程 + +```java +private Runnable handleChildProc(Arguments parsedArgs, FileDescriptor[] descriptors,FileDescriptor pipeFd, boolean isZygote) { + if (parsedArgs.niceName != null) { + Process.setArgV0(parsedArgs.niceName); + } + if (!isZygote) { + return ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs,null /* classLoader */); + } else { + return ZygoteInit.childZygoteInit(parsedArgs.targetSdkVersion, + parsedArgs.remainingArgs, null /* classLoader */); + } + } + } +``` + +在handleChildProc调用ZygoteInit.zygoteInit(),关于ZygoteInit.zygoteInit()方法的内容参考[Android进阶知识树——Android系统的启动过程](https://blog.csdn.net/Alexwll/article/details/100133524),其最终会使用反射调用ActivityThread.main()方法,程序进入进程初始化,关于ActivityThread中的操作这里不做分析,相信Android开发者应该了解; + +### 启动Binder线程池 + +本篇文章和上一篇[Android进阶知识树——Android系统的启动过程](https://blog.csdn.net/Alexwll/article/details/100133524)中都提到Binder线程池的创建,但都没有详细介绍,这里补充一下,程序在启动进程后会调用ZygoteInit.zygoteInit()方法,zygoteInit中调用本地方法nativeZygoteInit(),在ZygoteInit中声明了nativeZygoteInit方法; + +```java +public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) { + RuntimeInit.commonInit(); + ZygoteInit.nativeZygoteInit();// + return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader); + } + +private static final native void nativeZygoteInit(); +``` + +很明显nativeZygoteInit()是JNI方法(关于JNI见另一篇文章[Android进阶知识树——JNI和So库开发](https://blog.csdn.net/Alexwll/article/details/99703403)),他在AndroidRuntime中完成方法动态注册,nativeZygoteInit中对应c文件中com_android_internal_os_ZygoteInit_nativeZygoteInit()方法 + +```java +int register_com_android_internal_os_ZygoteInit_nativeZygoteInit(JNIEnv* env) +{ + const JNINativeMethod methods[] = { + { "nativeZygoteInit", "()V", + (void*) com_android_internal_os_ZygoteInit_nativeZygoteInit }, + }; + return jniRegisterNativeMethods(env, "com/android/internal/os/ZygoteInit", + methods, NELEM(methods)); +} + +static void com_android_internal_os_ZygoteInit_nativeZygoteInit(JNIEnv* env, jobject clazz) +{ + gCurRuntime->onZygoteInit(); +} +``` + +在com_android_internal_os_ZygoteInit_nativeZygoteInit()方法中调用gCurRuntime.onZygoteInit(),这里的gCurRuntime是AppRuntime对象,它继承AndroidRuntime,在AndroidRuntime的构造函数中被初始化,AppRuntime类在app_main中实现 + +```java +class AppRuntime : public AndroidRuntime +{ +virtual void onZygoteInit() { + sp proc = ProcessState::self(); + proc->startThreadPool(); + } +} +``` + +在onZygoteInit()中调用ProcessState类的startThreadPool(),startThreadPool()中调用 + +```java +void ProcessState::spawnPooledThread(bool isMain){ + if (mThreadPoolStarted) { + String8 name = makeBinderThreadName(); + sp t = new PoolThread(isMain); + t->run(name.string()); + } +} +``` + +在spawnPooledThread()中使用makeBinderThreadName()生成线程名称,然后创建PoolThread线程并执行线程 + +```java +class PoolThread : public Thread{ +protected: + virtual bool threadLoop() + { + IPCThreadState::self()->joinThreadPool(mIsMain); + return false; + } + const bool mIsMain; +}; +``` + +PoolThread继承Thread类,启动PoolThread对象就创建了一个新的线程,在PoolThread的threadLoop()方法中调用IPCThreadState的joinThreadPool()方法将创建的线程加入Binder线程吃中,那么新创建的应用进程就支持Binder进程通行了; + +总结一下整个进程启动的过程: + +1. AMS判断当前进程是否启动,对未启动的进程发送请求 +2. 首先根据程序的pid设置并赋值用户组gids +3. 将entryPoint赋值为ActivityThread的路径,然后开始执行进程启动 +4. 在与zygote交互中,首先根据设置的abi连接zygote进程中的socket,并判断是匹配主模式还是辅模式,连接成功后返回zygoteState对象 +5. 使用zygoteState将请求参数写入zygote的Socket中,zygote进程中读取请求的信息保存在数组中 +6. 使用参数数组创建Argument对象,并fork出程序进程,从而启动程序进程 +7. 进程启动后调用ZygotezInit.zygoteInit()方法,内部初始化Binder线程池实现进程通信,然后反射获取ActivityThread.main()方法,完成新进程的初始化 + + + +# AMS + +## AMS源码分析(一)Activity生命周期管理 + +AMS(ActivityManagerService)是Activity管理的核心组件,提供了Activity的启动、生命周期管理、栈管理等功能,熟悉AMS会对我们认识Activity的工作原理有很大的帮助。当前比较成熟的插件化技术,也是通过对Activity启动流程中的重要组件(如Instrumentation或主线程Handler等)进行Hook完成的,掌握AMS对我们学习插件化技术也有很大的帮助 + +AMS中内容很多,对它的分析也不可能面面俱到,我期望从Activity的启动、Activity消息回传(onActivityResult)、Activity栈管理、AMS与WMS和PMS的协同工作方面入手,希望本系列文章完成后可以对AMS有一个更新的认识 + +### Activity的生命周期 + +#### 一个Activity从启动到销毁所经历的周期 + +onCreate -> onStart -> onResume -> onPause -> onStop -> onDestory + +这属于Android最基础的知识,就不再赘述了 + +#### 从一个Activity启动另一个Activity的生命周期 + +现在有两个Activity,分别是AActivity和BActivity,如果从AActivity跳转到BActivity,那么它们的生命周期会是下面这个样子: + +AActivity#onPause -> BActivity#onCreate -> BActivity#onStart -> BActivity#onResume -> AActivity#onStop + +如下图: + +![img](https:////upload-images.jianshu.io/upload_images/3112838-c98340b7602caebe.image?imageMogr2/auto-orient/strip|imageView2/2/w/248/format/webp) + +而从BActivity返回时,它们的生命周期是这样的: + +BActivity#onPause -> AActivity#onStart -> AActivity#onResume -> BActivity#onStop -> BActivity#onDestroy + +![img](https:////upload-images.jianshu.io/upload_images/3112838-e0000f3c12fd415e.image?imageMogr2/auto-orient/strip|imageView2/2/w/317/format/webp) + +为什么是这样呢?下面我们从源码的角度去分析一下 + +### 源码分析 + +在做源码分析之前,我们要先理解两个概念 + +- Android Binder IPC机制 +- Hander idleHandler机制 + +#### Binder + +Binder是Android跨进程通信的核心,但不是本文的重点,这里不多做讲解,感兴趣的朋友可以看一下我之前写过的Binder系列文章:[Binder系列](https://www.jianshu.com/p/275bc9a53342) + +在本篇文章中,我们只需要知道每当看到类似`mRemote.transact` + 这种调用时,既是跨进程通信的开始 + +#### IdleHandler + +当消息队列空闲时会执行IdelHandler的queueIdle()方法,该方法返回一个boolean值, + 如果为false则执行完毕之后移除这条消息, + 如果为true则保留,等到下次空闲时会再次执行 + +也就是说,IdleHandler是Handler机制中MessageQueue空闲时才会执行的一种特殊Handler + +#### Activity在AMS中的标识 + +每一个Activity实例在AMS中都会对应一个ActivityRecord,ActivityRecord对象是在Activity启动过程中创建的,每个ActivityRecord中又会有一个ProcessRecord标记Activity所在的进程 + +而在APP进程中,在Activity启动时,也会创建一个ActivityClientRecord,与AMS中的ActivityRecord遥相呼应。 + +Activity、ActivityClientRecord、ActivityRecord互相联系的纽带是一个`token`对象,它继承于`IApplicationToken.Stub`,这是一个Binder对象,在Activity、ActivityClientRecord、ActivityRecord中均有一份。 + +Activity、ActivityClientRecord、ActivityRecord之间的关系图大致如下---注意区分所在进程: + +![img](https:////upload-images.jianshu.io/upload_images/3112838-0ff114d27e1a8d37.image?imageMogr2/auto-orient/strip|imageView2/2/w/623/format/webp) + +#### 启动过程时序图 + +黄色为APP进程,蓝色为AMS进程 + +![img](https:////upload-images.jianshu.io/upload_images/3112838-f98e18517de9cb28.image?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) + +#### 从AActivity跳转BActivity的生命周期分析 + +AMS的逻辑太多,本文只针对Activity声明周期相关的代码,其他相关逻辑后面几篇文章继续分析 + +从Activity开始 + + + +```kotlin +public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, + @Nullable Bundle options) { + if (mParent == null) { + options = transferSpringboardActivityOptions(options); + Instrumentation.ActivityResult ar = + mInstrumentation.execStartActivity( + this, mMainThread.getApplicationThread(), mToken, this, + intent, requestCode, options); + + } else { + + } +} +``` + +Instrumentation#execStartActivity方法: + + + +```csharp +public ActivityResult execStartActivity( + Context who, IBinder contextThread, IBinder token, Activity target, + Intent intent, int requestCode, Bundle options) { + ... + try { + intent.migrateExtraStreamToClipData(); + intent.prepareToLeaveProcess(who); + int result = ActivityManager.getService() + .startActivity(whoThread, who.getBasePackageName(), intent, + intent.resolveTypeIfNeeded(who.getContentResolver()), + token, target != null ? target.mEmbeddedID : null, + requestCode, 0, null, options); + checkStartActivityResult(result, intent); + } catch (RemoteException e) { + throw new RuntimeException("Failure from system", e); + } + return null; +} +``` + +重要入参及含义: + +- who: 启动源Activity +- contextThread : 宿主Activity的ApplicationThread +- token:Activity的身份标识 -> Activity#mToken -> ActivityClientRecord#token -> ActivityRecord#appToken -> IApplicationToken.Stub +- target: 哪个activity调用的start方法,因此这个Activity会接收任何的返回结果,如果start调用不是从Activity中发起的则有可能为null +- intent: 启动Activity时的intent +- requestCoode: 启动Activity时的requestCode +- options: 启动Activity的选项 + +注意token参数,本文将重点追踪它的变化,这个参数非常重要,后面会有很多地方用到 + +继续走逻辑,在这里进行了IPC调用,从APP进程转入AMS进程执行,`ActivityManager.getService() .startActivity`调用在AMS进程对应的是`ActivityManagerService#startActivity`,我们看一下 + +ActivityManagerService#startActivity 经过一系列的调用后,会走到 ActivityStarter#startActivityMayWait + + + +```dart +final int startActivityMayWait(IApplicationThread caller, int callingUid, + int requestRealCallingPid, int requestRealCallingUid, + String callingPackage, Intent intent, String resolvedType, + IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, + IBinder resultTo, String resultWho, int requestCode, int startFlags, + ProfilerInfo profilerInfo, WaitResult outResult, + Configuration globalConfig, Bundle bOptions, boolean ignoreTargetSecurity, int userId, + TaskRecord inTask, String reason) { + + ... + + // Collect information about the target of the Intent. + // zhangyulong 解析Intent中的Activity信息 + ActivityInfo aInfo = mSupervisor.resolveActivity(intent, rInfo, startFlags, profilerInfo); + + ... + + // zhangyulong 声明一个ActivityRecord数组 + final ActivityRecord[] outRecord = new ActivityRecord[1]; + int res = startActivityLocked(caller, intent, ephemeralIntent, resolvedType, + aInfo, rInfo, voiceSession, voiceInteractor, + resultTo, resultWho, requestCode, callingPid, + callingUid, callingPackage, realCallingPid, realCallingUid, startFlags, + options, ignoreTargetSecurity, componentSpecified, outRecord, inTask, + reason); + + Binder.restoreCallingIdentity(origId); + + ... + return res; + } +} +``` + +这里进行的工作是解析Intent信息,并继续调用`startActivityLocked`,注意入参中的`resultTo`就是从App进程传入的token,依旧是透传给`startActivityLocked` + + + +```dart +int startActivityLocked(IApplicationThread caller, Intent intent, Intent ephemeralIntent, + String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo, + IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, + IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid, + String callingPackage, int realCallingPid, int realCallingUid, int startFlags, + ActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified, + ActivityRecord[] outActivity, TaskRecord inTask, String reason) { + + if (TextUtils.isEmpty(reason)) { + throw new IllegalArgumentException("Need to specify a reason."); + } + mLastStartReason = reason; + mLastStartActivityTimeMs = System.currentTimeMillis(); + mLastStartActivityRecord[0] = null; + + mLastStartActivityResult = startActivity(caller, intent, ephemeralIntent, resolvedType, + aInfo, rInfo, voiceSession, voiceInteractor, resultTo, resultWho, requestCode, + callingPid, callingUid, callingPackage, realCallingPid, realCallingUid, startFlags, + options, ignoreTargetSecurity, componentSpecified, mLastStartActivityRecord, + inTask); + + if (outActivity != null) { + // mLastStartActivityRecord[0] is set in the call to startActivity above. + outActivity[0] = mLastStartActivityRecord[0]; + } + + // Aborted results are treated as successes externally, but we must track them internally. + return mLastStartActivityResult != START_ABORTED ? mLastStartActivityResult : START_SUCCESS; +} +``` + +这个方法也没做太多事情,将参数透传给`startActivity`继续执行 + + + +```java +private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent, + String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo, + IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, + IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid, + String callingPackage, int realCallingPid, int realCallingUid, int startFlags, + ActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified, + ActivityRecord[] outActivity, TaskRecord inTask) { + int err = ActivityManager.START_SUCCESS; + // Pull the optional Ephemeral Installer-only bundle out of the options early. + final Bundle verificationBundle + = options != null ? options.popAppVerificationBundle() : null; + + ProcessRecord callerApp = null; + if (caller != null) { + callerApp = mService.getRecordForAppLocked(caller); + if (callerApp != null) { + callingPid = callerApp.pid; + callingUid = callerApp.info.uid; + } else { + Slog.w(TAG, "Unable to find app for caller " + caller + + " (pid=" + callingPid + ") when starting: " + + intent.toString()); + err = ActivityManager.START_PERMISSION_DENIED; + } + } + + final int userId = aInfo != null ? UserHandle.getUserId(aInfo.applicationInfo.uid) : 0; + + if (err == ActivityManager.START_SUCCESS) { + Slog.i(TAG, "START u" + userId + " {" + intent.toShortString(true, true, true, false) + + "} from uid " + callingUid); + } + + ActivityRecord sourceRecord = null; + ActivityRecord resultRecord = null; + if (resultTo != null) { + // 通过resultTo在寻找记录在AMS中记录的ActivityRecord,这个ActivityRecord和启动Activity的源Activity对应 + sourceRecord = mSupervisor.isInAnyStackLocked(resultTo); + if (DEBUG_RESULTS) Slog.v(TAG_RESULTS, + "Will send result to " + resultTo + " " + sourceRecord); + if (sourceRecord != null) { + if (requestCode >= 0 && !sourceRecord.finishing) { + resultRecord = sourceRecord; + } + } + } + + final int launchFlags = intent.getFlags(); + + // 新建一条目标Activity的ActivityRecord,这条ActivityRecord和即将打开的Activity实例对应 + ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid, + callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(), + resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null, + mSupervisor, options, sourceRecord); + if (outActivity != null) { + outActivity[0] = r; + } + return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags, true, + options, inTask, outActivity); +} +``` + +记的我们在分析源码之前所画的ActivityRecord、ActivityClientRecord、Activity之间的关系图么?这里就派上用场了,入参`resultTo`就是启动Activity时传递的token,那么,在AMS中,必然会有一条ActivityRecord与之对应,通过`mSupervisor.isInAnyStackLocked(resultTo)`找到与AActivity对应的ActivityRecord。 + +这个方法还有一个重要的任务就是新建一条ActivityRecord记录,这条记录和即将打开的Activity,即BActivity对应。完成这些工作后,继续执行`startActivity`。 + +`startActivity`继续调用了`startActivityUnchecked`,这个方法最重要的功能是对Acitivity栈和Activity启动模式进行处理,篇幅很长,逻辑复杂,但是不是本篇的重点,所以我们后面的文章再着重分析,现在只关注Activity启动流程相关的部分 + + + +```java +private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord, + IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, + int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask, + ActivityRecord[] outActivity) { + + ... + + if (mDoResume) { + final ActivityRecord topTaskActivity = + mStartActivity.getTask().topRunningActivityLocked(); + if (!mTargetStack.isFocusable() + || (topTaskActivity != null && topTaskActivity.mTaskOverlay + && mStartActivity != topTaskActivity)) { + mTargetStack.ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS); + mWindowManager.executeAppTransition(); + } else { + if (mTargetStack.isFocusable() && !mSupervisor.isFocusedStack(mTargetStack)) { + mTargetStack.moveToFront("startActivityUnchecked"); + } + + mSupervisor.resumeFocusedStackTopActivityLocked(mTargetStack, mStartActivity, + mOptions); + } + } else { + mTargetStack.addRecentActivityLocked(mStartActivity); + } + ... + return START_SUCCESS; +} +``` + +##### AActivity#onPause + +注意看`mSupervisor.resumeFocusedStackTopActivityLocked(mTargetStack, mStartActivity, mOptions)`,这里,mStartActivity是即将启动的ActivityRecord,mTargetStack是在本方法中计算得到的它所在的ActivityStack,继续看`resumeFocusedStackTopActivityLocked` + +还记得在开篇时讲到的从AActivity跳转到BActivity的生命周期过程么?不记得没关系,复习一下先: + AActivity#onPause -> BActivity#onCreate -> BActivity#onStart -> BActivity#onResume -> AActivity#onStop, + 在`resumeFocusedStackTopActivityLocked`这个方法我们将接触到这个过程中的第一个声明周期方法,即`AActivity#onPause`,而且本身这个方法的调用过程也特别复杂,需要重点解析一下,先看一下代码: + + + +```java +private boolean resumeTopActivityInnerLocked(ActivityRecord prev, ActivityOptions options) { + if (!mService.mBooting && !mService.mBooted) { + // Not ready yet! + return false; + } + + // Find the next top-most activity to resume in this stack that is not finishing and is + // focusable. If it is not focusable, we will fall into the case below to resume the + // top activity in the next focusable task. + // 获取栈顶正在聚焦的Activity + final ActivityRecord next = topRunningActivityLocked(true /* focusableOnly */); + + final boolean hasRunningActivity = next != null; + + // TODO: Maybe this entire condition can get removed? + if (hasRunningActivity && getDisplay() == null) { + // zhangyulong 异常情况,有正在运行的activity但getDisplay为空 + return false; + } + + mStackSupervisor.cancelInitializingActivities(); + + // Remember how we'll process this pause/resume situation, and ensure + // that the state is reset however we wind up proceeding. + final boolean userLeaving = mStackSupervisor.mUserLeaving; + mStackSupervisor.mUserLeaving = false; + + if (!hasRunningActivity) { + // There are no activities left in the stack, let's look somewhere else. + // zhangyulong task 退干净了,恢复其他的task + return resumeTopActivityInNextFocusableStack(prev, options, "noMoreActivities"); + } + + next.delayedResume = false; + + ... + // Activity栈中有需要Pause操作的Activity + boolean pausing = mStackSupervisor.pauseBackStacks(userLeaving, next, false); + if (mResumedActivity != null) { + // 暂停当前正在显示的Activity + pausing |= startPausingLocked(userLeaving, false, next, false); + } + if (pausing && !resumeWhilePausing) { + if (next.app != null && next.app.thread != null) { + mService.updateLruProcessLocked(next.app, true, null); + } + if (DEBUG_STACK) mStackSupervisor.validateTopActivitiesLocked(); + return true; + } + + ... + + ActivityStack lastStack = mStackSupervisor.getLastStack(); + if (next.app != null && next.app.thread != null) { + ... + } else { + // Whoops, need to restart this activity! + if (!next.hasBeenLaunched) { + next.hasBeenLaunched = true; + } else { + if (SHOW_APP_STARTING_PREVIEW) { + next.showStartingWindow(null /* prev */, false /* newTask */, + false /* taskSwich */); + } + if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "Restarting: " + next); + } + if (DEBUG_STATES) Slog.d(TAG_STATES, "resumeTopActivityLocked: Restarting " + next); + mStackSupervisor.startSpecificActivityLocked(next, true, true); + } + + if (DEBUG_STACK) mStackSupervisor.validateTopActivitiesLocked(); + return true; +} +``` + +这个方法会执行两次,第一次进入时,`final ActivityRecord next = topRunningActivityLocked(true /* focusableOnly */);`获取到的是栈顶焦点Activity,在执行该方法之前,我们已经将目标ActivityRecord入栈,因此,这里获取的是要打开的目标ActivityRecord,`mStackSupervisor.pauseBackStacks(userLeaving, next, false);`是判断ActivityStack中是否有需要pause的Activity,如果有,说明还有活动的Activity没有Pause,我们要先执行Pause操作,通过执行`startPausingLocked(userLeaving, false, next, false);`将当前未Pause的Activity进行Pause,在执行完毕后方法直接return了。先进去看一下`startPausingLocked`: + + + +```java +final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping, + ActivityRecord resuming, boolean pauseImmediately) { + + // 将mResumedActivity赋值prev,并将mResumedActivity置空 + ActivityRecord prev = mResumedActivity; + mResumedActivity = null; + mPausingActivity = prev; + + + if (prev.app != null && prev.app.thread != null) { + if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Enqueueing pending pause: " + prev); + try { + EventLog.writeEvent(EventLogTags.AM_PAUSE_ACTIVITY, + prev.userId, System.identityHashCode(prev), + prev.shortComponentName); + mService.updateUsageStats(prev, false); + // 开始进行Pause操作 + prev.app.thread.schedulePauseActivity(prev.appToken, prev.finishing, + userLeaving, prev.configChangeFlags, pauseImmediately); + } catch (Exception e) { + // Ignore exception, if process died other code will cleanup. + Slog.w(TAG, "Exception thrown during pause", e); + mPausingActivity = null; + mLastPausedActivity = null; + mLastNoHistoryActivity = null; + } + } else { + mPausingActivity = null; + mLastPausedActivity = null; + mLastNoHistoryActivity = null; + } + + if (mPausingActivity != null) { + if (!uiSleeping) { + prev.pauseKeyDispatchingLocked(); + } else if (DEBUG_PAUSE) { + Slog.v(TAG_PAUSE, "Key dispatch not paused for screen off"); + } + + if (pauseImmediately) { + completePauseLocked(false, resuming); + return false; + + } else { + // 延时500ms处理pause后续操作 + schedulePauseTimeout(prev); + return true; + } + + } else { + if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Activity not running, resuming next."); + if (resuming == null) { + mStackSupervisor.resumeFocusedStackTopActivityLocked(); + } + return false; + } +} +``` + +`mResumedActivity`就是将被Pause的Activity,通过`prev.app.thread.schedulePauseActivity(prev.appToken, prev.finishing, userLeaving, prev.configChangeFlags, pauseImmediately);`对其进行Pause,这里实际是一个异步操作,因此,在方法的末端,添加了一个500ms的延时消息防止Pause操作timeout,当然,这个延时消息不一定会执行,如果pause操作在500ms以内完成会将这个消息取消。如果熟悉Activity启动流程的话,我们就会知道`schedulePauseActivity`最终一定会执行到`ApplicationThread#handlePauseActivity`,这个ApplicationThread在本文章的场景中是AActivity的ApplicationThread,看下代码: + + + +```java +private void handlePauseActivity(IBinder token, boolean finished, + boolean userLeaving, int configChanges, boolean dontReport, int seq) { + ActivityClientRecord r = mActivities.get(token); + if (DEBUG_ORDER) Slog.d(TAG, "handlePauseActivity " + r + ", seq: " + seq); + if (!checkAndUpdateLifecycleSeq(seq, r, "pauseActivity")) { + return; + } + if (r != null) { + //Slog.v(TAG, "userLeaving=" + userLeaving + " handling pause of " + r); + if (userLeaving) { + performUserLeavingActivity(r); + } + + r.activity.mConfigChangeFlags |= configChanges; + // 真正调用Activity#onPause + performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity"); + + // Make sure any pending writes are now committed. + if (r.isPreHoneycomb()) { + QueuedWork.waitToFinish(); + } + + // Tell the activity manager we have paused. + if (!dontReport) { + try { + // 通知AMS Activity已完成Pause + ActivityManager.getService().activityPaused(token); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + mSomeActivitiesChanged = true; + } +} +``` + +其中,`performPauseActivity`最终会调用AActivity的onPause,继续向下执行会通过`ActivityManager.getService().activityPaused(token)`通知AMS进程Activity已完成Pause, + + + +```java +// ActivityManagerService +public final void activityPaused(IBinder token) { + final long origId = Binder.clearCallingIdentity(); + synchronized(this) { + ActivityStack stack = ActivityRecord.getStackLocked(token); + if (stack != null) { + stack.activityPausedLocked(token, false); + } + } + Binder.restoreCallingIdentity(origId); + } +``` + +继续看`ActivityStack#activityPausedLocked` + + + +```java +final void activityPausedLocked(IBinder token, boolean timeout) { + + ... + + final ActivityRecord r = isInStackLocked(token); + if (r != null) { + // 取消Pause的延时等待消息消息 + mHandler.removeMessages(PAUSE_TIMEOUT_MSG, r); + if (mPausingActivity == r) { + if (DEBUG_STATES) Slog.v(TAG_STATES, "Moving to PAUSED: " + r + + (timeout ? " (due to timeout)" : " (pause complete)")); + mService.mWindowManager.deferSurfaceLayout(); + try { + // 完成Pause的后续操作 + completePauseLocked(true /* resumeNext */, null /* resumingActivity */); + } finally { + mService.mWindowManager.continueSurfaceLayout(); + } + return; + } + + .... +} +``` + +比较重要的两个操作: + +- 取消Pause的延时等待消息消息 +- 完成Pause的后续操作 + 完成Pause后续操作是在`ActivityStack#completePauseLocked`中 + + + +```java +private void completePauseLocked(boolean resumeNext, ActivityRecord resuming) { + ActivityRecord prev = mPausingActivity; + ... + if (prev != null) { + ... + // 将已被Pause的Activity加入到mStackSupervisor.mStoppingActivities列表中 + addToStopping(prev, true /* scheduleIdle */, false /* idleDelayed */); + ... + } + if (resumeNext) { + final ActivityStack topStack = mStackSupervisor.getFocusedStack(); + if (!topStack.shouldSleepOrShutDownActivities()) { + // Resume栈顶Activity, prev是当前在pause的 + mStackSupervisor.resumeFocusedStackTopActivityLocked(topStack, prev, null); + } else { + checkReadyForSleep(); + ActivityRecord top = topStack.topRunningActivityLocked(); + if (top == null || (prev != null && top != prev)) { + mStackSupervisor.resumeFocusedStackTopActivityLocked(); + } + } + } + + ... +} +``` + +重点关注`mStackSupervisor.resumeFocusedStackTopActivityLocked(topStack, prev, null);` + 兜兜转转,又回到了原点,我们即将第二次调用`ActivityStack#resumeTopActivityInnerLocked`,这次跟上一次又有所不同,因为已经完成活跃Activity的pause,因此,判断是否需要Pause的分支语句就不会执行,继续向下执行到`mStackSupervisor.startSpecificActivityLocked(next, true, true);`,这个方法还有一个重要的事情就是将已被Pause的Activity加入到mStackSupervisor.mStoppingActivities的列表中,这是一个ArrayList,至于什么时候用,后面会讲到 + +##### BActivity#onCreate + +讲到这里,我们只是刚完成了AActivity的Pause,有些朋友肯定已经着急了,那得分析到啥时候啊,快了快了,后面就很快了,加快进度,接着看`ActivityStackSupervisor#startSpecificActivityLocked` + + + +```java +void startSpecificActivityLocked(ActivityRecord r, + boolean andResume, boolean checkConfig) { + // 获取目标Activity的进程信息 + ProcessRecord app = mService.getProcessRecordLocked(r.processName, + r.info.applicationInfo.uid, true); + + r.getStack().setLaunchTime(r); + + if (app != null && app.thread != null) { + try { + if ((r.info.flags&ActivityInfo.FLAG_MULTIPROCESS) == 0 + || !"android".equals(r.info.packageName)) { + app.addPackage(r.info.packageName, r.info.applicationInfo.versionCode, + mService.mProcessStats); + } + // 启动Activity + realStartActivityLocked(r, app, andResume, checkConfig); + return; + } catch (RemoteException e) { + Slog.w(TAG, "Exception when starting activity " + + r.intent.getComponent().flattenToShortString(), e); + } + + // If a dead object exception was thrown -- fall through to + // restart the application. + } + // 如果没有进程信息则启动新的进程 + mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0, + "activity", r.intent.getComponent(), false, false, true); +} +``` + +这个方法的主要工作是给ActivityRecord分配进程,如果没有进程,则启动新的进程,从AActivity跳BActivity是同一个进程,因此会继续执行`realStartActivityLocked(r, app, andResume, checkConfig)`: + + + +```java +final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app, + boolean andResume, boolean checkConfig) throws RemoteException { + + ... + app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken, + System.identityHashCode(r), r.info, + + ... + return true; +} +``` + +方法比较长,掐头去尾只看`app.thread.scheduleLaunchActivity`,从这里开始进入App进程执行,`scheduleLaunchActivity`方法在APP进程对应的是`ApplicationThread#scheduleLaunchActivity`: + + + +```java +public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident, + ActivityInfo info, Configuration curConfig, Configuration overrideConfig, + CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor, + int procState, Bundle state, PersistableBundle persistentState, + List pendingResults, List pendingNewIntents, + boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) { + + updateProcessState(procState, false); + + ActivityClientRecord r = new ActivityClientRecord(); + + r.token = token; + r.ident = ident; + r.intent = intent; + r.referrer = referrer; + r.voiceInteractor = voiceInteractor; + r.activityInfo = info; + r.compatInfo = compatInfo; + r.state = state; + r.persistentState = persistentState; + + r.pendingResults = pendingResults; + r.pendingIntents = pendingNewIntents; + + r.startsNotResumed = notResumed; + r.isForward = isForward; + + r.profilerInfo = profilerInfo; + + r.overrideConfig = overrideConfig; + updatePendingConfiguration(curConfig); + + sendMessage(H.LAUNCH_ACTIVITY, r); +} +``` + +在这里创建了`ActivityClientRecord`,因此ActivityClientRecord和AMS中的ActivityRecord就对应起来了。向主线程发送`H.LAUNCH_ACTIVITY`消息,消息回调后执行handleLaunchActivity: + + + +```csharp +private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) { + // If we are getting ready to gc after going to the background, well + // we are back active so skip it. + ... + + Activity a = performLaunchActivity(r, customIntent); + + if (a != null) { + .... + handleResumeActivity(r.token, false, r.isForward, + !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason); + ... + } +.... +} +``` + +这里调用performLaunchActivity: + + + +```dart +private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { + // 创建Context + ContextImpl appContext = createBaseContextForActivity(r); + Activity activity = null; + try { + // 通过ClassLoader创建Activity实例 + java.lang.ClassLoader cl = appContext.getClassLoader(); + activity = mInstrumentation.newActivity( + cl, component.getClassName(), r.intent); + StrictMode.incrementExpectedActivityCount(activity.getClass()); + r.intent.setExtrasClassLoader(cl); + r.intent.prepareToEnterProcess(); + if (r.state != null) { + r.state.setClassLoader(cl); + } + } catch (Exception e) { + if (!mInstrumentation.onException(activity, e)) { + throw new RuntimeException( + "Unable to instantiate activity " + component + + ": " + e.toString(), e); + } + } + + try { + Application app = r.packageInfo.makeApplication(false, mInstrumentation); + + + + if (activity != null) { + //读取Activity信息 + CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager()); + Configuration config = new Configuration(mCompatConfiguration); + if (r.overrideConfig != null) { + config.updateFrom(r.overrideConfig); + } + if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity " + + r.activityInfo.name + " with config " + config); + Window window = null; + if (r.mPendingRemoveWindow != null && r.mPreserveWindow) { + window = r.mPendingRemoveWindow; + r.mPendingRemoveWindow = null; + r.mPendingRemoveWindowManager = null; + } + appContext.setOuterContext(activity); + // 执行Actiivity attach方法 + activity.attach(appContext, this, getInstrumentation(), r.token, + r.ident, app, r.intent, r.activityInfo, title, r.parent, + r.embeddedID, r.lastNonConfigurationInstances, config, + r.referrer, r.voiceInteractor, window, r.configCallback); + + if (customIntent != null) { + activity.mIntent = customIntent; + } + r.lastNonConfigurationInstances = null; + checkAndBlockForNetworkAccess(); + activity.mStartedActivity = false; + int theme = r.activityInfo.getThemeResource(); + if (theme != 0) { + activity.setTheme(theme); + } + + activity.mCalled = false; + // Activity onCreate执行 + if (r.isPersistable()) { + mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState); + } else { + mInstrumentation.callActivityOnCreate(activity, r.state); + } + if (!activity.mCalled) { + throw new SuperNotCalledException( + "Activity " + r.intent.getComponent().toShortString() + + " did not call through to super.onCreate()"); + } + r.activity = activity; + r.stopped = true; + if (!r.activity.mFinished) { + // Activity onStart执行 + activity.performStart(); + r.stopped = false; + } + if (!r.activity.mFinished) { + if (r.isPersistable()) { + if (r.state != null || r.persistentState != null) { + mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state, + r.persistentState); + } + } else if (r.state != null) { + mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state); + } + } + + } + r.paused = true; + + mActivities.put(r.token, r); + + } catch (SuperNotCalledException e) { + throw e; + + } catch (Exception e) { + + } + + return activity; +} +``` + +这个方法是Activity真正创建实例且onCreate执行的地方,通过调用Activity#attach方法,ActivityClientRecord的token就和Activity联系起来了 + +##### BActivity#onStart + +onCreate执行完成后,直接调用了`activity.performStart();`去执行onStart,因此我们可以得出结论:在Activity的`onCreate`和`onStart`方法中去处理逻辑,实际没有特别大的区别 + +##### BActivity#onResume + +`performLaunchActivity`调用有返回值,即创建的Activity,如果返回值不为空,则说明创建Activity成功,继续执行`handleResumeActivity` + + + +```java +final void handleResumeActivity(IBinder token, + boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) { + ActivityClientRecord r = mActivities.get(token); + if (!checkAndUpdateLifecycleSeq(seq, r, "resumeActivity")) { + return; + } + ... + // 执行onResume + r = performResumeActivity(token, clearHide, reason); + ... + + if (r != null) { + final Activity a = r.activity; + + // 向messageQueue发送一个Idle消息 + if (!r.onlyLocalRequest) { + r.nextIdle = mNewActivities; + mNewActivities = r; + if (localLOGV) Slog.v( + TAG, "Scheduling idle handler for " + r); + Looper.myQueue().addIdleHandler(new Idler()); + } + r.onlyLocalRequest = false; + + // 通知AMS完成了Resume操作 + if (reallyResume) { + try { + ActivityManager.getService().activityResumed(token); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + } + ... +} +``` + +`performResumeActivity`是Activity的onResume执行的地方 + +##### AActivity#onStop + +在执行完成后向住现成的MessageQueue发送了一个idle消息,这个很重要,具体是做什么的呢,我们进去看一下Idler的实现: + + + +```java +private class Idler implements MessageQueue.IdleHandler { + @Override + public final boolean queueIdle() { + ActivityClientRecord a = mNewActivities; + boolean stopProfiling = false; + if (mBoundApplication != null && mProfiler.profileFd != null + && mProfiler.autoStopProfiler) { + stopProfiling = true; + } + if (a != null) { + mNewActivities = null; + IActivityManager am = ActivityManager.getService(); + ActivityClientRecord prev; + do { + if (localLOGV) Slog.v( + TAG, "Reporting idle of " + a + + " finished=" + + (a.activity != null && a.activity.mFinished)); + if (a.activity != null && !a.activity.mFinished) { + try { + // 调用ActivityManagerService#activityIdle + am.activityIdle(a.token, a.createdConfig, stopProfiling); + a.createdConfig = null; + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + prev = a; + a = a.nextIdle; + prev.nextIdle = null; + } while (a != null); + } + if (stopProfiling) { + mProfiler.stopProfiling(); + } + ensureJitEnabled(); + return false; + } +} +``` + +看`am.activityIdle(a.token, a.createdConfig, stopProfiling)`这里,这种调用在本文中出现过很多次了,我们很容易得出结论:这是一次IPC调用,对应的方法在ActivityManagerService#activityIdle: + + + +```java +@Override +public final void activityIdle(IBinder token, Configuration config, boolean stopProfiling) { + final long origId = Binder.clearCallingIdentity(); + synchronized (this) { + ActivityStack stack = ActivityRecord.getStackLocked(token); + if (stack != null) { + ActivityRecord r = + mStackSupervisor.activityIdleInternalLocked(token, false /* fromTimeout */, + false /* processPausingActivities */, config); + if (stopProfiling) { + if ((mProfileProc == r.app) && mProfilerInfo != null) { + clearProfilerLocked(); + } + } + } + } + Binder.restoreCallingIdentity(origId); +} +``` + +做的工作不多,调用了`mStackSupervisor.activityIdleInternalLocked`: + + + +```java +final ActivityRecord activityIdleInternalLocked(final IBinder token, boolean fromTimeout, + boolean processPausingActivities, Configuration config) { + if (DEBUG_ALL) Slog.v(TAG, "Activity idle: " + token); + .... + // Atomically retrieve all of the other things to do. + final ArrayList stops = processStoppingActivitiesLocked(r, + true /* remove */, processPausingActivities); + NS = stops != null ? stops.size() : 0; + if ((NF = mFinishingActivities.size()) > 0) { + finishes = new ArrayList<>(mFinishingActivities); + mFinishingActivities.clear(); + } + + if (mStartingUsers.size() > 0) { + startingUsers = new ArrayList<>(mStartingUsers); + mStartingUsers.clear(); + } + ... + // Stop any activities that are scheduled to do so but have been + // waiting for the next one to start. + for (int i = 0; i < NS; i++) { + r = stops.get(i); + final ActivityStack stack = r.getStack(); + if (stack != null) { + if (r.finishing) { + stack.finishCurrentActivityLocked(r, ActivityStack.FINISH_IMMEDIATELY, false); + } else { + stack.stopActivityLocked(r); + } + } + } + ... + return r; +} +``` + +`processStoppingActivitiesLocked`做的操作就是寻找需要被Stop的Activity,进去看一下: + + + +```java +final ArrayList processStoppingActivitiesLocked(ActivityRecord idleActivity, + boolean remove, boolean processPausingActivities) { + ArrayList stops = null; + + final boolean nowVisible = allResumedActivitiesVisible(); + // 遍历mStoppingActivities列表,选择其中符合Stop条件的Activity + for (int activityNdx = mStoppingActivities.size() - 1; activityNdx >= 0; --activityNdx) { + ActivityRecord s = mStoppingActivities.get(activityNdx); + boolean waitingVisible = mActivitiesWaitingForVisibleActivity.contains(s); + if (DEBUG_STATES) Slog.v(TAG, "Stopping " + s + ": nowVisible=" + nowVisible + + " waitingVisible=" + waitingVisible + " finishing=" + s.finishing); + if (waitingVisible && nowVisible) { + mActivitiesWaitingForVisibleActivity.remove(s); + waitingVisible = false; + if (s.finishing) { + s.setVisibility(false); + } + } + if (remove) { + final ActivityStack stack = s.getStack(); + final boolean shouldSleepOrShutDown = stack != null + ? stack.shouldSleepOrShutDownActivities() + : mService.isSleepingOrShuttingDownLocked(); + if (!waitingVisible || shouldSleepOrShutDown) { + if (!processPausingActivities && s.state == PAUSING) { + // Defer processing pausing activities in this iteration and reschedule + // a delayed idle to reprocess it again + removeTimeoutsForActivityLocked(idleActivity); + scheduleIdleTimeoutLocked(idleActivity); + continue; + } + + if (DEBUG_STATES) Slog.v(TAG, "Ready to stop: " + s); + if (stops == null) { + stops = new ArrayList<>(); + } + stops.add(s); + mStoppingActivities.remove(activityNdx); + } + } + } + + return stops; +} +``` + +之前在执行AActivity的Pause操作后,我们将AActivity加入到了ActivityStackSupervisor类中一个叫mStoppingActivities的列表中,在这里这个列表就派上用场了,我们会遍历这个列表,从中获取符合条件的Activity并组装到一个新的列表中返回。 + +完成这个操作后,`activityIdleInternalLocked`开始遍历这个返回的列表进Stop操作。 + +看`stack.stopActivityLocked(r)`,凭我20年Android开发经验练就的火眼金睛,感觉这里就是AActivity#onStop触发的地方: + + + +```java +// ActivityStack +final void stopActivityLocked(ActivityRecord r) { + + + if (r.app != null && r.app.thread != null) { + adjustFocusedActivityStackLocked(r, "stopActivity"); + r.resumeKeyDispatchingLocked(); + try { + + r.app.thread.scheduleStopActivity(r.appToken, r.visible, r.configChangeFlags); + if (shouldSleepOrShutDownActivities()) { + r.setSleeping(true); + } + Message msg = mHandler.obtainMessage(STOP_TIMEOUT_MSG, r); + mHandler.sendMessageDelayed(msg, STOP_TIMEOUT); + } catch (Exception e) { + + } + } +} +``` + +果然,在这个方法内部执行了`r.app.thread.scheduleStopActivity`,这个方法就不进去看了,我们只需要知道它最终调用了r的onStop就可以了 + +至此,从AActivity跳转BActivity所经历的生命周期就全部完成了。 + +为什么要用IdleHandler? + +我猜测是因为google的工程师认为既然BActivity已经启动了,那么主线程Handler的首要任务还是处理B进程内部的事情,至于AActivity,反正都已经Pause了,就抽个空闲时间告诉AMS把它Stop就可以了。 + +#### 从BActivity返回到AActivity + +先回顾一下从BActivity返回AActivity经历的生命周期 + +BActivity#onPause -> AActivity#onStart -> AActivity#onResume -> BActivity#onStop -> BActivity#onDestroy + +先从BActivity的finish方法说起 + +BActivity的finish方法最终会调用到AMS的finishActivity方法, + + + +```java +// ActivityManagerService +public final boolean finishActivity(IBinder token, int resultCode, Intent resultData, + int finishTask) { + // Refuse possible leaked file descriptors + if (resultData != null && resultData.hasFileDescriptors() == true) { + throw new IllegalArgumentException("File descriptors passed in Intent"); + } + + synchronized(this) { + ActivityRecord r = ActivityRecord.isInStackLocked(token); + if (r == null) { + return true; + } + // Keep track of the root activity of the task before we finish it + TaskRecord tr = r.getTask(); + // 正在结束的Activity所在的ActivityTask + ActivityRecord rootR = tr.getRootActivity(); + if (rootR == null) { + Slog.w(TAG, "Finishing task with all activities already finished"); + } + + final long origId = Binder.clearCallingIdentity(); + try { + boolean res; + final boolean finishWithRootActivity = + finishTask == Activity.FINISH_TASK_WITH_ROOT_ACTIVITY; + if (finishTask == Activity.FINISH_TASK_WITH_ACTIVITY + || (finishWithRootActivity && r == rootR)) { + ... + } else { + res = tr.getStack().requestFinishActivityLocked(token, resultCode, + resultData, "app-request", true); + } + return res; + } finally { + Binder.restoreCallingIdentity(origId); + } + } +} +``` + +入参的含义: + +- token : 准备finish的Activity的Binder token引用 +- resultCode: resultCode 字面意思,在onActivityResult接收到的 +- resultData: ResultData 字面意思,在onActivityResult接收得到的 +- finishTask: 是否将ActivityStack一起finish + 在这里我们又和token见面了,在前面从AActivity启动BActivity的流程里我们分析过,这个token是BActivity在AMS中的标识。 + +`ActivityManagerService#finishActivity`经过层层调用,会调用到`ActivityStack#finishActivityLocked`: + + + +```java +final boolean finishActivityLocked(ActivityRecord r, int resultCode, Intent resultData, + String reason, boolean oomAdj, boolean pauseImmediately) { + if (r.finishing) { + Slog.w(TAG, "Duplicate finish request for " + r); + return false; + } + + mWindowManager.deferSurfaceLayout(); + try { + // 将r标记为finishing + r.makeFinishingLocked(); + ... + // 将resultCode和resultData设置给resultTo + finishActivityResultsLocked(r, resultCode, resultData); + + final boolean endTask = index <= 0 && !task.isClearingToReuseTask(); + final int transit = endTask ? TRANSIT_TASK_CLOSE : TRANSIT_ACTIVITY_CLOSE; + if (mResumedActivity == r) { + if (DEBUG_VISIBILITY || DEBUG_TRANSITION) Slog.v(TAG_TRANSITION, + "Prepare close transition: finishing " + r); + if (endTask) { + mService.mTaskChangeNotificationController.notifyTaskRemovalStarted( + task.taskId); + } + mWindowManager.prepareAppTransition(transit, false); + + // Tell window manager to prepare for this one to be removed. + r.setVisibility(false); + + if (mPausingActivity == null) { + if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Finish needs to pause: " + r); + if (DEBUG_USER_LEAVING) Slog.v(TAG_USER_LEAVING, + "finish() => pause with userLeaving=false"); + // 将当前处于活跃状态的Activity进行Pause + startPausingLocked(false, false, null, pauseImmediately); + } + + if (endTask) { + mStackSupervisor.removeLockedTaskLocked(task); + } + } else if (r.state != ActivityState.PAUSING) { + ... + } else { + if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Finish waiting for pause of: " + r); + } + + return false; + } finally { + mWindowManager.continueSurfaceLayout(); + } +} +``` + +本方法重点事项 + +1. 将r(即将要finish的Activity)的finishing标记位置为true +2. 将将resultCode和resultData设置给resultTo,这个resultTo是我们在创建r的时候传入的,在AActivity跳BActivity的流程中我们介绍过,本场景中resultTo指AActivity的token +3. 将当前处于活跃状态的Activity进行pause,本场景中活跃状态的Activity就是AActivity + +##### BActivity#onPause + +其中`startPausingLocked`方法我们在前面已经分析过了,这里就不再赘述了,详细的分析可以看3.5.1章节。通过之前的分析我们知道,startPausingLocked会执行BActvity的onPause回调并最终调用到`ActivityManagerService#activityPaused`方法,这个放我我们之前也做过分析,最终会调用到`ActivityStack#resumeTopActivityInnerLocked`,这个方法的调用和BActivity的启动流程略有不同 + +##### AActivity#onStart、AActivity#onResume + +在调用`ActivityStack#resumeTopActivityInnerLocked`时,因为我们已经完成对BActivity的Pause,因此,获取栈顶Activity 即next时,拿到的会是AActivity,而AActivity的app和thread均不为空,因此, + 判断语句`if (next.app != null && next.app.thread != null)` 成立,它会走入`ActivityStack#resumeTopActivityInnerLocked`如下分支: + + + +```dart +if (next.app != null && next.app.thread != null) { + if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "Resume running: " + next + + " stopped=" + next.stopped + " visible=" + next.visible); + try { + // Deliver all pending results. + ArrayList a = next.results; + if (a != null) { + final int N = a.size(); + if (!next.finishing && N > 0) { + if (DEBUG_RESULTS) Slog.v(TAG_RESULTS, + "Delivering results to " + next + ": " + a); + next.app.thread.scheduleSendResult(next.appToken, a); + } + } + + if (next.newIntents != null) { + next.app.thread.scheduleNewIntent( + next.newIntents, next.appToken, false /* andPause */); + } + + // Well the app will no longer be stopped. + // Clear app token stopped state in window manager if needed. + next.notifyAppResumed(next.stopped); + + EventLog.writeEvent(EventLogTags.AM_RESUME_ACTIVITY, next.userId, + System.identityHashCode(next), next.getTask().taskId, + next.shortComponentName); + + next.sleeping = false; + mService.showUnsupportedZoomDialogIfNeededLocked(next); + mService.showAskCompatModeDialogLocked(next); + next.app.pendingUiClean = true; + next.app.forceProcessStateUpTo(mService.mTopProcessState); + next.clearOptionsLocked(); + // zhangyulong ResumeNextActivity + next.app.thread.scheduleResumeActivity(next.appToken, next.app.repProcState, + mService.isNextTransitionForward(), resumeAnimOptions); + + if (DEBUG_STATES) Slog.d(TAG_STATES, "resumeTopActivityLocked: Resumed " + + next); + } catch (Exception e) { + ... + return true; + } + } + +} +``` + +注意看`next.app.thread.scheduleResumeActivity`这里,这个方法最终会调用AActivity的onRestart、onStart和onResume,这个部分的调用大家肯定都轻车熟路了,就不跟进去看了 + +##### BActivity#onStop、BActivity#onDestroy + +在BActivity启动流程分析中,我们知道在onResume时,会向主线程MessageQueue发送一个IdleHandler,这个IdleHandler最终会调用`ActivityStackSupervisor#activityIdleInternalLocked`,但这次的调用又跟之前有所不同,因为我们在之前的操作时已经将BActivity置为finishing,因此`if (r.finishing)`判断为true,方法调用会执行 + + + +```java +final ActivityRecord activityIdleInternalLocked(final IBinder token, boolean fromTimeout, + boolean processPausingActivities, Configuration config) { + if (DEBUG_ALL) Slog.v(TAG, "Activity idle: " + token); + + + // Atomically retrieve all of the other things to do. + final ArrayList stops = processStoppingActivitiesLocked(r, + true /* remove */, processPausingActivities); + NS = stops != null ? stops.size() : 0; + if ((NF = mFinishingActivities.size()) > 0) { + finishes = new ArrayList<>(mFinishingActivities); + mFinishingActivities.clear(); + } + + if (mStartingUsers.size() > 0) { + startingUsers = new ArrayList<>(mStartingUsers); + mStartingUsers.clear(); + } + + // Stop any activities that are scheduled to do so but have been + // waiting for the next one to start. + for (int i = 0; i < NS; i++) { + r = stops.get(i); + final ActivityStack stack = r.getStack(); + if (stack != null) { + if (r.finishing) { + // stop 且 pause + stack.finishCurrentActivityLocked(r, ActivityStack.FINISH_IMMEDIATELY, false); + } else { + // 仅stop + stack.stopActivityLocked(r); + } + } + } + + return r; +} +``` + +方法执行`stack.finishCurrentActivityLocked`后,在APP进程对应执行BActivity的onStop和onDestroy + +至此,BActivity返回AActivity的生命周期也执行完毕 + +## AMS源码分析(二)onActivityResult执行过程 + +### onActivityResult + +onActivityResult机制的使用,属于Android基础知识,我想每一个Android程序员都用过,这里就不多做解释了,看一下使用方式 + +#### AActivity跳转BAcitivty并从BActivity返回数据 + + + +```java +// AActivity +@Override +protected void onCreate(@Nullable Bundle savedInstanceState) { + activityName = "AActivity"; + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_stack_test); + jumpBtn = findViewById(R.id.jumpBtn); + jumpBtn.setOnClickListener(v -> { + BActivity.start(this, 1); + }); + setActivityName(); +} +@Override +protected void onActivityResult(int requestCode, int resultCode, Intent data) { + Log.d("zyl", activityName + " requestCode = " + requestCode + " resultCode = " + resultCode); + super.onActivityResult(requestCode, resultCode, data); +} + + +// BActivity +public static void start(Activity activity, int requestCode) { + activity.startActivityForResult(new Intent(activity, BActivity.class), requestCode); +} + +@Override +protected void onCreate(@Nullable Bundle savedInstanceState) { + activityName = "BActivity"; + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_stack_test); +} + +@Override +public void finish() { + setResult(102); + super.finish(); +} +``` + +当BActivity finish时,会将resultCode和requestCode通过AActivity的onActivityResult回传回来 + +### Intent.FLAG_ACTIVITY_FORWARD_RESULT + +这是onActivityResult的另一种表现形式,在日常开发中我们经常会遇到如下场景 + 1.我要从Activity A 跳到B再跳到C + 2.我要从Activity C 返回数据给Activity A + 3.这种跳转层级有可能有很多层,比如 A ->B -> C -> D -> E等等 + +有同学看完肯定会说,这个简单啊,每个Activity都实现onActivityResult,每次跳转都使用startActivityForResult不就行了,这样当然可以,但是代码会稍显冗余。实际上,Android系统已经帮我们想好了这种场景的处理方式了 + +Intent.FLAG_ACTIVITY_FORWARD_RESULT的作用就是最后一个Activity直接调用第一个Activity的onActivityResult + +#### 示例: + +以AActivity跳转BActivity跳转CActivity为例: + +##### AActivity以startActivityForResult方式打开BActivity + + + +```java +public static void start(Activity activity, int requestCode) { + Intent intent = new Intent(activity, BActivity.class); + activity.startActivityForResult(intent, requestCode); +} +``` + +##### BActivity以普通方式打开CActivity,设置Intent 的Flag Intent.FLAG_ACTIVITY_FORWARD_RESULT + + + +```java +public static void start(Context context) { + Intent intent = new Intent(context, CActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); + context.startActivity(intent); +} +``` + +CActivity和BActivity返回后,AActivity会接收到从CActivity传递过来的消息 + +### 源码解析 + +#### ActivityResult数据的写入 + +上篇文章我们在解析Activity的finish过程时其实稍微接触了一些onActivity的执行流程,在Activity的finish过程中,会调用到`ActivityStack#finishActivityResultsLocked`方法 + + + +```csharp +private void finishActivityResultsLocked(ActivityRecord r, int resultCode, Intent resultData) { + // zhangyulong 将结果传递给谁 + ActivityRecord resultTo = r.resultTo; + if (resultTo != null) { + if (DEBUG_RESULTS) Slog.v(TAG_RESULTS, "Adding result to " + resultTo + + " who=" + r.resultWho + " req=" + r.requestCode + + " res=" + resultCode + " data=" + resultData); + if (resultTo.userId != r.userId) { + if (resultData != null) { + resultData.prepareToLeaveUser(r.userId); + } + } + if (r.info.applicationInfo.uid > 0) { + mService.grantUriPermissionFromIntentLocked(r.info.applicationInfo.uid, + resultTo.packageName, resultData, + resultTo.getUriPermissionsLocked(), resultTo.userId); + } + // zhangyulong 将ActivityResult结果放在resultTo中保存 + resultTo.addResultLocked(r, r.resultWho, r.requestCode, resultCode, + resultData); + r.resultTo = null; + } + else if (DEBUG_RESULTS) Slog.v(TAG_RESULTS, "No result destination from " + r); + r.results = null; + r.pendingResults = null; + r.newIntents = null; + r.icicle = null; +} +``` + +入参含义: + +- r:即将finish的Activity +- resultCode: 要传递的resultCode +- resultData:要传递的Intent + 这个方法将result信息封装成一个ResultIfo对象,并保存在列表中 + +#### ActivityResult数据的传递 + +在上篇文章中,我们在分析Activity的finish流程时提到过`ActivityStack#resumeTopActivityInnerLocked`方法,这个方法还有一个作用就是向上一个Activity,即`resultTo`传递ActivityResult + + + +```java +private boolean resumeTopActivityInnerLocked(ActivityRecord prev, ActivityOptions options) { + ... + if (next.app != null && next.app.thread != null) { + ... + synchronized(mWindowManager.getWindowManagerLock()) { + ... + try { + // Deliver all pending results. + ArrayList a = next.results; + if (a != null) { + final int N = a.size(); + if (!next.finishing && N > 0) { + if (DEBUG_RESULTS) Slog.v(TAG_RESULTS, + "Delivering results to " + next + ": " + a); + next.app.thread.scheduleSendResult(next.appToken, a); + } + } + + + ... + } + } + + ... + } else { + .... + } + + if (DEBUG_STACK) mStackSupervisor.validateTopActivitiesLocked(); + return true; +} +``` + +通过调用`next.app.thread.scheduleSendResult(next.appToken, a)`将ResultInfo发送到App进程中对应的Activity中,并回调onActivityResult方法 + +#### Intent.FLAG_ACTIVITY_FORWARD_RESULT的实现 + +上篇文章我们分析过,Activity的启动流程会调用`ActivityStarter#startActivity`,这个方法有对Intent.FLAG_ACTIVITY_FORWARD_RESULT的处理,废话不多说,直接看源码: + + + +```java +private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent, + String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo, + IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, + IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid, + String callingPackage, int realCallingPid, int realCallingUid, int startFlags, + ActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified, + ActivityRecord[] outActivity, TaskRecord inTask) { + ... + + ActivityRecord sourceRecord = null; + ActivityRecord resultRecord = null; + if (resultTo != null) { + // zhangyulong 获取启动的源activity + sourceRecord = mSupervisor.isInAnyStackLocked(resultTo); + if (DEBUG_RESULTS) Slog.v(TAG_RESULTS, + "Will send result to " + resultTo + " " + sourceRecord); + if (sourceRecord != null) { + if (requestCode >= 0 && !sourceRecord.finishing) { + resultRecord = sourceRecord; + } + } + } + if ((launchFlags & Intent.FLAG_ACTIVITY_FORWARD_RESULT) != 0 && sourceRecord != null) { + // 添加Intent.FLAG_ACTIVITY_FORWARD_RESULT启动的Activity不能使用startActivityForResult启动 + if (requestCode >= 0) { + ActivityOptions.abort(options); + return ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT; + } + // 将源Activity的resultTo赋值给目标Activity + resultRecord = sourceRecord.resultTo; + if (resultRecord != null && !resultRecord.isInStackLocked()) { + resultRecord = null; + } + resultWho = sourceRecord.resultWho; + requestCode = sourceRecord.requestCode; + sourceRecord.resultTo = null; + if (resultRecord != null) { + resultRecord.removeResultsLocked(sourceRecord, resultWho, requestCode); + } + if (sourceRecord.launchedFromUid == callingUid) { + callingPackage = sourceRecord.launchedFromPackage; + } + } + ... + return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags, true, + options, inTask, outActivity); +} +``` + +代码比较简单,主要就是判断Intent中是否带有Intent.FLAG_ACTIVITY_FORWARD_RESULT标签,如果有,则将源Activity的resultTo赋值给目标Activity + +如果有多个中间Activity,比如使用这种方式启动ABCDE五个Activity,其中,BCD都是中间Activity, + 那么resultTo的传递过程如下: + 1.A启动B,A作为resultTo传递给B + 2.B启动C,添加Intent.FLAG_ACTIVITY_FORWARD_RESULT,C获取B的ResultTo即A + 3.C启动D,添加Intent.FLAG_ACTIVITY_FORWARD_RESULT,D获取C的ResultTo即A + 4.D启动E,添加Intent.FLAG_ACTIVITY_FORWARD_RESULT,E获取D的ResultTo即A + 5.从E逐级返回,当E返回时,将ResultInfo赋值给A,当返回到A时,触发`next.app.thread.scheduleSendResult(next.appToken, a)` + +## AMS源码分析(三)AMS中Activity栈管理详解 + +Activity栈管理是AMS的另一个重要功能,栈管理又和Activity的启动模式和startActivity时所设置的Flag息息相关,Activity栈管理的主要处理逻辑是在`ActivityStarter#startActivityUnchecked`方法中,本文也会围绕着这个方法进进出出,反复摩擦,直到脑海中都是它的形状。goolge的工程师起名还是很讲究的,为什么要带Unchecked呢? Unchecked-不确定,是因为在执行这个方法时,我要启动哪个Activity还没决定呢,具体为什么,我想看过这篇文章你就明白了。 + +### Activity栈管理相关类 + +#### ActivityStackSupervisor + +顾名思义,Activity栈的功能提供者和管理者 + +#### ActivityDisplay + +表示一个屏幕,Android支持三种屏幕:主屏幕,外接屏幕(HDMI等),虚拟屏幕(投屏)。一般情况下,即只有主屏幕时,ActivityStackSupervisor与ActivityDisplay都是系统唯一 + +#### TaskRecord + +ActivityTask记录, Task是我们管理Activity栈的重要单元,它的表现形式与逻辑和Activity启动模式息息相关,也是本文重点要分析的 + +#### ActivityStack + +针对ActivityRecord 和 TaskRecord进行管理,记录ActivityRecord的状态和TaskRecord的状态。在Android N之前只有两种ActivityStack:homeStack(launcher和recents Activity)和其他。Android N开始有5种,增加了DockedStack(分屏Activity)、PinnedStack(画中画Activity)、freeformstack(自由模式Activity),虽然它名字叫ActivityStack,但是跟我们熟知的数据结构中的栈基本上没啥关系,这也是有可能会增加一点理解难度的地方 + +#### 关系图: + +先说一下关系: + +- 一个ActivityDisplay包含多个ActivityStack +- 一个ActivityStack包含多个TaskRecord +- 一个TaskRecord包含多个ActivityRecord + +![img](https:////upload-images.jianshu.io/upload_images/3112838-9cc3f298a586ec45.png?imageMogr2/auto-orient/strip|imageView2/2/w/1106/format/webp) + +### 启动模式 + +#### standard + +标准启动模式,启动Activity时依次向栈顶添加ActivityRecord,返回时依次推出,示例:AActivity打开BActivity打开CActivity + +![img](https:////upload-images.jianshu.io/upload_images/3112838-cd8e980ebd343e68.png?imageMogr2/auto-orient/strip|imageView2/2/w/591/format/webp) + +这是基本的Activity启动模式,需要注意的点不太多 + +##### Intent.FLAG_ACTIVITY_CLEAR_TOP + +还是刚才的案例,如果依次打开AActivity->BActivity->CActivity->DActivity,此时在DActivity打开AActivity时Intent添加Flag `Intent.FLAG_ACTIVITY_CLEAR_TOP`,系统会从BActivity开始依次将Task中的Activity依次销毁,直到DActivity,因为DActivity处于活跃状态,因此会先执行onPause,在onPause后,会销毁原来的AActivity,然后打开新的AActivity,最后执行DActivity的onStop和onDestory + 流程图如下: + +![img](https:////upload-images.jianshu.io/upload_images/3112838-6c3bcffc85f5f084.png?imageMogr2/auto-orient/strip|imageView2/2/w/1189/format/webp) + + + +log: + + + +![img](https:////upload-images.jianshu.io/upload_images/3112838-474e338521ee9f68.png?imageMogr2/auto-orient/strip|imageView2/2/w/871/format/webp) + +##### 源码分析 + +###### 加入TaskRecord + +摘抄`ActivityStarter#startActivityUnchecked`部分代码如下: + + + +```kotlin +if (mStartActivity.resultTo == null && mInTask == null && !mAddingToTask + && (mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0) { + newTask = true; + // zhangyulong 使用一个旧的Task 或者新建一个 + result = setTaskFromReuseOrCreateNewTask( + taskToAffiliate, preferredLaunchStackId, topStack); +} else if (mSourceRecord != null) { + // zhangyulong 使用源Activity的task + result = setTaskFromSourceRecord(); +} else if (mInTask != null) { + // zhangyulong 使用启动时传递的task + result = setTaskFromInTask(); +} else { + // zhangyulong 理论上的可能,不可能走到这里 + setTaskToCurrentTopOrCreateNewTask(); +} +if (result != START_SUCCESS) { + return result; +} +``` + +因为我们使用标准模式启动,因此,`resultTo`和`mSourceRecord`均不为空,这段逻辑会执行`setTaskFromSourceRecord`: + + + +```cpp +private int setTaskFromSourceRecord() { + ... + addOrReparentStartingActivity(sourceTask, "setTaskFromSourceRecord"); + return START_SUCCESS; + } +addOrReparentStartingActivity最终会执行TaskRecord#addActivityAtIndex: + +void addActivityAtIndex(int index, ActivityRecord r) { + ... + mActivities.add(index, r); + ... + } +``` + +向对应的ActivityRecord中的mActivities添加本条记录,完成加入TaskRecord的操作 + +###### standard + Intent.FLAG_ACTIVITY_CLEAR_TOP + +回到`ActivityStarter#startActivityUnchecked`,摘抄部分逻辑如下: + + + +```kotlin +if (!mAddingToTask && (mLaunchFlags & FLAG_ACTIVITY_CLEAR_TOP) != 0) { + // 将ActivityTask目标Actiivty之上的Activity全部清空,返回值top为可以复用的Activity + ActivityRecord top = sourceTask.performClearTaskLocked(mStartActivity, mLaunchFlags); + mKeepCurTransition = true; + // 如果可复用的Activity不为空,直接调用它的onNewIntent方法并将其resume + if (top != null) { + ActivityStack.logStartActivity(AM_NEW_INTENT, mStartActivity, top.getTask()); + deliverNewIntent(top); + mTargetStack.mLastPausedActivity = null; + if (mDoResume) { + mSupervisor.resumeFocusedStackTopActivityLocked(); + } + ActivityOptions.abort(mOptions); + return START_DELIVERED_TO_TOP; + } +} +``` + +重点看`performClearTaskLocked`,这里是将目标Activity顶部元素清空的逻辑 + + + +```dart +/*** +* newR: 需要启动的新Activity +* launchFlags: 新Activity的启动模式 +*/ +final ActivityRecord performClearTaskLocked(ActivityRecord newR, int launchFlags) { + int numActivities = mActivities.size(); + for (int activityNdx = numActivities - 1; activityNdx >= 0; --activityNdx) { + ActivityRecord r = mActivities.get(activityNdx); + if (r.finishing) { + continue; + } + // 在目标Task中找到了和新Activity相同的记录 + if (r.realActivity.equals(newR.realActivity)) { + final ActivityRecord ret = r; + //将在其之上的Activity全部清除 + for (++activityNdx; activityNdx < numActivities; ++activityNdx) { + r = mActivities.get(activityNdx); + if (r.finishing) { + continue; + } + ActivityOptions opts = r.takeOptionsLocked(); + if (opts != null) { + ret.updateOptionsLocked(opts); + } + // 执行finishActivityLocked,如果Activity已经stop,会直接执行onDestroy, + // 如果Activity还在活跃,则会先执行onPause + if (mStack != null && mStack.finishActivityLocked( + r, Activity.RESULT_CANCELED, null, "clear-task-stack", false)) { + --activityNdx; + --numActivities; + } + } + + // ActivityInfo.LAUNCH_MULTIPLE == standrad + // 如果新Activity的launchMode是standard,且launchFlag没有FLAG_ACTIVITY_SINGLE_TOP,则将之前task + // 内的activity也结束,以便建立新的 + if (ret.launchMode == ActivityInfo.LAUNCH_MULTIPLE + && (launchFlags & Intent.FLAG_ACTIVITY_SINGLE_TOP) == 0 + && !ActivityStarter.isDocumentLaunchesIntoExisting(launchFlags)) { + if (!ret.finishing) { + if (mStack != null) { + mStack.finishActivityLocked( + ret, Activity.RESULT_CANCELED, null, "clear-task-top", false); + } + // 返回空,说明不执行onNewIntent + return null; + } + } + + return ret; + } + } + return null; +} +``` + +看完这部分代码,我想FLAG_ACTIVITY_CLEAR_TOP是怎么工作的大家也就明白了。 + +#### singleTop + +singleTop:栈顶唯一,它和standrad的区别在于,如果是standrad模式,在栈顶启动一个相同的Activity,会创建一个新的Activity实例,如果是singleTop模式,在栈顶启动相同的Activity则只会调用原有Activity的onNewIntent,如果原Activity不在栈顶,那么表现形式就和standrad相同 + +##### 流程图: + +###### 原Activity不在栈顶 + +![img](https:////upload-images.jianshu.io/upload_images/3112838-a44aedbebde4ad4a.png?imageMogr2/auto-orient/strip|imageView2/2/w/767/format/webp) + + + +###### 原Activity在栈顶 + +![img](https:////upload-images.jianshu.io/upload_images/3112838-0a897cf31cb9aa8b.png?imageMogr2/auto-orient/strip|imageView2/2/w/1103/format/webp) + + + +##### singleTop + Intent.FLAG_ACTIVITY_CLEAR_TOP + +现在设想一种场景,AActivitylanchMode为singleTop,在AActivity的基础上依次打开了BActivity、CActivity, 在CActivity再次打开AActivity,但打开时设置Intent属性`Intent.FLAG_ACTIVITY_CLEAR_TOP`,此时的启动流程: + +![img](https:////upload-images.jianshu.io/upload_images/3112838-7ba227bafbceb763.png?imageMogr2/auto-orient/strip|imageView2/2/w/922/format/webp) + + + +##### log + +###### 原Activity不在栈顶 + +![img](https:////upload-images.jianshu.io/upload_images/3112838-6a5468b4fbb56192.png?imageMogr2/auto-orient/strip|imageView2/2/w/961/format/webp) + + + +###### 原Activity在栈顶 + +![img](https:////upload-images.jianshu.io/upload_images/3112838-09cafa3d81155741.png?imageMogr2/auto-orient/strip|imageView2/2/w/1022/format/webp) + + + +###### 原Actiivty在栈顶且设置Intent.FLAG_ACTIVITY_CLEAR_TOP + +![img](https:////upload-images.jianshu.io/upload_images/3112838-78c410cfc373debf.png?imageMogr2/auto-orient/strip|imageView2/2/w/821/format/webp) + + + +##### 源码分析 + +我们又要进入`ActivityStarter#startActivityUnchecked`方法了, startActivityUnchecked:你要对我负责555... + +摘抄startActivityUnchecked方法中关于singleTop模式的处理如下: + + + +```kotlin +// 要启动的Activity正好是当前在栈顶的Activity +// 当前聚焦的ActivityStack +final ActivityStack topStack = mSupervisor.mFocusedStack; +// 当前聚焦的ActivityStack中的栈顶Actiivty +final ActivityRecord topFocused = topStack.topActivity(); +// 当前栈顶Activity +final ActivityRecord top = topStack.topRunningNonDelayedActivityLocked(mNotTop); +final boolean dontStart = top != null && mStartActivity.resultTo == null + && top.realActivity.equals(mStartActivity.realActivity) + && top.userId == mStartActivity.userId + && top.app != null && top.app.thread != null + && ((mLaunchFlags & FLAG_ACTIVITY_SINGLE_TOP) != 0 + || mLaunchSingleTop || mLaunchSingleTask); +// dontStart为true说明可以直接复用栈顶Activity +if (dontStart) { + // For paranoia, make sure we have correctly resumed the top activity. + topStack.mLastPausedActivity = null; + if (mDoResume) { + mSupervisor.resumeFocusedStackTopActivityLocked(); + } + ActivityOptions.abort(mOptions); + if ((mStartFlags & START_FLAG_ONLY_IF_NEEDED) != 0) { + // We don't need to start a new activity, and the client said not to do + // anything if that is the case, so this is it! + return START_RETURN_INTENT_TO_CALLER; + } + // zhangyulong singleTop sigleTask onNewIntent 执行 + deliverNewIntent(top); + + return START_DELIVERED_TO_TOP; +} +``` + +需要注意的是,当我们设置目标Activity launchMode是singleTop时,判断条件用的是mLaunchSingleTop,而(mLaunchFlags & FLAG_ACTIVITY_SINGLE_TOP是指启动时设置的Intent的Flag属性。这两种方式都能达到singleTop的效果。 + +###### singleTop + Intent.FLAG_ACTIVITY_CLEAR_TOP + +这部分的逻辑处理和standard时差不多,还是`performClearTaskLocked`方法,这个方法在3.1.2.2章节已经分析过了,这里摘抄部分逻辑如下: + + + +```kotlin +// ActivityInfo.LAUNCH_MULTIPLE == standrad +// 如果新Activity的launchMode是standard,且launchFlag没有FLAG_ACTIVITY_SINGLE_TOP,则将之前task +// 内的activity也结束,以便建立新的 +if (ret.launchMode == ActivityInfo.LAUNCH_MULTIPLE + && (launchFlags & Intent.FLAG_ACTIVITY_SINGLE_TOP) == 0 + && !ActivityStarter.isDocumentLaunchesIntoExisting(launchFlags)) { + if (!ret.finishing) { + if (mStack != null) { + mStack.finishActivityLocked( + ret, Activity.RESULT_CANCELED, null, "clear-task-top", false); + } + // 返回空,说明不执行onNewIntent + return null; + } +} +``` + +因为我们的launchMode是FLAG_ACTIVITY_SINGLE_TOP,条件不成立,不会销毁命中的原Activity,但原Actiivty上面的记录均已销户,此时它已经是栈顶Activity了,继续执行会执行到`startActivityUnchecked`方法中3.2.3章节部分,和上面的逻辑就一致了。 + +#### singleTask + +singleTask: 栈内唯一,如果Activity设置了launchMode为singleTask,那么在整个ActivityStack中有且仅有一个实例存在。有些朋友看到它名字叫singleTask,就想当然的认为它是Task内唯一的,我们不要被它的名字骗了。需要注意的一点是,singleTask模式启动是默认clearTop的。 + +##### 启动流程 + +###### 场景一 + +假设AActivity的launchMode为singleTask,AActivity后依次启动BActivity和CActivity, CActivity又启动了AActivity, + 那么这个过程经历的流程如下: + + + +![img](https:////upload-images.jianshu.io/upload_images/3112838-1fa789833f2166c9.png?imageMogr2/auto-orient/strip|imageView2/2/w/901/format/webp) + + + +log: + + + +![img](https:////upload-images.jianshu.io/upload_images/3112838-a4fbd4f21483e22d.png?imageMogr2/auto-orient/strip|imageView2/2/w/896/format/webp) + + + +###### 场景二 + + 假设AActivity和BActivity都是Standard,CActivity为singleTask,CActiivty再次启动CActivity,这个过程的启动流程: + +![img](https:////upload-images.jianshu.io/upload_images/3112838-acca060d14e0b0c0.png?imageMogr2/auto-orient/strip|imageView2/2/w/783/format/webp) + + + +log: + + + +![img](https:////upload-images.jianshu.io/upload_images/3112838-1260cfaa0a9d1eee.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) + + + +###### 场景三 + + 看完前面两种场景,肯定会有同学不服气,你不是说singleTask是栈内唯一么,这两种场景都是task内的处理啊,那不就应该是task内唯一么,你说栈内唯一拿出证据来啊!别着急,证据马上来。 + +现在假设AActivity是singleTask, BActivity是standard, BActivity打开CActivity时创建新的Task, 然后CActivity再次打开AActivity + 流程如下: + 1.AActivity在其自身所在task启动BActivity + 2.BActivity在启动CActivity时创建新的Task + 3.CActivity启动AActivity时将AActivity所在的Task移到顶部 + 4.AActivity将BActivity清除并重新启动 + +流程图: + + + +![img](https:////upload-images.jianshu.io/upload_images/3112838-fe78ffaab3dbbbb6.png?imageMogr2/auto-orient/strip|imageView2/2/w/1052/format/webp) + + + +看下log是不是这样: + + + +![img](https:////upload-images.jianshu.io/upload_images/3112838-fea64811c8bae7ea.png?imageMogr2/auto-orient/strip|imageView2/2/w/1162/format/webp) + + + +log也验证了这个说法的正确性,从这个案例中我们看出,虽然AB在一个task, C在另一个task,但C启动A的时候,并没有在其自身的task启动,而是操作AB所在的task。因此,singeTask是栈内唯一的。 + +##### singeTask源码分析 + +再进入一次`ActivityStarter#startActivityUnchecked`一次,此时`startActivityUnchecked`内心活动:别进了,再进就怀孕了!! + +为什么说singleTask自带clearTop属性呢? 看下`startActivityUnchecked`的这段逻辑: + + + +```dart +// 如果启动Intent设置了FLAG_ACTIVITY_CLEAR_TOP或者目标Activity启动模式为singleInstance或者singleTask,执行以下逻辑 +if ((mLaunchFlags & FLAG_ACTIVITY_CLEAR_TOP) != 0 + || isDocumentLaunchesIntoExisting(mLaunchFlags) + || mLaunchSingleInstance || mLaunchSingleTask) { + final TaskRecord task = reusedActivity.getTask(); + // 将与新Actiivty在Task内相同的Activity的顶部元素清空 + final ActivityRecord top = task.performClearTaskForReuseLocked(mStartActivity, + mLaunchFlags); + ... +} +``` + +那么task切换到栈顶是在哪里呢,这段逻辑执行完成后,就会执行到Task切换的逻辑了,代码: + + + +```cpp +// 将目标Activity所在Task移动到栈顶 +reusedActivity = setTargetStackAndMoveToFrontIfNeeded(reusedActivity); +``` + +当前面的逻辑完成后,可复用用的Activity就已经在Task顶部,而Task也已经在Stack顶部,完事俱备,只欠东风,继续执行`startActivityUnchecked`会执行到3.2.3的内容,和singleTop的处理是一致的,最终回调了目标Activity的onNewIntent。 + +#### singleInstance + +singleInstance这个启动模式比sigleTask更NB一些,它不光在栈内唯一,而且还独占一个Task,一看就是那种有独立办公室的老板,跟其他打工人完全不是一个等级的。关于singleInstance的栈管理和切换,你可以把它理解成只有一个singleTask的Activity存在的Task就比较好理解了,上面我们也已经分析过了。 + +### Intent.FLAG_ACTIVITY_NEW_TASK、taskAffinity、新Task的创建 + +先看几个有意思的案例: + 现在有两个Activity,分别是AActivity和BActivity,我们先设置A和B均为standard,在A启动B时设置`Intent.FLAG_ACTIVITY_NEW_TASK`,跳转时Task会新建么? + 看一下log: + +![img](https:////upload-images.jianshu.io/upload_images/3112838-76e4c948605a09a9.png?imageMogr2/auto-orient/strip|imageView2/2/w/767/format/webp) + + + + + 从图上我们看到,A和B的TaskId都是305,也就是没有创建新的Task,怎么回事,Intent.FLAG_ACTIVITY_NEW_TASK怎么失效了? + + + +这个时候把B的launchMode设置为singleTask呢? + 看一下log: + + + +![img](https:////upload-images.jianshu.io/upload_images/3112838-c27c170253ec3831.png?imageMogr2/auto-orient/strip|imageView2/2/w/1039/format/webp) + + + +依然没有生效!! + +这个时候保持B的launchMode为singleTask, 设置B的taskAffinity为".b"试一下: + + + +![img](https:////upload-images.jianshu.io/upload_images/3112838-6c40dcb6ecf32be1.png?imageMogr2/auto-orient/strip|imageView2/2/w/1032/format/webp) + + + +生效了!! + +这个时候把Intent.FLAG_ACTIVITY_NEW_TASK取消,保持B的launchMode为singleTask, 设置B的taskAffinity为".b"试一下: + +![img](https:////upload-images.jianshu.io/upload_images/3112838-e0c3f1585d574b24.png?imageMogr2/auto-orient/strip|imageView2/2/w/1061/format/webp) + + + +生效了! + +这个时候把Intent.FLAG_ACTIVITY_NEW_TASK取消,修改B的launchMode为standard, 设置B的taskAffinity为".b"试一下: + + + +![img](https:////upload-images.jianshu.io/upload_images/3112838-bd3c6b843d72f901.png?imageMogr2/auto-orient/strip|imageView2/2/w/1025/format/webp) + + + +没生效! + +这个时候把B的taskAffinity删掉,设置B为singleInstace试一下: + + + +![img](https:////upload-images.jianshu.io/upload_images/3112838-88f51fe4a9a20a16.png?imageMogr2/auto-orient/strip|imageView2/2/w/1034/format/webp) + + + +又生效了!! + +看到这里是不是感觉已经晕了,那到底啥时候生效啥时候失效啊!别着急,看完源码我们再做总结 + +我们又要进入`ActivityStarter#startActivityUnchecked`方法了, startActivityUnchecked:不挣扎了,已经有你的形状了... + +#### Intent.FLAG_ACTIVITY_NEW_TASK的自动设置 + +startActivityUnchecked前面几行代码执行了一个叫做`computeLaunchingTaskFlags`的方法,这个方法的作用是根据新Activity的launchMode对launchFlag做处理: + + + +```csharp +private void computeLaunchingTaskFlags() { + ... + if (mInTask == null) { + if (mSourceRecord == null) { + ... + } else if (mSourceRecord.launchMode == LAUNCH_SINGLE_INSTANCE) { + // 如果源Activity是singleInstance,则新启动Activity时自动添加launchFlag FLAG_ACTIVITY_NEW_TASK + mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK; + } else if (mLaunchSingleInstance || mLaunchSingleTask) { + // 如果新Activity的launchMode是singleInstace或者singleTask,则新启动Activity时自动添加launchFlag FLAG_ACTIVITY_NEW_TASK + mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK; + } + } +} +``` + +也就是说,如果一个Actiivty是singleInstacne的,那么不管是别人启动它还是它启动别人,都会自动添加启动flag FLAG_ACTIVITY_NEW_TASK, 如果是singeTask,则只有别人启动它时才会这样设置 + +#### taskAffinity的识别 + +上一部分的逻辑执行后,`startActivityUnchecked`会执行`getReusableIntentActivity`方法,这个方法主要是寻找ActivityStack中是否有可复用的Task, 返回值会可复用Task的顶部元素: + + + +```java +private ActivityRecord getReusableIntentActivity() { + + // 设置了launchFlag为FLAG_ACTIVITY_NEW_TASK或者 launchMode为singleInstance或singleTask + boolean putIntoExistingTask = ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0 && + (mLaunchFlags & FLAG_ACTIVITY_MULTIPLE_TASK) == 0) + || mLaunchSingleInstance || mLaunchSingleTask; + + // inTask为null 且requestCode小于0(即resultTo == null) + putIntoExistingTask &= mInTask == null && mStartActivity.resultTo == null; + ActivityRecord intentActivity = null; + if (mOptions != null && mOptions.getLaunchTaskId() != -1) { + final TaskRecord task = mSupervisor.anyTaskForIdLocked(mOptions.getLaunchTaskId()); + intentActivity = task != null ? task.getTopActivity() : null; + } else if (putIntoExistingTask) { + if (mLaunchSingleInstance) { + // 如果launchMode为singleInstance,只要当前状态下Stack中有和要启动的Activity相同的记录,就说明可以复用 + intentActivity = mSupervisor.findActivityLocked(mIntent, mStartActivity.info, + mStartActivity.isHomeActivity()); + } else if ((mLaunchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) { + // 没研究 + intentActivity = mSupervisor.findActivityLocked(mIntent, mStartActivity.info, + !mLaunchSingleTask); + } else { + // 其他情况查找Stack中是否有适用的Task和可复用的Actiivty + intentActivity = mSupervisor.findTaskLocked(mStartActivity, mSourceDisplayId); + } + } + return intentActivity; +} +``` + +`findActivityLocked`逻辑比较简单,就是在整个Stack中遍历Activity作对比,重点看`ActivitySuperVisor#findTaskLocked`,`ActivitySuperVisor#findTaskLocked`中调用了`ActivityStack#findTaskLocked`,看一下重要逻辑: + + + +```csharp +void findTaskLocked(ActivityRecord target, FindTaskResult result) { + ... + } else if (!isDocument && !taskIsDocument + && result.r == null && task.rootAffinity != null) { + // 如果Task的rootAffinity和新Activity的taskAffinity匹配,则说明有可复用的栈 + // ,task的rootAffinity一般由底部Actiivty决定,不特意设置的话,一般使用包名 + if (task.rootAffinity.equals(target.taskAffinity)) { + result.r = r; + result.matchedByRootAffinity = true; + } + } else if (DEBUG_TASKS) Slog.d(TAG_TASKS, "Not a match: " + task); + } +} +``` + +这里就是匹配taskAffinity的地方。回到`getReusableIntentActivity`方法,说一下它的返回值逻辑: + +- 如果launchMode是singleInstance,则判断当前stack中是否有相同Actiivty,如果有则返回对应Actiivty,否则是null +- 如果launchMode是其他,则判断当前stack中是否有可以匹配其affinity的Task,如果有则返回对应Task顶部Activity,否则是null + +#### 是否创建新task的识别 + +如果`getReusableIntentActivity`方法返回值不为null,`startActivityUncheck`后面的逻辑会执行`setTaskFromReuseOrCreateNewTask`方法: + + + +```java +private void setTaskFromIntentActivity(ActivityRecord intentActivity) { + if ((mLaunchFlags & (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK)) + == (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK)) { + final TaskRecord task = intentActivity.getTask(); + task.performClearTaskLocked(); + // 设置启动新Actiivty时所使用的task + mReuseTask = task; + mReuseTask.setIntent(mStartActivity); + } + ... +} +``` + +注意,这个方法执行的必要条件是`getReusableIntentActivity`方法返回值不为null,入参intentActivity即是`getReusableIntentActivity`方法的返回值。 + 因此,假如mReuseTask 为null,则启动Actiivty时会创建新的task,否则向mReuseTask 中添加,逻辑如下: + + + +```java +private int setTaskFromReuseOrCreateNewTask( + TaskRecord taskToAffiliate, int preferredLaunchStackId, ActivityStack topStack) { + mTargetStack = computeStackFocus( + mStartActivity, true, mLaunchBounds, mLaunchFlags, mOptions); + if (mReuseTask == null) { + // 创建新的Task + final TaskRecord task = mTargetStack.createTaskRecord( + mSupervisor.getNextTaskIdForUserLocked(mStartActivity.userId), + mNewTaskInfo != null ? mNewTaskInfo : mStartActivity.info, + mNewTaskIntent != null ? mNewTaskIntent : mIntent, mVoiceSession, + mVoiceInteractor, !mLaunchTaskBehind /* toTop */, mStartActivity.mActivityType); + // 向新Task中添加 + addOrReparentStartingActivity(task, "setTaskFromReuseOrCreateNewTask - mReuseTask"); + ... + } else { + // 向旧task中添加 + addOrReparentStartingActivity(mReuseTask, "setTaskFromReuseOrCreateNewTask"); + } + + ... + return START_SUCCESS; +} +``` + +#### 总结 + +通过上面的代码分析,我们可以总结出在Activity启动过程中创建新Task的条件: + +1. standard、singleTop模式 Intent.FLAG_ACTIVITY_NEW_TASK 和taskAffinity必须同时设置 +2. sinlgeTask模式 只需设置taskAffinity,Intent.FLAG_ACTIVITY_NEW_TASK 可有可无 +3. singeInstance Intent.FLAG_ACTIVITY_NEW_TASK 和taskAffinity均可有可无 + + + +# PMS + +## 深入PMS源码(一)—— PMS的启动过程和执行流程 + +### PMS简介 + +作为Android开发者,或多或少的都接触过Android的framework层架构,这也是开发者从使用Android到了解安卓的过程,framework层的核心功能有AMS、PMS、WMS等,这三个也是系统中最基础的使用,在Android程序启动完成后回启动一系列的核心服务,AMS、PMS、WMS就是在此过程中启动的,之后的系列文章之后会一次介绍他们,本篇主要介绍PMS关于PMS文章共分为3篇,本篇最为首篇也是PMS的主要逻辑部分; + +- PMS主要功能 + +1. 管理设备上安装的所有应用程序,并在系统启动时加载应用程序; +2. 根据请求的Intent匹配到对应的Activity、Provider、Service,提供包含包名和Component的信息对象; +3. 调用需要权限的系统函数时,检查程序是否具备相应权限从而保证系统安全; +4. 提供应用程序的安装、卸载的接口; + +- PMS包管理 + +1. 应用程序层:使用getPackageManager()获取包的管理对象PackageManager,PMS使用的也是Binder通信,PackageManager是从ServiceManager中获取注册的Binder对象,具体的实现为PackageManagerService,PMS实现对所有程序的安装和加载; +2. PMS服务层:PMS运行在SystemServer进程中,主要使用/system/etc/permissions.xml和/data/system/packages.xml管理包信息; +3. 数据文件管理:PMS负责对系统的配置文件、apk安装文件、apk的数据文件执行管理、读写、创建和删除等功能; + (1)程序文件:所有系统程序的文件处于/system/app/目录下,第三方程序文件处于/data/app/目录下,在程序安装过程中PMS会将要安装的apk文件复制到/data/app/目录下,以包名命名apk文件并添加“-x”后缀,在文件更新时会修改后缀编号; + (2)/data/dalvik-cache/ 目录保存了程序中的执行代码,在应用程序运行前PMS会从apk中提取dex文件并保存在该目录下,以便之后能快速运行; + (3)对framework库文件,PMS会将其中所有的apk、jar文件中提取dex文件,将dex文件保存在/data/dalvik-cache/目录下; + (4)应用程序所使用的数据文件:数据以键值对保存、数据库保存、File保存所产生的文件都保存带/data/data/xxx/目录下,PMS在程序卸载会删除相应文件; + +- /data/system/packages.xml :系统的配置文件,记录所有的应用程序的包管理信息,PMS根据此文件管理所有程序 + +1. last-platform-version:记录系统最后一次修改的版本信息; +2. permissions:保存系统中所有的权限信息列表,系统权限以androd开头、自定义权限以包名开头; +3. sigs:签名标签,一个程序只能有一个签名但可以有多个证书,包含count属性表示证书数量; +4. cert:表示签名证书,包含index、key属性,index表示证书的下标; +5. perms:表示一个程序中声明使用的权限列表,存在package标签之下; +6. package:包含一个应用程序的对应关系: + (1)name:应用程序的包名 + (2)codePath:程序apk文件所在的路径 + (3)nativeLibraryPath:程序中使用的native文件路径,一般指程序包下的lib文件中导入的依赖 + (4)flags:表示应用程序的类型 + (5)it、ut:分别表示程序首次安装的install time、更新时间update time + (6)userId:表示应用程序在Linux下的用户id + (7)shareId:表示应用程序所共享的Linux用户Id,与userId互斥 + (8)installer:安装器的名称,在调用PackageManager.installPackage()方法时设置的名称 + +### PMS的启动过程 + +在Android系统启动过程中,程序会执行到SystemServer中,然后调用startBootstrapServices()方法启动核心服务,在startBootstrapServices()方法中完成PMS的启动: + +```java +private void startBootstrapServices() { + mPackageManagerService = PackageManagerService.main(mSystemContext, installer,mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore); // 1、调用main()创建PMS对象,注册Binder + +mPackageManager = mSystemContext.getPackageManager(); //2、初始化PackageManager对象 + } +``` + +1. 调用PackageManagerService.main()方法,在main()方法中创建PMS的对象,并向ServiceManager注册Binder + +```java +public static PackageManagerService main(Context context, Installer installer, boolean factoryTest, boolean onlyCore) { + // 1、创建PMS对象 + PackageManagerService m = new PackageManagerService(context, installer,factoryTest, onlyCore); + m.enableSystemUserPackages(); + ServiceManager.addService("package", m); // 2、注册PMS对象到ServiceManager中 + final PackageManagerNative pmn = m.new PackageManagerNative(); // 3、 创建PMN对象 + ServiceManager.addService("package_native", pmn); // 4、注册PMN对象 + return m; + } + +``` + +1. 调用ContextImpl.getPackageManager()获取PackageManager对象,getPackageManager()中使用ActivityThread.getPackageManager()获取前面创建并注册的Binder对象,然后创建ApplicationPackageManager实例 + +```java + @Override +public PackageManager getPackageManager() { + IPackageManager pm = ActivityThread.getPackageManager(); // 3、初始化PackageManagerService的代理Binder对象 + if (pm != null) { + return (mPackageManager = new ApplicationPackageManager(this, pm)); //创建Packagemanager的实例 + } + return null; +} +``` + +1. 程序在获取PMS对象时会调用ActivityThread.getPackageManager(),从ServiceManager中获取Binder,并获取BInder代理对象PMS实例 + +```java +public static IPackageManager getPackageManager() { + if (sPackageManager != null) { + return sPackageManager; + } + IBinder b = ServiceManager.getService("package”); // 获取注册Binder + sPackageManager = IPackageManager.Stub.asInterface(b); // 获取IPackageManager代理对象,即PMS + return sPackageManager; +} +``` + +从上面的3个过程可以得出以下结论: + +1. PMS使用Binder通信机制,最终IPackageManager接口的实现类为PackageManagerService类; +2. 系统中获取的PackageManager对象具体实现的子类是ApplicationPackageManager对象; + +### PMS构造函数 + +由上面的分析知道,在系统启动后程序执行PMS的构造函数创建对象,整个系统对程序的管理就从这里开始,先介绍下相关类和属性信息: + +- PMS中属性 + +1. ArrayMap mPackages:在扫描程序文件目录时会将信息保存在Package对象中,然后将所有程序包名极其的package保存在此集合中; +2. Settings mSettings:保存整个系统信息的Setting对象; +3. ActivityIntentResolver mActivities:遍历所有程序的目录,并解析所有的注册清单文件,将提取所有的Intent-filter数据保存在对应的集合中; +4. ActivityIntentResolver mReceivers:同上 +5. ServiceIntentResolver mServices:同上 +6. ProviderIntentResolver mProviders:同上 + +- PackageParser:解析apk文件的主要类,执行解析操作; +- PackageParser.Package:PackageParser的内部类,保存apk文件中的解析信息,每个应用程序对应一个Package对象,属性信息如下: + +1. String packageName:程序包名 +2. String codePath: 软件包的路径 +3. ApplicationInfo applicationInfo :applicationInfo对象 +4. final ArrayList permissions:申请权限的集合 +5. final ArrayList activities:Activity标签解析的结合 +6. final ArrayList receivers:Receiver标签解析的结合 +7. final ArrayList providers :Provider标签解析的结合 +8. final ArrayList services :Service标签解析的结合 +9. Bundle mAppMetaData = null:注册清单中设置的信息 +10. int mVersionCode:版本Code +11. String mVersionName:版本名称 +12. int mCompileSdkVersion:Sdk版本 + +- Settings:PMS内部主要保存信息的类,主要属性如下: + +1. mSettingsFilename:配置系统目录下的package.xml文件 + +```java +mSystemDir = new File(dataDir, "system"); // +mSettingsFilename = new File(mSystemDir, "packages.xml"); //获取系统目录下package.xml文件 +``` + +1. mBackupSettingsFilename:配置系统目录下的packages-backup.xml文件,一般在创建和修改package文件前,会先创建packages-backup保存原来信息,在操作读写后会删除此文件; + +```java +mBackupSettingsFilename = new File(mSystemDir, "packages-backup.xml"); +``` + +1. mPackageListFilename:配置系统目录下的packages.list文件,保存了所有应用程序列表,每一行对应一个应用程序 + +```java +mPackageListFilename = new File(mSystemDir, "packages.list”); +如:com.android.hai 10032 1 data/data/com.android.hai ,第一项:应用程序包名、第二项:Linux用户Id、第三项:1表示可以debug,0表示不能debug、第四项:程序数据文件目录 +``` + +1. ArrayMap mPackages:解析package.xml文件中的每个程序信息保存在PackageSetting对象中,将所有程序的PackageSetting都填充到集合中 +2. mDisabledSysPackages:保存那些没有经过正常程序卸载的应用程序列表,按照正常卸载程序时,PMS会自动删除package.xml文件中的信息,使用adb命令或其他方法删除时package文件信息则不会删除,系统启动时PMS会检查package文件,并检查对应的应用程序的文件目录,从而判断是否意外删除,如果意外删除则加入mDisabledSysPackages集合; +3. mUserIds:保存Linux下所有的用户Id列表 +4. mPendingPackages:在解析每个PackageSetting时如果是使用sharedId,则将此Setting加入此集合,等相应的share-user标签后再补充Setting +5. mPastSignatures:保存所有的签名文件信息 +6. mPermissions:保存所有的权限信息 +7. ArraySet mInstallerPackages:已安装的应用软件包 + +#### PMS的工作过程 + +- 构造函数 + +```java +public PackageManagerService(Context context, Installer installer, // PMS的构造函数 + boolean factoryTest, boolean onlyCore) { +sUserManager = new UserManagerService(context, this, // 创建UserManagerService +new UserDataPreparer(mInstaller, mInstallLock, mContext, mOnlyCore), mPackages); +mSettings = new Settings(mPermissionManager.getPermissionSettings(), mPackages); //1、创建保存系统信息的Settings对象 + +mHandlerThread = new ServiceThread(TAG,Process.THREAD_PRIORITY_BACKGROUND, true ); +mHandlerThread.start(); +mHandler = new PackageHandler(mHandlerThread.getLooper()); // 2、使用HandlerThread初始化PackageHandler对象 + +mFirstBoot = !mSettings.readLPw(sUserManager.getUsers(false)); // 3、调用readLPw()读取并解析配置package.xml +} + +final int packageSettingCount = mSettings.mPackages.size(); +for (int i = packageSettingCount - 1; i >= 0; i--) { + PackageSetting ps = mSettings.mPackages.valueAt(i); // 4、遍历所有的程序的packageSettings对象 + if (!isExternal(ps) && (ps.codePath == null || !ps.codePath.exists()) + && mSettings.getDisabledSystemPkgLPr(ps.name) != null) { // 判断文件已经被删除的或程序已被卸载 + mSettings.mPackages.removeAt(i); // 移除对应的PackageSettings对象 + mSettings.enableSystemPackageLPw(ps.name); // 从mDisabledSysPackages集合中也移除此程序的Settings对象 + } +} +File frameworkDir = new File(Environment.getRootDirectory(), "framework”); // 5、获取系统的framework文件 +Iterator pkgSettingIter = mSettings.mPackages.values().iterator(); +while (pkgSettingIter.hasNext()) { + PackageSetting ps = pkgSettingIter.next(); + if (isSystemApp(ps)) { // 6、保存已经安装的系统的应用程序 + mExistingSystemPackages.add(ps.name); + } +} +scanDirTracedLI(frameworkDir, // 7、扫描framework文件目录 + mDefParseFlags + | PackageParser.PARSE_IS_SYSTEM_DIR, + scanFlags + | SCAN_NO_DEX + | SCAN_AS_SYSTEM + | SCAN_AS_PRIVILEGED, + 0); +final File systemAppDir = new File(Environment.getRootDirectory(), "app"); +scanDirTracedLI(systemAppDir, // 8、扫描系统app文件 + mDefParseFlags + | PackageParser.PARSE_IS_SYSTEM_DIR, + scanFlags + | SCAN_AS_SYSTEM, + 0); +。。。。。。扫描各种目录下的文件信息 +scanDirTracedLI(sAppInstallDir, 0, scanFlags | SCAN_REQUIRE_KNOWN, 0); //9、扫描安装app目录/data/app/ + +mSettings.writeLPr(); // 10、写入配置文件 +Runtime.getRuntime().gc(); +``` + +在启动程序后,PMS的所有工作基本都在构造函数中执行的,具体的执行过程见上面代码注释,这里列出几点主要的执行步骤: + +1. 创建Settings对象,将PMS中的mPackage集合传入,此集合保存所有apk的解析数据 +2. 调用readLPw()方法解析系统配置文件package.xml +3. 调用scanDirTracedLI()扫描系统app +4. 调用scanDirTracedLI()扫描/data/app/下安装的第三方啊app +5. 执行mSettings.writeLPr()将扫描后的结果,重新写入配置文件 + +上面的几个主要过程即可实现PMS对所有安装程序的执行和管理,下面从源码的角度分析下PMS具体的执行细节; + +#### 解析配置文件package.xml + +- readLPw() + +```java + boolean readLPw(@NonNull List users) { + FileInputStream str = null; + if (mBackupSettingsFilename.exists()) { // 1、如果package-backUp.xml 文件存在,读取package-back.xml文件 + str = new FileInputStream(mBackupSettingsFilename); // 一般当写入配置时发生意外才会存在此文件 + } + +str = new FileInputStream(mSettingsFilename); // 2、正常情况下读取package.xml文件信息 +XmlPullParser parser = Xml.newPullParser(); // 3、使用PullParser解析xml文件 +parser.setInput(str, StandardCharsets.UTF_8.name()); + +while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { // 4、循环读取xml的节点数据 + String tagName = parser.getName(); // 读取每个标签名称 +3044 if (tagName.equals("package")) { +3045 readPackageLPw(parser); // 解析package标签中信息保存在PackageSetting对象中 +3046 } else if (tagName.equals("permissions")) { +3047 mPermissions.readPermissions(parser); // 解析系统所有的权限信息,保存在PermissionSettings中 +3048 } else if (tagName.equals("shared-user")) { +3051 readSharedUserLPw(parser); // 如果使用share-user,解析此标签 + } + // 在解析package标签时,如果程序使用的shareUserId,则将此setting对象加入此集合,等相应共享的程序加载完成后再完善信息 + final int N = mPendingPackages.size(); +3172 for (int i = 0; i < N; i++) { // 5、遍历mPendingPackage集合中的PackageSetting对象,逐步完善每个对象 +3173 final PackageSetting p = mPendingPackages.get(i); +3174 final int sharedUserId = p.getSharedUserId(); +3175 final Object idObj = getUserIdLPr(sharedUserId); // 获取次shareUserId对应的程序 +3176 if (idObj instanceof SharedUserSetting) { +3177 final SharedUserSetting sharedUser = (SharedUserSetting) idObj; +3178 p.sharedUser = sharedUser; // 完善p的信息 +3179 p.appId = sharedUser.userId; +3180 addPackageSettingLPw(p, sharedUser); +3181 } +3192 } +3193 mPendingPackages.clear(); // 清除集合 +} +} +``` + +文件在readLPw()中首先判断mBackupSettingsFilename文件是否存在,前面提到当更新配置文件时,系统会先将package.xml文件重名为backup文件,然后创建package.xml并将mSettings内容写入文件,写入完成之后将back-up为难删除,如果此过程发生意外则系统会保留back-up文件,再此重启PMS时会优先读取backup文件,一般情况会直接读取package.xml,读取信息后使用Xml解析文件中信息,在解析中提取每个标签中的属性,这里需要注意的是package 标签,其中包好应用程序的基本信息,程序回调用readPackageLPw()解析; + +- readPackageLPw():解析并获取package标签下的信息,并创建PackageSetting对象保存信息; + +```java + private void readPackageLPw(XmlPullParser parser) throws XmlPullParserException, IOException { +name = parser.getAttributeValue(null, ATTR_NAME); // 1、提前标签下的属性信息,保存在相应的变量中 +realName = parser.getAttributeValue(null, "realName"); +idStr = parser.getAttributeValue(null, "userId"); +uidError = parser.getAttributeValue(null, "uidError"); +sharedIdStr = parser.getAttributeValue(null, "sharedUserId"); +codePathStr = parser.getAttributeValue(null, "codePath"); +....... +version = parser.getAttributeValue(null, "version"); +timeStampStr = parser.getAttributeValue(null, "it"); +timeStampStr = parser.getAttributeValue(null, "ut"); +//2、创建PackageSetting对象保存从package标签解析出的信息,并保存在集合中mPackages集合中 +packageSetting = addPackageLPw(name.intern(), realName, new File(codePathStr), +3836 new File(resourcePathStr), legacyNativeLibraryPathStr, primaryCpuAbiString, +3837 secondaryCpuAbiString, cpuAbiOverrideString, userId, versionCode, pkgFlags, +3838 pkgPrivateFlags, parentPackageName, null /*childPackageNames*/, +3839 null /*usesStaticLibraries*/, null /*usesStaticLibraryVersions*/); +packageSetting.setTimeStamp(timeStamp); +packageSetting.firstInstallTime = firstInstallTime; +packageSetting.lastUpdateTime = lastUpdateTime; + +if (sharedIdStr != null) {//3、如果使用sharedId,则创建packageSetting对象保存在mPending集合中,稍后补充解析信息 +3853 if (sharedUserId > 0) { +3854 packageSetting = new PackageSetting(name.intern(), realName, new File( +3855 codePathStr), new File(resourcePathStr), legacyNativeLibraryPathStr, +3856 primaryCpuAbiString, secondaryCpuAbiString, cpuAbiOverrideString, +3857 versionCode, pkgFlags, pkgPrivateFlags, parentPackageName, +3858 null /*childPackageNames*/, sharedUserId, +3859 null /*usesStaticLibraries*/, null /*usesStaticLibraryVersions*/); +3860 packageSetting.setTimeStamp(timeStamp); +3861 packageSetting.firstInstallTime = firstInstallTime; +3862 packageSetting.lastUpdateTime = lastUpdateTime; +3863 mPendingPackages.add(packageSetting); // 4、将packageSetting保存在mPendingPackages集合中 +3873 } +if (installerPackageName != null) { + mInstallerPackages.add(installerPackageName); // 添加到已安装程序列表集合中 +} +``` + +在readPackageLPw()中首先从解析器parser中提取所有的标签信息,然后调用addPackageLPw()方法保存这些属性,最后当程序使用shareId时先暂时将对象的解析 添加到mPendingPackages集合中,等共享的应用执行结束后再补充信息; + +- addPackageLPw() + +```java +PackageSetting addPackageLPw(String name,......) { +598 PackageSetting p = mPackages.get(name); //从集合中取出对象 +599 if (p != null) { +600 if (p.appId == uid) { +601 return p; +602 } +606 } +607 p = new PackageSetting(name, realName, codePath, resourcePath, // 创建PackageSetting对象 +608 legacyNativeLibraryPathString, primaryCpuAbiString, secondaryCpuAbiString, +609 cpuAbiOverrideString, vc, pkgFlags, pkgPrivateFlags, parentPackageName, +610 childPackageNames, 0 /*userId*/, usesStaticLibraries, usesStaticLibraryNames); +611 p.appId = uid; +612 if (addUserIdLPw(uid, p, name)) { +613 mPackages.put(name, p); // 在mpackages中添加保存对象 +614 return p; +615 } +616 return null; +617 } +} +``` + +addPackageLPw()中直接创建PackageSetting对象,将解析的信息封装起来,然后以程序的name为Key将PackageSetting对象添加的mPackages集合中,那此时mPackages就保存了手机中所有app的应用信息; + +#### 扫描安装的应用程序 + +- scanDirTracedLI():扫描/data/app/目录文件下的所有apk文件信息,在scanDirTracedLI()中直接调用scanDirLI()方法 + +```java +private void scanDirLI(File scanDir, int parseFlags, int scanFlags, long currentTime) { +final File[] files = scanDir.listFiles(); // 1、获取目录下的文件集合,即所有的apk列表,然后逐个解析 +try (ParallelPackageParser parallelPackageParser = new ParallelPackageParser( + mSeparateProcesses, mOnlyCore, mMetrics, mCacheDir, + mParallelPackageParserCallback)) { // 2、创建ParallelPackageParser对象 + int fileCount = 0; + for (File file : files) { + parallelPackageParser.submit(file, parseFlags); //3、遍历files集合,使用parallelPackageParser提交每个apk文件 + fileCount++; + } + for (; fileCount > 0; fileCount--) { + ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take();//4、取出每个File的扫描结果,执行scan + scanPackageChildLI(parseResult.pkg, parseFlags, scanFlags,currentTime, null); + } +} +``` + +scanDirLI()方法执行的逻辑很简单: + +1. 遍历文件目录下的所有文件 +2. 创建ParallelPackageParser对象,调用submit()方法提交请求,执行解析每个apk文件 +3. 调用parallelPackageParser.take()逐个取出每个解析的结果 + +到这里我们知道PMS是对/data/app/目录中所有apk文件进行解析,在之前的版本中会直接创建PackageParser对象执行解析,在Androip P版本中引入ParallelPackageParser类,使用线程池和队列执行程序的解析; + +- ParallelPackageParser:内部使用线程池和队列执行文件目录中apk扫描解析 + +```java +private final BlockingQueue mQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); // 1、保存请求结果的队列,阻塞线程 +private final ExecutorService mService = ConcurrentUtils.newFixedThreadPool(MAX_THREADS, + "package-parsing-thread", Process.THREAD_PRIORITY_FOREGROUND); // 2、创建线程池 + +public void submit(File scanFile, int parseFlags) { + mService.submit(() -> { // 3、线程池提交任务 + ParseResult pr = new ParseResult(); + try { + PackageParser pp = new PackageParser(); + pp.setSeparateProcesses(mSeparateProcesses); + pp.setOnlyCoreApps(mOnlyCore); + pp.setDisplayMetrics(mMetrics); + pp.setCacheDir(mCacheDir); + pp.setCallback(mPackageParserCallback); + pr.scanFile = scanFile; + pr.pkg = parsePackage(pp, scanFile, parseFlags); // 执行文件的解析扫描,将结果封装在ParseResult中 + } + try { + mQueue.put(pr); // 4、将解析的结果添加到队列中 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + mInterruptedInThread = Thread.currentThread().getName(); + } + }); +} +public ParseResult take() { + try { + return mQueue.take(); // 从队列中取出解析结果 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } +} +``` + +ParallelPackageParser中利用线程池处理并发问题,执行多个apk文件的解析,并使用阻塞队列的方式同步线程的数据,在submit提交的任务run()方法中,创建了PackageParser对象并调用parser()方法解析apk,之后将解析的结果封装在ParseResult中,最后添加到mQueue队列中,PMS中在依次调用take()方法从mQueue队列中获取执行的结果; + +- PackageParser.parsePackage():解析每个apk文件 + +```java +public Package parsePackage(File packageFile, int flags, boolean useCaches) + throws PackageParserException { + Package parsed = useCaches ? getCachedResult(packageFile, flags) : null; // 1、从缓存中获取解析结果 + if (parsed != null) { + return parsed; + } + if (packageFile.isDirectory()) { // 2、判断是文件还是文件夹,分别执行不同的方法; + parsed = parseClusterPackage(packageFile, flags); // 执行文件夹解析 + } else { + parsed = parseMonolithicPackage(packageFile, flags); // 执行单个apk文件解析,单个安装apk文件时执行 + } + cacheResult(packageFile, flags, parsed); // 3、缓存解析的结果parser + return parsed; +} +``` + +ParserPackage.parser()中开始执行apk文件的解析,对于apk文件执行parseMonolithicPackage(),在执行解析结束后会缓存解析结果; + +```java +public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException { + final PackageLite lite = parseMonolithicPackageLite(apkFile, flags); + final SplitAssetLoader assetLoader = new DefaultSplitAssetLoader(lite, flags); // 1、使用AssetManager加载资源文件 + try { + final Package pkg = parseBaseApk(apkFile, assetLoader.getBaseAssetManager(), flags); // 执行parseBaseAPk + pkg.setCodePath(apkFile.getCanonicalPath()); + pkg.setUse32bitAbi(lite.use32bitAbi); + return pkg; + } +} +``` + +- parseMonolithicPackageLite():先解析出apk文件的基本信息 + +```java +private static PackageLite parseMonolithicPackageLite(File packageFile, int flags) throws PackageParserException { + final ApkLite baseApk = parseApkLite(packageFile, flags); + final String packagePath = packageFile.getAbsolutePath(); + + return new PackageLite(packagePath, baseApk, null, null, null, null, null, null); + } + +//先轻量级解析apk的基本信息 + private static ApkLite parseApkLite(String codePath, XmlPullParser parser, AttributeSet attrs,SigningDetails signingDetails) + throws IOException, XmlPullParserException, PackageParserException { + final Pair packageSplit = parsePackageSplitNames(parser, attrs); + for (int i = 0; i < attrs.getAttributeCount(); i++) { + final String attr = attrs.getAttributeName(i); + if (attr.equals("installLocation")) { + installLocation = attrs.getAttributeIntValue(i, + PARSE_DEFAULT_INSTALL_LOCATION); + } else if (attr.equals("versionCode")) { + versionCode = attrs.getAttributeIntValue(i, 0); + } else if (attr.equals("versionCodeMajor")) { + versionCodeMajor = attrs.getAttributeIntValue(i, 0); + } else if (attr.equals("revisionCode")) { + revisionCode = attrs.getAttributeIntValue(i, 0); + } + ..... + } + ...... + return new ApkLite(codePath, packageSplit.first, packageSplit.second, isFeatureSplit, + configForSplit, usesSplitName, versionCode, versionCodeMajor, revisionCode, + installLocation, verifiers, signingDetails, coreApp, debuggable, + multiArch, use32bitAbi, extractNativeLibs, isolatedSplits); + } +``` + +在parseMonolithicPackage()中先调用parseApkLite()将File先简单的解析以下,这里的解析只是获取注册清单中的基础信息,并将信息保存在ApkLite对象中,然后将ApkLite和文件路径 +封装在PackageLite对象中; + +- DefaultSplitAssetLoader:内部使用AssestManager加载apk文件的资源,并缓存AssestManager对象信息,主要针对split APK; + +- parseBaseApk():解析每个apk文件 + +```java +private Package parseBaseApk(File apkFile, AssetManager assets, int flags) + throws PackageParserException {// 参数:apk文件File、apk文件路径destCodePath + final String apkPath = apkFile.getAbsolutePath(); // 1、获取apk文件路径 + mArchiveSourcePath = sourceFile.getPath(); +399 int cookie = assmgr.findCookieForPath(mArchiveSourcePath); // 2、将apk的路径添加到AssetManager中加载资源 +401 parser = assmgr.openXmlResourceParser(cookie, "AndroidManifest.xml”); // 3、解析xml注册清单文件 + Resources res = new Resources(assmgr, metrics, null); // 4、创建Resource对象 + pkg = parseBaseApk(res, parser, flags, errorText); // 解析parser中的信息,保存在Package对象中 +pkg.setVolumeUuid(volumeUuid); +pkg.setApplicationVolumeUuid(volumeUuid); +pkg.setBaseCodePath(apkPath); +pkg.setSigningDetails(SigningDetails.UNKNOWN); +return pkg; +} +``` + +parseBaseApk()中从apkFile对象中获取apk文件路径,然后使用assmgr加载apk文件中的资源,从文件中读取注册清单文件,然后调用parseBaseApk解析注册清单; + +1. parseBaseApk():从Parser对象中获取解析的信息,保存在Package对象中 + +```java + private Package parseBaseApk(String apkPath,Resources res, XmlResourceParser parser, int flags, String[] outError){ +Pair packageSplit = parsePackageSplitNames(parser, parser); //1、从parser中解析出“package”设置的包名pkgName +pkgName = packageSplit.first; +splitName = packageSplit.second; +Package pkg = new Package(pkgName); // 2、创建Package对象,保存apk的包名 +//从Resource中获取各种信息,并保存在Package的属性中 + pkg.mVersionCode = sa.getInteger(…... +pkg.mVersionName = sa.getNonConfigurationString(…... + pkg.mSharedUserId = str.intern(…...); +pkg.mSharedUserLabel = sa.getResourceId(…... +pkg.installLocation = sa.getInteger(…... +pkg.applicationInfo.installLocation = pkg.installLocation; +return parseBaseApkCommon(pkg, null, res, parser, flags, outError); // 3、调用parseBaseApkCommon()继续解析文件 +} +``` + +parseBaseApk中从parser中提取注册清单中的基础信息,并封装保存在Pakage对象中,然后调用parseBaseApkCommon()方法继续解析清单文件中内容 + +- parseBaseApkCommon() + +```java +//parseBaseApkCommon()从Parser对象中解析数据信息 +int outerDepth = parser.getDepth(); // 获取parser的深度 +while ((type=parser.next()) != parser.END_DOCUMENT. // 1、循环解析parser对象 +804 && (type != parser.END_TAG || parser.getDepth() > outerDepth)) { + if (tagName.equals("application")) { +if (!parseBaseApplication(pkg, res, parser, attrs, flags, outError)) { // 2、解析application标签 +825 return null; +}else if (tagName.equals("permission")) { // 3、解析权限标签 +832 if (parsePermission(pkg, res, parser, attrs, outError) == null) { +833 return null; +834 } +} else if (tagName.equals("uses-feature")) { // 4、解析使用的user-feature标签,并保存在Package的集合中 + FeatureInfo fi = new FeatureInfo(); + ……. +pkg.reqFeatures.add(fi); +}else if (tagName.equals("uses-sdk")) { // 解析user-sdk标签 +}else if (tagName.equals("supports-screens")) { // 解析support-screens标签 +} +} +} +``` + +parseBaseApkCommon()中主要负责解析清单文件中的各种标签信息,其中最主要的就是解析标签下的四大组件的信息,在遇到applicaiton标签时直接调用了parseBaseApplication()执行解析; + +- parseBaseApplication(),主要的解析工作 + +```java +String tagName = parser.getName(); +final ApplicationInfo ai = owner.applicationInfo; +ai.theme = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestApplication_theme, 0); +ai.descriptionRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestApplication_description, 0); +ai.maxAspectRatio = sa.getFloat(R.styleable.AndroidManifestApplication_maxAspectRatio, 0); + +// 分别解析四大组件,将解析结果保存在Package对应的集合中 +1644 if (tagName.equals("activity")) { +1645 Activity a = parseActivity(owner, res, parser, attrs, flags, outError, false); +1651 owner.activities.add(a); +1653 } else if (tagName.equals("receiver")) { +1654 Activity a = parseActivity(owner, res, parser, attrs, flags, outError, true); +1660 owner.receivers.add(a); +1662 } else if (tagName.equals("service")) { +1663 Service s = parseService(owner, res, parser, attrs, flags, outError); +1669 owner.services.add(s); +1671 } else if (tagName.equals("provider")) { +1672 Provider p = parseProvider(owner, res, parser, attrs, flags, outError); +1678 owner.providers.add(p); +1680 } +``` + +清单文件解析共分两部分: + +1. 解析出application标签下设置的name类名、icon、theme、targetSdk、processName等属性标签并保存在ApplicationInfo对象中 +2. 循环解析activity、receiver、service、provider四个标签,并将信息到保存在Package中对应的集合中 + +下面逐个分析下四大组件是如何解析保存的: + +- parserActivity():解析Activity和Receiver标签并返回Activity对象封装所有属性; + +```java +private Activity parseActivity(Package owner,...) + throws XmlPullParserException, IOException { +TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestActivity); +cachedArgs.mActivityArgs.tag = receiver ? "" : "”; // 1、判断为activity或receiver +cachedArgs.mActivityArgs.sa = sa; +cachedArgs.mActivityArgs.flags = flags; +Activity a = new Activity(cachedArgs.mActivityArgs, new ActivityInfo()); // 2、创建Activity实例,并初始化系列属性 +a.info.theme = sa.getResourceId(R.styleable.AndroidManifestActivity_theme, 0); +a.info.taskAffinity = buildTaskAffinityName(owner.applicationInfo.packageName, + owner.applicationInfo.taskAffinity, str, outError); +a.info.launchMode = ... +if (parser.getName().equals("intent-filter")) { // 3、解析intent-filter + ActivityIntentInfo intent = new ActivityIntentInfo(a); + if (!parseIntent(res, parser, true /*allowGlobs*/, true /*allowAutoVerify*/, + intent, outError)) { + return null; + } + a.order = Math.max(intent.getOrder(), a.order); + a.intents.add(intent); // 将intent设置到Activity +} +return a; +``` + +在解析Activity和Receiver标签时,当标签设置intent-filter时则创建一个ActivityIntentInfo对象,并调用parseIntent()将intent-filter标签下的信息解析到ActivityIntentInfo中,并将ActivityIntentInfo对象保存在a.intents的集合中,简单的说一个intent-filter对应一个ActivityIntentInfo对象,一个Activity和Receiver可以包好多个intent-filter; + +1. parseIntent():解析每个intent-filter标签下的action、name等属性值,并将所有的属性值保存在outInfo对象中,这里的outInfo是ActivityIntentInfo对象; + +```java +while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + String nodeName = parser.getName(); // 1、获取节点名称 + if (nodeName.equals("action")) { //2、处理action节点 + String value = parser.getAttributeValue( // 3、获取action节点的name属性,并保存在outInfo属性中 + ANDROID_RESOURCES, "name"); + outInfo.addAction(value); // +} else if (nodeName.equals("category")) { + String value = parser.getAttributeValue( // 4、获取category属性,保存在outInfo属性中 + ANDROID_RESOURCES, "name"); + outInfo.addCategory(value); // 添加到Category集合中 +} else if (nodeName.equals("data")) { +sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestData); // 解析parser为sa属性 +String str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_mimeType, 0); + outInfo.addDataType(str); // 获取并保存mimeType中 +str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_scheme, 0); + outInfo.addDataScheme(str); // 获取并保存scheme中 +String host = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_host, 0); // +String port = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_port, 0); + outInfo.addDataAuthority(host, port); // 获取并保存host、port属性 +outInfo.hasDefault = outInfo.hasCategory(Intent.CATEGORY_DEFAULT); // 设置Default属性 +} +``` + +2. ActivityIntentInfo 继承 IntentInfo ,IntentInfo继承IntentFilter属性,所以outInfo保存的数据都保存在IntentFilter中,在IntentFilter中有标签对应的集合,如actions、mDataSchemes等,所以遍历的所有intent-filter中设置的数据都保存在其中; + +```java +public final static class ActivityIntentInfo extends IntentInfo {} +public static abstract class IntentInfo extends IntentFilter {} +public final void addAction(String action) { + if (!mActions.contains(action)) { + mActions.add(action.intern()); // 在Intent-Filter中保存action + } +} +public final void addCategory(String category) { + if (mCategories == null) mCategories = new ArrayList(); + if (!mCategories.contains(category)) { + mCategories.add(category.intern()); // + } +} +public final void addDataScheme(String scheme) { + if (mDataSchemes == null) mDataSchemes = new ArrayList(); + if (!mDataSchemes.contains(scheme)) { + mDataSchemes.add(scheme.intern()); // + } +} +``` + +- parserService():解析Service标签并返回Service对象,对应service标签下的信息保存在ServiceIntentInfo对象中,ServiceIntentInfo的作用和保存和ActivityIntentInfo一样 + +```java +TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestService); +cachedArgs.mServiceArgs.sa = sa; +cachedArgs.mServiceArgs.flags = flags; +Service s = new Service(cachedArgs.mServiceArgs, new ServiceInfo()); // 创建Service对象 +if (parser.getName().equals("intent-filter")) { // 解析intent-filter节点 + ServiceIntentInfo intent = new ServiceIntentInfo(s); + if (!parseIntent(res, parser, true /*allowGlobs*/, false /*allowAutoVerify*/, + intent, outError)) { + return null; + } + s.order = Math.max(intent.getOrder(), s.order); + s.intents.add(intent); +} else if (parser.getName().equals("meta-data")) { // 解析meta-data数据,保存在bundle中 + if ((s.metaData=parseMetaData(res, parser, s.metaData, + outError)) == null) { + return null; + } +} +``` + +- parserProvider():解析provider标签并返回provider对象 + +```java +TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestProvider); +cachedArgs.mProviderArgs.tag = ""; +cachedArgs.mProviderArgs.sa = sa; +cachedArgs.mProviderArgs.flags = flags; + +Provider p = new Provider(cachedArgs.mProviderArgs, new ProviderInfo()); // 创建Provider对象 +p.info.exported = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestProvider_exported, + providerExportedDefault); +String permission = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestProvider_permission, 0); +p.info.multiprocess = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestProvider_multiprocess, + false); +p.info.authority = cpname.intern(); +if (!parseProviderTags( // 调用parseProvider解析provider下的标签 + res, parser, visibleToEphemeral, p, outError)) { + return null; +} +``` + +在解析provider标签中,创建ProviderInfo对象保存设置的属性信息,如export、permission等,然后调用parseProviderTags解析provider标签中使用的其他标签信息 + +1. parseProviderTags(): + +```java +private boolean parseProviderTags(Resources res, XmlResourceParser parser, + boolean visibleToEphemeral, Provider outInfo, String[] outError){ + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { +if (parser.getName().equals("intent-filter")) { + ProviderIntentInfo intent = new ProviderIntentInfo(outInfo); + if (!parseIntent(res, parser, true /*allowGlobs*/, false /*allowAutoVerify*/, + intent, outError)) { + return false; + } + outInfo.order = Math.max(intent.getOrder(), outInfo.order); + outInfo.intents.add(intent); +} +if (parser.getName().equals("meta-data")) { + if ((outInfo.metaData=parseMetaData(res, parser, + outInfo.metaData, outError)) == null) { + return false; + } +} +if (parser.getName().equals("grant-uri-permission")) { +String str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestGrantUriPermission_path, 0); +PatternMatcher pa = new PatternMatcher(str, PatternMatcher.PATTERN_LITERAL); // 创建PatterMatcher匹配权限 +PatternMatcher[] newp = new PatternMatcher[N+1]; +newp[N] = pa; +outInfo.info.uriPermissionPatterns = newp; // 保存临时权限数组 +} +if (parser.getName().equals("path-permission")) { // 保存路径权限 +newp[N] = pa; +outInfo.info.pathPermissions = newp; +} +} +} +``` + +主要解析如下: + +1. 对于intent-filter标签,调用parserIntent()解析保存在ProviderIntentInfo对象中,并添加到intent集合中 +2. 解析设置的meta-data值 +3. 解析grant-uri-permission和path-permission等权限的匹配状况保存在Provider对象中的数组中; + +到此apk文件已经解析完成,相应的文件和属性都封装在Package对象中,并都保存在PMS属性集合mPackages集合中; + +#### 将apk解析数据同步到PMS的属性中 + +在使用线程池执行所有的apk解析后,所有的解析结果都保存在队列中,系统会循环调用take()方法取出解析的结果 + +```java +for (; fileCount > 0; fileCount--) { + ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take(); + +scanPackageChildLI(parseResult.pkg, parseFlags, scanFlags, + currentTime, null); + } +``` + +取出apk文件解析结果后,调用scanPackageChildLI()扫描获取到的ParseResult中的Pakcage对象,scanPackageChildLI()中直接调用addForInitLI()方法 + +- addForInitLI() + +```java +synchronized (mPackages) { +final PackageSetting installedPkgSetting = mSettings.getPackageLPr(pkg.packageName); // 1、获取从配置文件中读取的Settings + +final PackageParser.Package scannedPkg = scanPackageNewLI(pkg, parseFlags, scanFlags | SCAN_UPDATE_SIGNATURE, currentTime, user); // 调用scanPackageNewLI将pkg中的数据保存到PMS的变量中 +} + +// scanPackageNewLI():创建ScanRequest对象,执行文件扫描修改或更新对应的PackageSetting对象 +final ScanRequest request = new ScanRequest(pkg, sharedUserSetting, + pkgSetting == null ? null : pkgSetting.pkg, pkgSetting, disabledPkgSetting, + originalPkgSetting, realPkgName, parseFlags, scanFlags, + (pkg == mPlatformPackage), user); +final ScanResult result = scanPackageOnlyLI(request, mFactoryTest, currentTime); +if (result.success) { + commitScanResultsLocked(request, result); // 调用commitScanResultsLocked() +} +``` + +commitScanResultsLocked()中直接调用commitPackageSettings()调教apk的解析数据 + +```java +commitPackageSettings(pkg, oldPkg, pkgSetting, user, scanFlags, +(parseFlags & PackageParser.PARSE_CHATTY) != 0 /*chatty*/); // 提交包解析数据 +``` + +- commitPackageSettings():将解析得到的Package中的信息保存到PMS内部变量中,并创建程序包所需的各种文件信息 + +```java +final String pkgName = pkg.packageName; +if (pkg.packageName.equals("android")) { // 1、针对系统包,特殊处理属性的初始化 + mPlatformPackage = pkg; +2897 pkg.mVersionCode = mSdkVersion; +2898 mAndroidApplication = pkg.applicationInfo; +2899 mResolveActivity.applicationInfo = mAndroidApplication; +2900 mResolveActivity.name = ResolverActivity.class.getName(); +。。。。。。 +2912 mResolveComponentName = new ComponentName( +2913 mAndroidApplication.packageName, mResolveActivity.name); +} + int N = pkg.usesLibraries != null ? pkg.usesLibraries.size() : 0; +2950 for (int i=0; i MAX_RETRIES) { // 尝试超过4次报错 +4684 mHandler.sendEmptyMessage(MCS_GIVE_UP); // 超过4次后发送报错信息,回调错误方法 +4685 handleServiceError(); +4686 return false; +4687 } else { +4688 handleStartCopy(); // 子类需重写,执行文件复制操作 + res = true; +4691 } +4692 } catch (RemoteException e) { +4694 mHandler.sendEmptyMessage(MCS_RECONNECT); +4695 } +4696 handleReturnCode(); + return res; +4697 } +4699 final void serviceError() { +4701 handleServiceError(); +4702 handleReturnCode(); +4703 } +4704 abstract void handleStartCopy() throws RemoteException; // 抽象方法 +4705 abstract void handleServiceError(); +4706 abstract void handleReturnCode(); + } +``` + +由上面源码知道,HandlerParams是抽象类,内部提供程序文件的复制功能,执行将安装的apk文件复制到程序的制定安装位置,程序调用startCopy()后开始执行文件赋值,具体赋值调用抽象方法handleStartCopy(),所以具体的功能子类需要重写此方法,在copy发生异常时会发送Handler消息实现重试,最多重试4次后抛出异常; + +### PMS中APK安装过程 + +程序的安装执行到PMS中后,从Packagemanager的installpackage()方法开始,间接调用到PMS中installStage()函数,installStage()中首先创建InstallParams对象,保存要安装的文件URI和其他信息,然后发送INIT_COPY异步消息启动文件复制 + +```java +public void installStage(….) { +4590 Message msg = mHandler.obtainMessage(INIT_COPY); // 创建INIT_COPY消息 +4591 msg.obj = new InstallParams(packageURI, observer, flags, +4592 installerPackageName); // 创建InstallParams对象封装安装包属性信息 +4593 mHandler.sendMessage(msg); +4594 } +``` + +Handler在处理INIT_COPY消息时,根据mBound变量确定是否绑定了MSC服务,未绑定的先调用connectToService()执行绑定过程,然后将HandlerParams加入mPendingInstalls集合中等待执行,在服务绑定后发送MCS_BOUND消息,对于已经绑定的直接发送MSC_BOUND事件执行任务 + +```java + if (!mBound) { // 1、判断服务是否绑定 +457 if (!connectToService()) { // 2、执行Service绑定 +459 params.serviceError(); +460 return; +461 } else { +464 mPendingInstalls.add(idx, params); // 3、添加要执行的任务 +465 } +466 } else { +467 mPendingInstalls.add(idx, params); // 3、添加任务 +470 if (idx == 0) { +471 mHandler.sendEmptyMessage(MCS_BOUND); // 4、就一个事件时,直接发送事件立即执行 +472 } +473 } +``` + +在处理MCS_BOUND消息时,使用获取的Binder服务从mPendingInstalls列表中取出安装信息HandlerParams类中, + +```java +if (msg.obj != null) { +mContainerService = (IMediaContainerService) msg.obj; // 1、获取对应的服务 +} +HandlerParams params = mPendingInstalls.get(0); // 2、取出要安装的参数 +if (params != null) { +If(params.startCopy()){ // 3、执行Copy文件,最终执行到FileInstallArgs.copyApk() + if (mPendingInstalls.size() > 0) { + mPendingInstalls.remove(0); // 删除集合中的数据 + } +}; +} +``` + +在Handler处理事件时,从消息msg中获取IMediaContainerService服务对象,并从mPendingInstalls集合中获取要执行的任务,此处获取的是前面创建的InstallParams对象,然后调用params.startCopy()方法执行文件复制,按照HandlerParams的源码程序会调用InstallParams.handleStartCopy()方法; + +```java +// InstallParams:实现全新安装时的复制过程 + class InstallParams extends HandlerParams { + public void handleStartCopy() throws RemoteException { + mArgs = createInstallArgs(this); // 1、 +4834 if (ret == PackageManager.INSTALL_SUCCEEDED) { +4837 ret = mArgs.copyApk(mContainerService, true); // 2、 +4838 } + } +} +``` + +在InstallParams中主要执行两个过程: + +1. 根据Params类型创建InstallerArgs对象保存请求数据 +2. 调用InstallArgs.copyApk()方法实现复制 + +```java + private InstallArgs createInstallArgs(InstallParams params) { +4931 if (installOnSd(params.flags)) { +4932 return new SdInstallArgs(params);//复制到外部存储时使用 +4933 } else { +4934 return new FileInstallArgs(params); // 复制到内部存储时使用,在FileInstallArgs内部调用IMediaContainerService服务执行Copy过程 + } +} +``` + +InstallArgs本身是一个抽象类,内部包含了很多属性信息,它的实现类主要有两个FileInstallArgs和SdInstallArgs,FileInstallArgs主要实现将文件复制到内部存储,而SdInstallArgs执行复制到外部文件存储,这里会创建FileInstallArgs对象,在FileInstallArgs.copy()中直接调用doCopyApk(); + +- FileInstallArgs.copyApk() + +```java +int doCopyApk(IMediaContainerService imcs, boolean temp) throws RemoteException { +// 1、创建临时文件路径:data/app/vmd.sessionId.tmp 文件目录 +final File tempDir = mInstallerService.allocateStageDirLegacy(volumeUuid, isEphemeral); +codeFile = tempDir; +resourceFile = tempDir; +final IParcelFileDescriptorFactory target = new IParcelFileDescriptorFactory.Stub() { + @Override + public ParcelFileDescriptor open(String name, int mode) throws RemoteException { + try { + final File file = new File(codeFile, name); // 创建apk文件 + final FileDescriptor fd = Os.open(file.getAbsolutePath(), + O_RDWR | O_CREAT, 0644); + Os.chmod(file.getAbsolutePath(), 0644); + return new ParcelFileDescriptor(fd); //2、以临时File创建ParcelFileDescriptor对象,用于进程通信 + } + } +}; +// 3、调用IMS方法实现文件Copy,将apk文件复制到temp文件中,然后将tmp文件复制到data/app/下,删除临时tmp文件 +ret = imcs.copyPackage(origin.file.getAbsolutePath(), target); +} +``` + +在doCopyApk()中,首先在data/app/目录下创建临时目录data/app/vmd.sessionId.tmp 文件,然后实例化IParcelFileDescriptorFactory.Stub()对象,并以临时文件File创建ParcelFileDescriptor类用于Binder通信,最后调用imcs.copyPackage()执行文件复制,这里imcs实际是DefaultContainerService的对象; + +- DefaultContainerService.copyPackage() + +```java +final File packageFile = new File(packagePath); // 1、获取要复制的apk文件 +pkg = PackageParser.parsePackageLite(packageFile, 0); // 2、parser程序包 +return copyPackageInner(pkg, target); // 3、执行文件复制 + +private int copyPackageInner(PackageLite pkg, IParcelFileDescriptorFactory target) + throws IOException, RemoteException { + copyFile(pkg.baseCodePath, target, "base.apk”); // 1、复制apk文件 + if (!ArrayUtils.isEmpty(pkg.splitNames)) { + for (int i = 0; i < pkg.splitNames.length; i++) { + copyFile(pkg.splitCodePaths[i], target, "split_" + pkg.splitNames[i] + ".apk”); // 2、复制拆分的apk + } + } +return PackageManager.INSTALL_SUCCEEDED; +} +``` + +在copyPackage()方法中,首先根据文件路径获取apk文件,调用PackageParser解析apk文件,这里的解析主要是看apk是否包含split apk,然后调用copyPackageInner执行文件复制工作,在copyPackageInner中调用copyFile() + +```java +private void copyFile(String sourcePath, IParcelFileDescriptorFactory target, String targetName) + throws IOException, RemoteException { + InputStream in = null; + OutputStream out = null; + try { + in = new FileInputStream(sourcePath); + out = new ParcelFileDescriptor.AutoCloseOutputStream( + target.open(targetName, ParcelFileDescriptor.MODE_READ_WRITE)); // 回调target + FileUtils.copy(in, out); // 执行文件复制,在tmp/base.apk/ + } finally { + IoUtils.closeQuietly(out); + IoUtils.closeQuietly(in); + } +} +``` + +在copyFile()中,首先回调target的open()方法完成ParcelFileDescriptor的创建,然后调用FileUtils.copy()方法,将apk文件复制到tmp/base.apk/中,由前面的HandlerParam知道apk复制成功之后,回调InstallParams.handleReturnCode(),handleReturnCode()中直接调用processPendingInstall()方法 + +```java +private void processPendingInstall(final InstallArgs args, final int currentStatus) { + mHandler.post(new Runnable() { //1、mHandler发送延时任务 + public void run() { + mHandler.removeCallbacks(this); //移除其它任务 + PackageInstalledInfo res = new PackageInstalledInfo(); + res.setReturnCode(currentStatus); + res.uid = -1; + res.pkg = null; + res.removedInfo = null; + if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) { + args.doPreInstall(res.returnCode); //2、执行安装前处理 + synchronized (mInstallLock) { + installPackageTracedLI(args, res); //3、执行安装和解析 + } + args.doPostInstall(res.returnCode, res.uid);//4、执行安装后的扫尾工作 + } + }); +} +``` + +在processPendingInstall()中直接发送handler事件执行APK的安装工作,在Handler事件中首先创建PackageInstalledInfo对象,保存安装的状态,在复制成功后执行以下操作: + +1. 调用args.doPreInstall()执行安装apk前准备; +2. 调用installPackageTracedLI(args, res)执行apk的解析和安装工作 +3. 调用args.doPostInstall()执行apk文件安装完成后的工作 + +- args.doPreInstall(res.returnCode):如果复制状态不为Success,则清除复制的文件 + +```java +int doPreInstall(int status) { + if (status != PackageManager.INSTALL_SUCCEEDED) {//未复制成功清除文件 + cleanUp(); + } + return status; +} +``` + +- installPackageTracedLI()中直接调用installPackageLI() + +```java +private void installPackageLI(InstallArgs args, PackageInstalledInfo res) { + final int installFlags = args.installFlags; + final String installerPackageName = args.installerPackageName; + final File tmpPackageFile = new File(args.getCodePath()); + PackageParser pp = new PackageParser(); + pkg = pp.parsePackage(tmpPackageFile, parseFlags); // 1、解析apk文件 +//检查apk +//校验签名 +// 2、/data/app/vmdl18300388.tmp/base.apk,重命名为/data/app/包名-1/base.apk。 +if (!args.doRename(res.returnCode, pkg, oldCodePath)) { + return; +} +if (replace) { +replacePackageLIF(pkg, parseFlags, scanFlags, args.user, + installerPackageName, res, args.installReason); // 3、替换apk +} else { +installNewPackageLIF(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES, + args.user, installerPackageName, volumeUuid, res, args.installReason); // 3、安装新的apk +} +} +``` + +在installPackageLI()中主要执行以下操作: + +1. 创建PackageParser对象解析复制的临时apk文件; +2. 检查复制的apk文件并校验签名信息等; +3. 将复制的apk文件目录,重命名为以包名为目录的文件 +4. 确认是已安装的apk更新还是新安装的app,对于新安装的app执行installNewPackageLIF()方法 + +- installNewPackageLIF():安装新的apk + +```java +PackageParser.Package newPackage = scanPackageTracedLI(pkg, parseFlags, scanFlags, // 重新扫描apk文件 + System.currentTimeMillis(), user); +updateSettingsLI(newPackage, installerPackageName, null, res, user, installReason); // 更新Setting +if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) { + prepareAppDataAfterInstallLIF(newPackage); // 为安装好的App准备数据 +} +``` + +在installNewPackageLIF()中首先调用scanPackageTracedLI()重新扫描apk文件,scanPackageTracedLI内部会调用scanPackageNewLI()提交解析结果,将解析的结果Package对象添加到PMS的属性中,详细流程参见深入PMS源码(一)—— PMS的启动过程和执行流程,在安装完成后调用updateSettingsLI()更新mSetting属性,最后调用prepareAppDataAfterInstallLIF()为新安装的程序准备数据; + +```java +scannedPkg = scanPackageNewLI(pkg, parseFlags, scanFlags, currentTime, user); +``` + +到此程序的安装就完成了,上面主要介绍了Android系统是如何实现安装和文件复制的,在复制完成后,就会向PMS启动过程一样解析和处理apk文件; + +### 应用程序的卸载过程 + +- 应用程序的卸载 + +```java +packageManager.packageInstaller.uninstall() //卸载程序 +``` + +由深入PMS源码(一)—— PMS的启动过程和执行流程分析知道此处packageManager获取的是ApplicationPackageManager,而packageInstaller是在ApplicationPackageManager内部实例化的PackageInstaller的对象 + +```java +@Override + public PackageInstaller getPackageInstaller() { + synchronized (mLock) { + if (mInstaller == null) { + try { + mInstaller = new PackageInstaller(mPM.getPackageInstaller(), + mContext.getPackageName(), mContext.getUserId()); + } + } + return mInstaller; + } + } +``` + +在创建PackageInstaller对象中,调用mPM.getPackageInstaller()调用PMS中方法获取PackageInstallerService对象,并将其封装在PackageInstaller对象中,程序继续调用PackageInstaller.uninstall()中 + +```java +public void uninstall(@NonNull String packageName, @DeleteFlags int flags, + @NonNull IntentSender statusReceiver) { + uninstall(new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), + flags, statusReceiver); + } +``` + +uninstall()中首先根据传入的包名创建VersionedPackage对象,然后继续调用uninstall的重载方法 + +```java + @RequiresPermission(anyOf = { + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.REQUEST_DELETE_PACKAGES}) + public void uninstall(@NonNull VersionedPackage versionedPackage, @DeleteFlags int flags, + @NonNull IntentSender statusReceiver) { + try { + mInstaller.uninstall(versionedPackage, mInstallerPackageName, + flags, statusReceiver, mUserId); //2、 + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +``` + +在uninstall()中直接调用mInstaller.uninstall(),这里的mInstaller对象就是在创建PackageInstaller时传入的第一个参数,即PMS中实例话的PackageInstallerService对象,PackageInstallerService实现了IPackageInstaller.Stub类,可用于进程间通信 + +```java +public class PackageInstallerService extends IPackageInstaller.Stub { + @Override + public void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags,IntentSender statusReceiver, int userId) throws RemoteException { + mPermissionManager.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall"); // 1、检查权限 + final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext, + statusReceiver, versionedPackage.getPackageName(), + isDeviceOwnerOrAffiliatedProfileOwner, userId); //2、 + + } + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DELETE_PACKAGES)== PackageManager.PERMISSION_GRANTED) { + mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags); + } +} +``` + +在PackageInstallerService.install()权限中,主要执行3个流程: + +1. 创建PackageDeleteObserverAdapter对象,用于接收响应删除的结果 +2. 检查当前程序是否具有删除权限 +3. 调用PMS的deletePackageVersioned()方法执行程序的卸载 + +从上面分析应用程序真正开始卸载,是从PackageManager的deletePackageAsUser()函数开始,间接调用PMS的deletePackageVersioned()方法 + +```java +@Override +public void deletePackageVersioned(VersionedPackage versionedPackage, + final IPackageDeleteObserver2 observer, final int userId, final int deleteFlags) { + mHandler.post(new Runnable() { //发送Handler事件 + public void run() { + mHandler.removeCallbacks(this); // 移除事件 + int returnCode; + final PackageSetting ps = mSettings.mPackages.get(internalPackageName);//获取PackageSettings对象 + boolean doDeletePackage = true; + if (ps != null) { + final boolean targetIsInstantApp = + ps.getInstantApp(UserHandle.getUserId(callingUid)); + doDeletePackage = !targetIsInstantApp + || canViewInstantApps; + } + if (doDeletePackage) { + if (!deleteAllUsers) { + returnCode = deletePackageX(internalPackageName, versionCode, + userId, deleteFlags); + } + } + try { + observer.onPackageDeleted(packageName, returnCode, null);// 删除后回调 + } + } + }); +} +``` + +在deletePackageVersioned()中发送Post事件执行异步删除操作,在Handler事件中调用deletePackageX()方法 + +```java + int deletePackageX(String packageName, long versionCode, int userId, int deleteFlags) { + final PackageRemovedInfo info = new PackageRemovedInfo(this); + if (isPackageDeviceAdmin(packageName, removeUser)) { + Slog.w(TAG, "Not removing package " + packageName + ": has active device admin"); +return PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER; + } +info.origUsers = uninstalledPs.queryInstalledUsers(allUsers, true); + res = deletePackageLIF(packageName, UserHandle.of(removeUser), true, allUsers, + deleteFlags | PackageManager.DELETE_CHATTY, info, true, null); + +} +``` + +在deletePackageX()中首先判断要卸载的程序是否是admin管理的,如果是则直接return,如果不是则执行deletePackageLIF()方法继续卸载程序 + +- deletePackageLIF() + +```java + p = mPackages.get(packageName); //1、获取保存应用程序的Package对象 +if (isSystemApp(p)) { +ret = deleteSystemPackageLI(p, flags, outInfo, writeSettings); //2、如果是系统app +6376 } else { +// 3、处理第三方app卸载 +ret = deleteInstalledPackageLIF(p, deleteCodeAndResources, flags, outInfo, +6381 writeSettings); +6382 } +``` + +deletePackageLIF()中根据app类型区分系统app和第三方app,执行不同的卸载程序,对于第三方app执行deleteInstalledPackageLIF()方法,在deleteInstalledPackageLIF()中也是直接调用removePackageDataLI() + +- removePackageDataLIF() + +```java + private void removePackageDataLI(PackageParser.Package p, PackageRemovedInfo outInfo,int flags, boolean writeSettings) { +6183 String packageName = p.packageName; +6187 removePackageLI(p, (flags&REMOVE_CHATTY) != 0); // 1、移除mPackages集合中保存的package对象 + +if ((flags & PackageManager.DELETE_KEEP_DATA) == 0) { + final PackageParser.Package resolvedPkg; + resolvedPkg = new PackageParser.Package(ps.name); + resolvedPkg.setVolumeUuid(ps.volumeUuid); + } + destroyAppDataLIF(resolvedPkg, UserHandle.USER_ALL, + StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE); // 2、调用Installer执行清除/data/app/目录下的文件信息 + destroyAppProfilesLIF(resolvedPkg, UserHandle.USER_ALL); + //3、发送包清理事件 + schedulePackageCleaning(packageName, UserHandle.USER_ALL, true); + } + +6210 synchronized (mPackages) { +6211 if (deletedPs != null) { +6212 if ((flags&PackageManager.DONT_DELETE_DATA) == 0) { +6213 if (outInfo != null) { +6214 outInfo.removedUid = mSettings.removePackageLPw(packageName); // 4、移除mSetting中保存的数据 +6215 } +6216 if (deletedPs != null) { + mHandler.post(new Runnable() { + @Override + public void run() {//5、发送事件杀死进程信息 + killApplication(deletedPs.name, deletedPs.appId, + KILL_APP_REASON_GIDS_CHANGED); + } + }); +6221 } +6222 } +6223 } + if (writeSettings) { + mSettings.writeLPr(); // 6、写入mSettings信息,更新Package.xml文件 + } +} +``` + +在removePackageDataLIF()方法中执行了卸载删除的重要操作,具体如下: + +1. 调用removePackageLI()删除mPackages集合中保存的次apk的解析对象Package,在removePackageLI中会调用cleanPackageDataStructuresLILPw(),删除此PMS中保存的此应用程序中的四大组件信息 + +```java + void cleanPackageDataStructuresLILPw(PackageParser.Package pkg, boolean chatty) { + int N = pkg.providers.size(); + for (i=0; i { +private final ArraySet mFilters = new ArraySet(); //保存所有的intent对应的匹配对象 +private final ArrayMap mTypeToFilter = new ArrayMap(); //保存Type 匹配的Intent集合 +private final ArrayMap mBaseTypeToFilter = new ArrayMap(); +private final ArrayMap mWildTypeToFilter = new ArrayMap();// 保存所有带“*”通配符类型 +private final ArrayMap mSchemeToFilter = new ArrayMap(); //已注册所有Uri方案 +private final ArrayMap mActionToFilter = new ArrayMap();//保存所有指定Action 的Intent集合 +private final ArrayMap mTypedActionToFilter = new ArrayMap(); // 匹配已注册且指定Mime Type类型Intent +} +``` + +上面的IntentResolver是一个范型,内部储存者IntentFilter的子类,而ActivityIntentInfo继承IntentInfo,IntentInfo又继承于IntentFilter类,所以此处直接保存的就是ActivityIntentInfo对象,在IntentFilter中保存着设置的多个属性 + +```java +private int mPriority; // 优先级 +private int mOrder; // 层级 +private final ArrayList mActions; // 设置的actions集合 +private ArrayList mCategories = null; // 设置categories集合 +private ArrayList mDataSchemes = null; // 数据集合 +private ArrayList mDataSchemeSpecificParts = null; +private ArrayList mDataAuthorities = null; // 匹配权限 +private ArrayList mDataPaths = null; // 匹配数据path +private ArrayList mDataTypes = null; // 数据类型 +``` + +- addFilter():添加匹配意图 + +添加PMS扫描获得的可匹配的IntentFilter对象,在PMS解析时将每个Activity中设置的标签中信息保存在一个IntentFilter对象中,在IntentFilter中保存着标签下所有的action、type、scheme、categories的集合,在addFilter()时将这些集合中数据取出作为Key,将包含相同Key的Intent-Filter保存在数组中,然后以Key为键将这些集合存储在Intent-Resolve的ArrayMap中; + +```java +public void addFilter(F f) { + mFilters.add(f); // 在总的Filter中保存数据 + int numS = register_intent_filter(f, f.schemesIterator(), + mSchemeToFilter, " Scheme: “); // 判断数据uri,保存在mSchemeToFilter集合中 + int numT = register_mime_types(f, " Type: “); //判断baseType 和 mWildType 类型 + if (numS == 0 && numT == 0) { + register_intent_filter(f, f.actionsIterator(), + mActionToFilter, " Action: “); // 判断匹配action对应的 + } + if (numT != 0) { + register_intent_filter(f, f.actionsIterator(), + mTypedActionToFilter, " TypedAction: “); // 添加mTypedActionToFilter集合 + } +} +``` + +在addFilter()中首先将传入的intent-filter对象保存在mFilters中,也就是说mFilters中保存着所有的匹配对象,然后依次调用register_intent_filter()、register_mime_types()、register_intent_filter()、register_intent_filter()方法分别解析IntentFilter对象中的属性信息,并保存在相应的HashMap中,这里以register_intent_filter()为例,register_intent_filter中传入的是f.actionsIterator()对象,即获取是actions集合的迭代器 + +```java +private final int register_intent_filter(F filter, Iterator i, ArrayMap dest, String prefix) { + int num = 0; + while (i.hasNext()) { // 遍历Iterator + String name = i.next(); // 取出数据集合DataSchemes下一个数据 + num++; + addFilter(dest, name, filter); // 添加到对应的Filter集合中,此处为mSchemeToFilter集合数据 + } + return num; +} +``` + +在register_intent_filter中使用迭代器遍历每个action,然后取出每个action标签下设置的name属性,调用addFilter()方法执行保存,在addFilter()中传入要保存的相应的ArrayMap对象,如action对应的就是mActionToFilter + +```java +private final void addFilter(ArrayMap map, String name, F filter) { + F[] array = map.get(name); // 根据name的名称去除保存数据的数组 + if (array == null) { + array = newArray(2); // 创建数组 + map.put(name, array); // 保存数据 + array[0] = filter; // 保存filter + } else { + final int N = array.length; + int i = N; + while (i > 0 && array[i-1] == null) { // 遍历寻找为null的位置 + i--; + } + if (i < N) { + array[i] = filter; + } else { + F[] newa = newArray((N*3)/2); // 原数组长度不够,扩展原数组长度 + System.arraycopy(array, 0, newa, 0, N); + newa[N] = filter; + map.put(name, newa); + } + } +} +``` + +addFilter()的执行逻辑很简单: + +1. 首先根据传入的name从相应的集合中获取数组,如果不存在则创建新数组,此时直接保存IntentFilter对象; +2. 如果已经存在数组遍历循转数组中空位置,如果存在将IntentFilter对象保存在数组中; +3. 如果不存在则扩展数组长度,然后保存IntentFilter数据; +4. 最后将保存IntentFilter数据的数组添加到对应的ArrayMap中; + +到此程序中注册的四大组件和相应的信息都会保存在IntentResolver类中,在使用时只需要根据设置的条件去查找匹配的对象即可; + +### IntentFilter的查找匹配 + +查询匹配方式:IntentResolver查询时分别从请求的Intent中取出数据(如:action、mime、scheme),然后分别以每个数据变量为Key,取出每个相应的ArrayMao中保存的对应的IntentFilter数组,最后求去这些数组的交集并去重,达到精准匹配的目的,最后返回匹配的所有ResolveInfo集合; + +一般在查找是否能匹配意图时,会调用packageManager.queryIntentActivities(intent,flag)查找匹配的IntentResolve,这里的packageManager对象实际调用ApplicationPackageManager对象,程序进入ApplicationPackageManager中,在ApplicationPackageManager.queryIntentActivities()中直接调用了queryIntentActivitiesAsUser() + +- ApplicationPackageManager + +```java +@Override +@SuppressWarnings("unchecked") +public List queryIntentActivitiesAsUser(Intent intent, + int flags, int userId) { + try { + ParceledListSlice parceledList = + mPM.queryIntentActivities(intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), + flags, userId);2、 + return parceledList.getList(); + } +} +``` + +在queryIntentActivitiesAsUser()中调用PMS中方法查询匹配Intent,PMS.queryIntentActivities()中间接调用queryIntentActivitiesInternal()方法 + +- queryIntentActivitiesInternal() + +```java +if (!sUserManager.exists(userId)) return Collections.emptyList(); // 判断userId +final String instantAppPkgName = getInstantAppPackageName(filterCallingUid); // 获取userId对应的进程包名 +final String pkgName = intent.getPackage(); // 获取Intent中携带的包名 +ComponentName comp = intent.getComponent(); // 获取Component对象 +final List list = new ArrayList(1); +final ActivityInfo ai = getActivityInfo(comp, flags, userId); // 首先从PMs中保存的mActivites集合中获取ActivityInfo信息 +result = filterIfNotSystemUser(mActivities.queryIntent( // 从mActivities中查询匹配的List + intent, resolvedType, flags, userId), userId); +``` + +在queryIntentActivitiesInternal()方法中: + +1. 首先判断当前用于的userId,如果不存在则直接返回空集合; +2. 从请求的Intent中获取包含的Component对象; +3. 如果Component不为空说明此时是显示启动,直接调用首先从PMs中保存的mActivites集合中获取ActivityInfo信息; +4. 对于Component为空的则调用mActivities.queryIntent()匹配能处理的ActivityInfo对象; + +- ActivityIntentResolve.queryIntent()中直接调用父类IntentResolve.queryIntent(),方法执行到IntentResolve.queryIntent()中 + +```java +public List queryIntent(Intent intent, String resolvedType, boolean defaultOnly, + int userId) { +String scheme = intent.getScheme(); // 获取scheme +ArrayList finalList = new ArrayList(); +F[] firstTypeCut = null; //保存匹配的结果 +F[] secondTypeCut = null; +F[] thirdTypeCut = null; +F[] schemeCut = null; +if (scheme != null) { + schemeCut = mSchemeToFilter.get(scheme); // 1、先根据scheme匹配,获取匹配的数组 +} + final String baseType = resolvedType.substring(0, slashpos); + if (!baseType.equals("*")) { + if (resolvedType.length() != slashpos+2 + || resolvedType.charAt(slashpos+1) != '*') { + firstTypeCut = mTypeToFilter.get(resolvedType); // 根据type匹配 + secondTypeCut = mWildTypeToFilter.get(baseType); + } else { + firstTypeCut = mBaseTypeToFilter.get(baseType); + secondTypeCut = mWildTypeToFilter.get(baseType); + } + thirdTypeCut = mWildTypeToFilter.get("*"); + } else if (intent.getAction() != null) { + firstTypeCut = mTypedActionToFilter.get(intent.getAction()); + } +} +if (resolvedType == null && scheme == null && intent.getAction() != null) { + firstTypeCut = mActionToFilter.get(intent.getAction()); // 2、根据action匹配 +} +if (firstTypeCut != null) { + buildResolveList(intent, categories, debug, defaultOnly, resolvedType, + scheme, firstTypeCut, finalList, userId); // 不断进行匹配 +} +if (secondTypeCut != null) { + buildResolveList(intent, categories, debug, defaultOnly, resolvedType, + scheme, secondTypeCut, finalList, userId); +} +if (thirdTypeCut != null) { + buildResolveList(intent, categories, debug, defaultOnly, resolvedType, + scheme, thirdTypeCut, finalList, userId); +} +if (schemeCut != null) { + buildResolveList(intent, categories, debug, defaultOnly, resolvedType, + scheme, schemeCut, finalList, userId); +} +sortResults(finalList); // 将数组中元素按照优先级排序 +return finalList; // 返回最终匹配的结果集合 +``` + +在queryIntent()中首先从Intent中取出所有属性信息,如action、scheme等,然后创建四个数组对象,这四个数组主要保存根据Intent中设置的属性查找出对应的数组,之后从每个ArrayMap中匹配返回相应的数组,然后根据四个数数组是否存在数据从而多此执行buildResolveList()方法,buildResolveList主要是获取四个数组中保存的IntentFilter对象转换为输出的对象,然后执行sortResults()其中的重复对象此时即可返回最终匹配结果; + +```java +private void buildResolveList(Intent intent, FastImmutableArraySet categories, + boolean debug, boolean defaultOnly, String resolvedType, String scheme, + F[] src, List dest, int userId) { +final int N = src != null ? src.length : 0; +F filter; +for (i=0; i= 0) { + if (!defaultOnly || filter.hasCategory(Intent.CATEGORY_DEFAULT)) { + final R oneResult = newResult(filter, match, userId); // 将filter转换为 ResolveInfo + if (oneResult != null) { + dest.add(oneResult); // 保存到dest集合中 + } + } +} +} +} +``` + +在上述的匹配过程之后即可获取到所有的可处理请求的Activity对象,然后将其中的信息设置在Intent之后再执行请求,此时就是像动态启动Activity一样直接启动目标活动; + +```java +Intent intent = new Intent(intentToResolve); +intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); +intent.setClassName(ris.get(0).activityInfo.packageName, +ris.get(0).activityInfo.name); +return intent; +``` + +到此PMS中关于保存IntentFilter和静态启动过程中的匹配过程介绍完毕了,通过上面的学习相信对PMS如何处理apk文件和系统如何识别要启动的程序或活动有了更深的理解,本篇也是PMS系列的最后一篇,希望整个系列的分析对大家学习有所帮助; + + + +# WMS + +## WMS(一):WMS的诞生 + +此前我用多篇文章介绍了WindowManager,这个系列我们来介绍WindowManager的管理者WMS,首先我们先来学习WMS是如何产生的。本文源码基于Android 8.0,与Android 7.1.2相比有一个比较直观的变化就是Java FrameWork采用了Lambda表达式。 + +### WMS概述 + +WMS是系统的其他服务,无论对于应用开发还是Framework开发都是重点的知识,它的职责有很多,主要有以下几点: + +#### 窗口管理 + +WMS是窗口的管理者,它负责窗口的启动、添加和删除,另外窗口的大小和层级也是由WMS进行管理的。窗口管理的核心成员有DisplayContent、WindowToken和WindowState。 + +#### 窗口动画 + +窗口间进行切换时,使用窗口动画可以显得更炫一些,窗口动画由WMS的动画子系统来负责,动画子系统的管理者为WindowAnimator。 + +#### 输入系统的中转站 + +通过对窗口的触摸从而产生触摸事件,InputManagerService(IMS)会对触摸事件进行处理,它会寻找一个最合适的窗口来处理触摸反馈信息,WMS是窗口的管理者,因此,WMS“理所应当”的成为了输入系统的中转站。 + +#### Surface管理 + +窗口并不具备有绘制的功能,因此每个窗口都需要有一块Surface来供自己绘制。为每个窗口分配Surface是由WMS来完成的。 + +WMS的职责可以简单总结为下图。 + + + +![img](https://s2.ax1x.com/2019/05/28/VexIGd.png) + + + +### WMS的诞生 + +WMS的知识点非常多,在了解这些知识点前,我们十分有必要知道WMS是如何产生的。WMS是在SyetemServer进程中启动的,不了解SyetemServer进程的可以查看在[Android系统启动流程(三)解析SyetemServer进程启动过程](https://link.juejin.cn?target=http%3A%2F%2Fliuwangshu.cn%2Fframework%2Fbooting%2F3-syetemserver.html)这篇文章。 +先来查看SyetemServer的main方法: +**frameworks/base/services/java/com/android/server/SystemServer.java** + +```java +public static void main(String[] args) { + new SystemServer().run(); +} +``` + +main方法中只调用了SystemServer的run方法,如下所示。 +**frameworks/base/services/java/com/android/server/SystemServer.java** + +```java +private void run() { + try { + System.loadLibrary("android_servers");//1 + ... + mSystemServiceManager = new SystemServiceManager(mSystemContext);//2 + mSystemServiceManager.setRuntimeRestarted(mRuntimeRestart); + LocalServices.addService(SystemServiceManager.class, mSystemServiceManager); + // Prepare the thread pool for init tasks that can be parallelized + SystemServerInitThreadPool.get(); + } finally { + traceEnd(); // InitBeforeStartServices + } + try { + traceBeginAndSlog("StartServices"); + startBootstrapServices();//3 + startCoreServices();//4 + startOtherServices();//5 + SystemServerInitThreadPool.shutdown(); + } catch (Throwable ex) { + Slog.e("System", "******************************************"); + Slog.e("System", "************ Failure starting system services", ex); + throw ex; + } finally { + traceEnd(); + } + ... + } +``` + +run方法代码很多,这里截取了关键的部分,在注释1处加载了libandroid_servers.so。在注释2处创建SystemServiceManager,它会对系统的服务进行创建、启动和生命周期管理。接下来的代码会启动系统的各种服务,在注释3中的startBootstrapServices方法中用SystemServiceManager启动了ActivityManagerService、PowerManagerService、PackageManagerService等服务。在注释4处的方法中则启动了BatteryService、UsageStatsService和WebViewUpdateService。注释5处的startOtherServices方法中则启动了CameraService、AlarmManagerService、VrManagerService等服务,这些服务的父类为SystemService。从注释3、4、5的方法名称可以看出,官方把大概80多个系统服务分为了三种类型,分别是引导服务、核心服务和其他服务,其中其他服务为一些非紧要和一些不需要立即启动的服务,WMS就是其他服务的一种。 +我们来查看startOtherServices方法是如何启动WMS的: + +**frameworks/base/services/java/com/android/server/SystemServer.java** + +```Java + private void startOtherServices() { + ... + traceBeginAndSlog("InitWatchdog"); + final Watchdog watchdog = Watchdog.getInstance();//1 + watchdog.init(context, mActivityManagerService);//2 + traceEnd(); + traceBeginAndSlog("StartInputManagerService"); + inputManager = new InputManagerService(context);//3 + traceEnd(); + traceBeginAndSlog("StartWindowManagerService"); + ConcurrentUtils.waitForFutureNoInterrupt(mSensorServiceStart, START_SENSOR_SERVICE); + mSensorServiceStart = null; + wm = WindowManagerService.main(context, inputManager, + mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL, + !mFirstBoot, mOnlyCore, new PhoneWindowManager());//4 + ServiceManager.addService(Context.WINDOW_SERVICE, wm);//5 + ServiceManager.addService(Context.INPUT_SERVICE, inputManager);//6 + traceEnd(); + ... + try { + wm.displayReady();//7 + } catch (Throwable e) { + reportWtf("making display ready", e); + } + ... + try { + wm.systemReady();//8 + } catch (Throwable e) { + reportWtf("making Window Manager Service ready", e); + } + ... +} +``` + +startOtherServices方法用于启动其他服务,其他服务大概有70多个,上面的代码只列出了WMS以及和它相关的IMS的启动逻辑,剩余的其他服务的启动逻辑也都大同小异。 +在注释1、2处分别得到Watchdog实例并对它进行初始化,Watchdog用来监控系统的一些关键服务的运行状况,后文会再次提到它。在注释3处创建了IMS,并赋值给IMS类型的inputManager对象。注释4处执行了WMS的main方法,其内部会创建WMS,需要注意的是main方法其中一个传入的参数就是注释1处创建的IMS,WMS是输入事件的中转站,其内部包含了IMS引用并不意外。结合上文,我们可以得知WMS的main方法是运行在SystemServer的run方法中,换句话说就是运行在"system_server"线程”中,后面会再次提到"system_server"线程。 +注释5和注释6处分别将WMS和IMS注册到ServiceManager中,这样如果某个客户端想要使用WMS,就需要先去ServiceManager中查询信息,然后根据信息与WMS所在的进程建立通信通路,客户端就可以使用WMS了。注释7处用来初始化显示信息,注释8处则用来通知WMS,系统的初始化工作已经完成,其内部调用了WindowManagerPolicy的systemReady方法。 +我们来查看注释4处WMS的main方法,如下所示。 +**frameworks/base/services/core/java/com/android/server/wm/WindowManagerService .java** + +```java +public static WindowManagerService main(final Context context, final InputManagerService im, + final boolean haveInputMethods, final boolean showBootMsgs, final boolean onlyCore, + WindowManagerPolicy policy) { + DisplayThread.getHandler().runWithScissors(() ->//1 + sInstance = new WindowManagerService(context, im, haveInputMethods, showBootMsgs, + onlyCore, policy), 0); + return sInstance; + } +``` + +在注释1处调用了DisplayThread的getHandler方法,用来得到DisplayThread的Handler实例。DisplayThread是一个单例的前台线程,这个线程用来处理需要低延时显示的相关操作,并只能由WindowManager、DisplayManager和InputManager实时执行快速操作。注释1处的runWithScissors方法中使用了Java8中的Lambda表达式,它等价于如下代码: + +```java +DisplayThread.getHandler().runWithScissors(new Runnable() { + @Override + public void run() { + sInstance = new WindowManagerService(context, im, haveInputMethods, showBootMsgs, + onlyCore, policy);//2 + } + }, 0); +``` + +在注释2处创建了WMS的实例,这个过程运行在Runnable的run方法中,而Runnable则传入到了DisplayThread对应Handler的runWithScissors方法中,说明WMS的创建是运行在“android.display”线程中。需要注意的是,runWithScissors方法的第二个参数传入的是0,后面会提到。来查看Handler的runWithScissors方法里做了什么: + +**frameworks/base/core/java/android/os/Handler.java** + +```java +public final boolean runWithScissors(final Runnable r, long timeout) { + if (r == null) { + throw new IllegalArgumentException("runnable must not be null"); + } + if (timeout < 0) { + throw new IllegalArgumentException("timeout must be non-negative"); + } + if (Looper.myLooper() == mLooper) {//1 + r.run(); + return true; + } + BlockingRunnable br = new BlockingRunnable(r); + return br.postAndWait(this, timeout); + } +``` + +开头对传入的Runnable和timeout进行了判断,如果Runnable为null或者timeout小于0则抛出异常。注释1处根据每个线程只有一个Looper的原理来判断当前的线程("system_server"线程)是否是Handler所指向的线程("android.display"线程),如果是则直接执行Runnable的run方法,如果不是则调用BlockingRunnable的postAndWait方法,并将当前线程的Runnable作为参数传进去 ,BlockingRunnable是Handler的内部类,代码如下所示。 +**frameworks/base/core/java/android/os/Handler.java** + +```java +private static final class BlockingRunnable implements Runnable { + private final Runnable mTask; + private boolean mDone; + public BlockingRunnable(Runnable task) { + mTask = task; + } + @Override + public void run() { + try { + mTask.run();//1 + } finally { + synchronized (this) { + mDone = true; + notifyAll(); + } + } + } + public boolean postAndWait(Handler handler, long timeout) { + if (!handler.post(this)) {//2 + return false; + } + synchronized (this) { + if (timeout > 0) { + final long expirationTime = SystemClock.uptimeMillis() + timeout; + while (!mDone) { + long delay = expirationTime - SystemClock.uptimeMillis(); + if (delay <= 0) { + return false; // timeout + } + try { + wait(delay); + } catch (InterruptedException ex) { + } + } + } else { + while (!mDone) { + try { + wait();//3 + } catch (InterruptedException ex) { + } + } + } + } + return true; + } + } +``` + +注释2处将当前的BlockingRunnable添加到Handler的任务队列中。前面runWithScissors方法的第二个参数为0,因此timeout等于0,这样如果mDone为false的话会一直调用注释3处的wait方法使得当前线程("system_server"线程)进入等待状态,那么等待的是哪个线程呢?我们往上看,注释1处,执行了传入的Runnable的run方法(运行在"android.display"线程),执行完毕后在finally代码块中将mDone设置为true,并调用notifyAll方法唤醒处于等待状态的线程,这样就不会继续调用注释3处的wait方法。因此得出结论,"system_server"线程线程等待的就是"android.display"线程,一直到"android.display"线程执行完毕再执行"system_server"线程,这是因为"android.display"线程内部执行了WMS的创建,显然WMS的创建优先级更高些。 +WMS的创建就讲到这,最后我们来查看WMS的构造方法: + +**frameworks/base/services/core/java/com/android/server/wm/WindowManagerService .java** + +```java +private WindowManagerService(Context context, InputManagerService inputManager, + boolean haveInputMethods, boolean showBootMsgs, boolean onlyCore, WindowManagerPolicy policy) { + ... + mInputManager = inputManager;//1 + ... + mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); + mDisplays = mDisplayManager.getDisplays();//2 + for (Display display : mDisplays) { + createDisplayContentLocked(display);//3 + } + ... + mActivityManager = ActivityManager.getService();//4 + ... + mAnimator = new WindowAnimator(this);//5 + mAllowTheaterModeWakeFromLayout = context.getResources().getBoolean( + com.android.internal.R.bool.config_allowTheaterModeWakeFromWindowLayout); + LocalServices.addService(WindowManagerInternal.class, new LocalService()); + initPolicy();//6 + // Add ourself to the Watchdog monitors. + Watchdog.getInstance().addMonitor(this);//7 + ... + } +``` + +注释1处用来保存传进来的IMS,这样WMS就持有了IMS的引用。注释2处通过DisplayManager的getDisplays方法得到Display数组(每个显示设备都有一个Display实例),接着遍历Display数组,在注释3处的createDisplayContentLocked方法会将Display封装成DisplayContent,DisplayContent用来描述一快屏幕。 +注释4处得到AMS实例,并赋值给mActivityManager ,这样WMS就持有了AMS的引用。注释5处创建了WindowAnimator,它用于管理所有的窗口动画。注释6处初始化了窗口管理策略的接口类WindowManagerPolicy(WMP),它用来定义一个窗口策略所要遵循的通用规范。注释7处将自身也就是WMS通过addMonitor方法添加到Watchdog中,Watchdog用来监控系统的一些关键服务的运行状况(比如传入的WMS的运行状况),这些被监控的服务都会实现Watchdog.Monitor接口。Watchdog每分钟都会对被监控的系统服务进行检查,如果被监控的系统服务出现了死锁,则会杀死Watchdog所在的进程,也就是SystemServer进程。 + +查看注释6处的initPolicy方法,如下所示。 +**frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java** + +```java +private void initPolicy() { + UiThread.getHandler().runWithScissors(new Runnable() { + @Override + public void run() { + WindowManagerPolicyThread.set(Thread.currentThread(), Looper.myLooper()); + mPolicy.init(mContext, WindowManagerService.this, WindowManagerService.this);//1 + } + }, 0); + } +``` + +initPolicy方法和此前讲的WMS的main方法的实现类似,注释1处执行了WMP的init方法,WMP是一个接口,init方法的具体实现在PhoneWindowManager(PWM)中。PWM的init方法运行在"android.ui"线程中,它的优先级要高于initPolicy方法所在的"android.display"线程,因此"android.display"线程要等PWM的init方法执行完毕后,处于等待状态的"android.display"线程才会被唤醒从而继续执行下面的代码。 + +在本文中共提到了3个线程,分别是"system_server"、"android.display"和"android.ui",为了便于理解,下面给出这三个线程之间的关系。 + + + +![img](https://s2.ax1x.com/2019/05/28/VexoRA.png) + + + +"system_server"线程中会调用WMS的main方法,main方法中会创建WMS,创建WMS的过程运行在"android.display"线程中,它的优先级更高一些,因此要等创建WMS完毕后才会唤醒处于等待状态的"system_server"线程。 +WMS初始化时会执行initPolicy方法,initPolicy方法会调用PWM的init方法,这个init方法运行在"android.ui"线程,并且优先级更高,因此要先执行完PWM的init方法后,才会唤醒处于等待状态的"android.display"线程。 +PWM的init方法执行完毕后会接着执行运行在"system_server"线程的代码,比如本文前部分提到WMS的systemReady方法。 + +## WMS(二):WMS的重要成员和Window的添加过程 + +在本系列的上一篇文章中,我们学习了WMS的诞生,WMS被创建后,它的重要的成员有哪些?Window添加过程的WMS部分做了什么呢?这篇文章会给你解答。 + +### WMS的重要成员 + +所谓WMS的重要成员是指WMS中的重要的成员变量,如下所示。 +**frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java** + +```java +final WindowManagerPolicy mPolicy; +final IActivityManager mActivityManager; +final ActivityManagerInternal mAmInternal; +final AppOpsManager mAppOps; +final DisplaySettings mDisplaySettings; +... +final ArraySet mSessions = new ArraySet<>(); +final WindowHashMap mWindowMap = new WindowHashMap(); +final ArrayList mFinishedStarting = new ArrayList<>(); +final ArrayList mFinishedEarlyAnim = new ArrayList<>(); +final ArrayList mWindowReplacementTimeouts = new ArrayList<>(); +final ArrayList mResizingWindows = new ArrayList<>(); +final ArrayList mPendingRemove = new ArrayList<>(); +WindowState[] mPendingRemoveTmp = new WindowState[20]; +final ArrayList mDestroySurface = new ArrayList<>(); +final ArrayList mDestroyPreservedSurface = new ArrayList<>(); +... +final H mH = new H(); +... +final WindowAnimator mAnimator; +... + final InputManagerService mInputManager +``` + +这里列出了WMS的部分成员变量,下面分别对它们进行简单的介绍。 + + **mPolicy:WindowManagerPolicy** +WindowManagerPolicy(WMP)类型的变量。WindowManagerPolicy是窗口管理策略的接口类,用来定义一个窗口策略所要遵循的通用规范,并提供了WindowManager所有的特定的UI行为。它的具体实现类为PhoneWindowManager,这个实现类在WMS创建时被创建。WMP允许定制窗口层级和特殊窗口类型以及关键的调度和布局。 + + **mSessions:ArraySet** +ArraySet类型的变量,元素类型为Session。在[Android解析WindowManager(三)Window的添加过程](https://link.juejin.cn?target=http%3A%2F%2Fliuwangshu.cn%2Fframework%2Fwm%2F3-add-window.html)这篇文章中我提到过Session,它主要用于进程间通信,其他的应用程序进程想要和WMS进程进行通信就需要经过Session,并且每个应用程序进程都会对应一个Session,WMS保存这些Session用来记录所有向WMS提出窗口管理服务的客户端。 +**mWindowMap:WindowHashMap** +WindowHashMap类型的变量,WindowHashMap继承了HashMap,它限制了HashMap的key值的类型为IBinder,value值的类型为WindowState。WindowState用于保存窗口的信息,在WMS中它用来描述一个窗口。综上得出结论,mWindowMap就是用来保存WMS中各种窗口的集合。 + + **mFinishedStarting:ArrayList** +ArrayList类型的变量,元素类型为AppWindowToken,它是WindowToken的子类。要想理解mFinishedStarting的含义,需要先了解WindowToken是什么。WindowToken主要有两个作用: + +- 可以理解为窗口令牌,当应用程序想要向WMS申请新创建一个窗口,则需要向WMS出示有效的WindowToken。AppWindowToken作为WindowToken的子类,主要用来描述应用程序的WindowToken结构, + 应用程序中每个Activity都对应一个AppWindowToken。 +- WindowToken会将相同组件(比如Acitivity)的窗口(WindowState)集合在一起,方便管理。 + +mFinishedStarting就是用于存储已经完成启动的应用程序窗口(比如Acitivity)的AppWindowToken的列表。 +除了mFinishedStarting,还有类似的mFinishedEarlyAnim和mWindowReplacementTimeouts,其中mFinishedEarlyAnim存储了已经完成窗口绘制并且不需要展示任何已保存surface的应用程序窗口的AppWindowToken。mWindowReplacementTimeout存储了等待更换的应用程序窗口的AppWindowToken,如果更换不及时,旧窗口就需要被处理。 + + **mResizingWindows:ArrayList** +ArrayList类型的变量,元素类型为WindowState。 +mResizingWindows是用来存储正在调整大小的窗口的列表。与mResizingWindows类似的还有mPendingRemove、mDestroySurface和mDestroyPreservedSurface等等。其中mPendingRemove是在内存耗尽时设置的,里面存有需要强制删除的窗口。mDestroySurface里面存有需要被Destroy的Surface。mDestroyPreservedSurface里面存有窗口需要保存的等待销毁的Surface,为什么窗口要保存这些Surface?这是因为当窗口经历Surface变化时,窗口需要一直保持旧Surface,直到新Surface的第一帧绘制完成。 + + **mAnimator:WindowAnimator** +WindowAnimator类型的变量,用于管理窗口的动画以及特效动画。 + + **mH:H** +H类型的变量,系统的Handler类,用于将任务加入到主线程的消息队列中,这样代码逻辑就会在主线程中执行。 + + **mInputManager:InputManagerService** +InputManagerService类型的变量,输入系统的管理者。InputManagerService(IMS)会对触摸事件进行处理,它会寻找一个最合适的窗口来处理触摸反馈信息,WMS是窗口的管理者,因此,WMS“理所应当”的成为了输入系统的中转站,WMS包含了IMS的引用不足为怪。 + +### Window的添加过程(WMS部分) + +我们知道Window的操作分为两大部分,一部分是WindowManager处理部分,另一部分是WMS处理部分,如下所示。 + +![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/10/26/d4cbb185457f36fd6fe1bd58e7470868~tplv-t2oaga2asx-watermark.awebp) + + +在[Android解析WindowManager(三)Window的添加过程](https://link.juejin.cn?target=http%3A%2F%2Fliuwangshu.cn%2Fframework%2Fwm%2F3-add-window.html)这篇文章中,我讲解了Window的添加过程的WindowManager处理部分,这一篇文章我们接着来学习Window的添加过程的WMS部分。 +无论是系统窗口还是Activity,它们的Window的添加过程都会调用WMS的addWindow方法,由于这个方法代码逻辑比较多,这里分为3个部分来阅读。 +**frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java** + + + +#### addWindow方法part1 + +```java + public int addWindow(Session session, IWindow client, int seq, + WindowManager.LayoutParams attrs, int viewVisibility, int displayId, + Rect outContentInsets, Rect outStableInsets, Rect outOutsets, + InputChannel outInputChannel) { + + int[] appOp = new int[1]; + int res = mPolicy.checkAddPermission(attrs, appOp);//1 + if (res != WindowManagerGlobal.ADD_OKAY) { + return res; + } + ... + synchronized(mWindowMap) { + if (!mDisplayReady) { + throw new IllegalStateException("Display has not been initialialized"); + } + final DisplayContent displayContent = mRoot.getDisplayContentOrCreate(displayId);//2 + if (displayContent == null) { + Slog.w(TAG_WM, "Attempted to add window to a display that does not exist: " + + displayId + ". Aborting."); + return WindowManagerGlobal.ADD_INVALID_DISPLAY; + } + ... + if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {//3 + parentWindow = windowForClientLocked(null, attrs.token, false);//4 + if (parentWindow == null) { + Slog.w(TAG_WM, "Attempted to add window with token that is not a window: " + + attrs.token + ". Aborting."); + return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN; + } + if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW + && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) { + Slog.w(TAG_WM, "Attempted to add window with token that is a sub-window: " + + attrs.token + ". Aborting."); + return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN; + } + } + ... +} +... +} +``` + +WMS的addWindow返回的是addWindow的各种状态,比如添加Window成功,无效的display等等,这些状态被定义在WindowManagerGlobal中。 +注释1处根据Window的属性,调用WMP的checkAddPermission方法来检查权限,具体的实现在PhoneWindowManager的checkAddPermission方法中,如果没有权限则不会执行后续的代码逻辑。注释2处通过displayId来获得窗口要添加到哪个DisplayContent上,如果没有找到DisplayContent,则返回WindowManagerGlobal.ADD_INVALID_DISPLAY这一状态,其中DisplayContent用来描述一块屏幕。注释3处,type代表一个窗口的类型,它的数值介于FIRST_SUB_WINDOW和LAST_SUB_WINDOW之间(1000~1999),这个数值定义在WindowManager中,说明这个窗口是一个子窗口,不了解窗口类型取值范围的请阅读[Android解析WindowManager(二)Window的属性](https://link.juejin.cn?target=http%3A%2F%2Fliuwangshu.cn%2Fframework%2Fwm%2F2-window-property.html)这篇文章。注释4处,attrs.token是IBinder类型的对象,windowForClientLocked方法内部会根据attrs.token作为key值从mWindowMap中得到该子窗口的父窗口。接着对父窗口进行判断,如果父窗口为null或者type的取值范围不正确则会返回错误的状态。 + +#### addWindow方法part2 + +```java +... + AppWindowToken atoken = null; + final boolean hasParent = parentWindow != null; + WindowToken token = displayContent.getWindowToken( + hasParent ? parentWindow.mAttrs.token : attrs.token);//1 + final int rootType = hasParent ? parentWindow.mAttrs.type : type;//2 + boolean addToastWindowRequiresToken = false; + + if (token == null) { + if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) { + Slog.w(TAG_WM, "Attempted to add application window with unknown token " + + attrs.token + ". Aborting."); + return WindowManagerGlobal.ADD_BAD_APP_TOKEN; + } + if (rootType == TYPE_INPUT_METHOD) { + Slog.w(TAG_WM, "Attempted to add input method window with unknown token " + + attrs.token + ". Aborting."); + return WindowManagerGlobal.ADD_BAD_APP_TOKEN; + } + if (rootType == TYPE_VOICE_INTERACTION) { + Slog.w(TAG_WM, "Attempted to add voice interaction window with unknown token " + + attrs.token + ". Aborting."); + return WindowManagerGlobal.ADD_BAD_APP_TOKEN; + } + if (rootType == TYPE_WALLPAPER) { + Slog.w(TAG_WM, "Attempted to add wallpaper window with unknown token " + + attrs.token + ". Aborting."); + return WindowManagerGlobal.ADD_BAD_APP_TOKEN; + } + ... + if (type == TYPE_TOAST) { + // Apps targeting SDK above N MR1 cannot arbitrary add toast windows. + if (doesAddToastWindowRequireToken(attrs.packageName, callingUid, + parentWindow)) { + Slog.w(TAG_WM, "Attempted to add a toast window with unknown token " + + attrs.token + ". Aborting."); + return WindowManagerGlobal.ADD_BAD_APP_TOKEN; + } + } + final IBinder binder = attrs.token != null ? attrs.token : client.asBinder(); + token = new WindowToken(this, binder, type, false, displayContent, + session.mCanAddInternalSystemWindow);//3 + } else if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {//4 + atoken = token.asAppWindowToken();//5 + if (atoken == null) { + Slog.w(TAG_WM, "Attempted to add window with non-application token " + + token + ". Aborting."); + return WindowManagerGlobal.ADD_NOT_APP_TOKEN; + } else if (atoken.removed) { + Slog.w(TAG_WM, "Attempted to add window with exiting application token " + + token + ". Aborting."); + return WindowManagerGlobal.ADD_APP_EXITING; + } + } else if (rootType == TYPE_INPUT_METHOD) { + if (token.windowType != TYPE_INPUT_METHOD) { + Slog.w(TAG_WM, "Attempted to add input method window with bad token " + + attrs.token + ". Aborting."); + return WindowManagerGlobal.ADD_BAD_APP_TOKEN; + } + } + ... +``` + +注释1处通过displayContent的getWindowToken方法来得到WindowToken。注释2处,如果有父窗口就将父窗口的type值赋值给rootType,如果没有将当前窗口的type值赋值给rootType。接下来如果WindowToken为null,则根据rootType或者type的值进行区分判断,如果rootType值等于TYPE_INPUT_METHOD、TYPE_WALLPAPER等值时,则返回状态值WindowManagerGlobal.ADD_BAD_APP_TOKEN,说明rootType值等于TYPE_INPUT_METHOD、TYPE_WALLPAPER等值时是不允许WindowToken为null的。通过多次的条件判断筛选,最后会在注释3处隐式创建WindowToken,这说明当我们添加窗口时是可以不向WMS提供WindowToken的,前提是rootType和type的值不为前面条件判断筛选的值。WindowToken隐式和显式的创建肯定是要加以区分的,注释3处的第4个参数为false就代表这个WindowToken是隐式创建的。接下来的代码逻辑就是WindowToken不为null的情况,根据rootType和type的值进行判断,比如在注释4处判断如果窗口为应用程序窗口,在注释5处会将WindowToken转换为专门针对应用程序窗口的AppWindowToken,然后根据AppWindowToken的值进行后续的判断。 + +#### addWindow方法part3 + +```java + ... +final WindowState win = new WindowState(this, session, client, token, parentWindow, + appOp[0], seq, attrs, viewVisibility, session.mUid, + session.mCanAddInternalSystemWindow);//1 + if (win.mDeathRecipient == null) {//2 + // Client has apparently died, so there is no reason to + // continue. + Slog.w(TAG_WM, "Adding window client " + client.asBinder() + + " that is dead, aborting."); + return WindowManagerGlobal.ADD_APP_EXITING; + } + + if (win.getDisplayContent() == null) {//3 + Slog.w(TAG_WM, "Adding window to Display that has been removed."); + return WindowManagerGlobal.ADD_INVALID_DISPLAY; + } + + mPolicy.adjustWindowParamsLw(win.mAttrs);//4 + win.setShowToOwnerOnlyLocked(mPolicy.checkShowToOwnerOnly(attrs)); + res = mPolicy.prepareAddWindowLw(win, attrs);//5 + ... + win.attach(); + mWindowMap.put(client.asBinder(), win);//6 + if (win.mAppOp != AppOpsManager.OP_NONE) { + int startOpResult = mAppOps.startOpNoThrow(win.mAppOp, win.getOwningUid(), + win.getOwningPackage()); + if ((startOpResult != AppOpsManager.MODE_ALLOWED) && + (startOpResult != AppOpsManager.MODE_DEFAULT)) { + win.setAppOpVisibilityLw(false); + } + } + + final AppWindowToken aToken = token.asAppWindowToken(); + if (type == TYPE_APPLICATION_STARTING && aToken != null) { + aToken.startingWindow = win; + if (DEBUG_STARTING_WINDOW) Slog.v (TAG_WM, "addWindow: " + aToken + + " startingWindow=" + win); + } + + boolean imMayMove = true; + win.mToken.addWindow(win);//7 + if (type == TYPE_INPUT_METHOD) { + win.mGivenInsetsPending = true; + setInputMethodWindowLocked(win); + imMayMove = false; + } else if (type == TYPE_INPUT_METHOD_DIALOG) { + displayContent.computeImeTarget(true /* updateImeTarget */); + imMayMove = false; + } else { + if (type == TYPE_WALLPAPER) { + displayContent.mWallpaperController.clearLastWallpaperTimeoutTime(); + displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; + } else if ((attrs.flags&FLAG_SHOW_WALLPAPER) != 0) { + displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; + } else if (displayContent.mWallpaperController.isBelowWallpaperTarget(win)) { + displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; + } + } + ... +``` + +在注释1处创建了WindowState,它存有窗口的所有的状态信息,在WMS中它代表一个窗口。从WindowState传入的参数,可以发现WindowState中包含了WMS、Session、WindowToken、父类的WindowState、LayoutParams等信息。紧接着在注释2和3处分别判断请求添加窗口的客户端是否已经死亡、窗口的DisplayContent是否为null,如果是则不会再执行下面的代码逻辑。注释4处调用了WMP的adjustWindowParamsLw方法,该方法的实现在PhoneWindowManager中,会根据窗口的type对窗口的LayoutParams的一些成员变量进行修改。注释5处调用WMP的prepareAddWindowLw方法,用于准备将窗口添加到系统中。 +注释6处将WindowState添加到mWindowMap中。注释7处将WindowState添加到该WindowState对应的WindowToken中(实际是保存在WindowToken的父类WindowContainer中),这样WindowToken就包含了相同组件的WindowState。 + +#### addWindow方法总结 + +addWindow方法分了3个部分来进行讲解,主要就是做了下面4件事: + +1. 对所要添加的窗口进行检查,如果窗口不满足一些条件,就不会再执行下面的代码逻辑。 +2. WindowToken相关的处理,比如有的窗口类型需要提供WindowToken,没有提供的话就不会执行下面的代码逻辑,有的窗口类型则需要由WMS隐式创建WindowToken。 +3. WindowState的创建和相关处理,将WindowToken和WindowState相关联。 +4. 创建和配置DisplayContent,完成窗口添加到系统前的准备工作。 + +### 结语 + +在本篇文章中我们首先学习了WMS的重要成员,了解这些成员有利于对WMS的进一步分析。接下来我们又学习了Window的添加过程的WMS部分,将addWindow方法分为了3个部分来进行讲解,从addWindow方法我们得知WMS有3个重要的类分别是WindowToken、WindowState和DisplayContent,关于它们会在本系列后续的文章中进行介绍。 + +## WMS(三):Window的删除过程 + +在本系列文章中,我提到过:Window的操作分为两大部分,一部分是WindowManager处理部分,另一部分是WMS处理部分,Window的删除过程也不例外,本篇文章会介绍Window的删除过程,包括了两大处理部分的内容。 + +### Window的删除过程 + +和[Android解析WindowManagerService(二)WMS的重要成员和Window的添加过程](https://link.juejin.cn?target=http%3A%2F%2Fliuwangshu.cn%2Fframework%2Fwms%2F2-wms-member.html)这篇文章中Window的创建和更新过程类似,要删除Window需要先调用WindowManagerImpl的removeView方法,removeView方法中又会调用WindowManagerGlobal的removeView方法,我们就从这里开始讲起。为了表述的更易于理解,本文将要删除的Window(View)简称为V。WindowManagerGlobal的removeView方法如下所示。 + +**frameworks/base/core/java/android/view/WindowManagerGlobal.java** + +```java +public void removeView(View view, boolean immediate) { + if (view == null) { + throw new IllegalArgumentException("view must not be null"); + } + synchronized (mLock) { + int index = findViewLocked(view, true);//1 + View curView = mRoots.get(index).getView(); + removeViewLocked(index, immediate);//2 + if (curView == view) { + return; + } + throw new IllegalStateException("Calling with view " + view + + " but the ViewAncestor is attached to " + curView); + } + } +``` + +注释1处找到要V在View列表中的索引,在注释2处调用了removeViewLocked方法并将这个索引传进去,如下所示。 **frameworks/base/core/java/android/view/WindowManagerGlobal.java** + +```java +private void removeViewLocked(int index, boolean immediate) { + ViewRootImpl root = mRoots.get(index);//1 + View view = root.getView(); + if (view != null) { + InputMethodManager imm = InputMethodManager.getInstance();//2 + if (imm != null) { + imm.windowDismissed(mViews.get(index).getWindowToken());//3 + } + } + boolean deferred = root.die(immediate);//4 + if (view != null) { + view.assignParent(null); + if (deferred) { + mDyingViews.add(view); + } + } + } +``` + +注释1处根据传入的索引在ViewRootImpl列表中获得V的ViewRootImpl。注释2处得到InputMethodManager实例,如果InputMethodManager实例不为null则在注释3处调用InputMethodManager的windowDismissed方法来结束V的输入法相关的逻辑。注释4处调用ViewRootImpl 的die方法,如下所示。 + +**frameworks/base/core/java/android/view/ViewRootImpl.java** + +```java +boolean die(boolean immediate) { + //die方法需要立即执行并且此时ViewRootImpl不在执行performTraversals方法 + if (immediate && !mIsInTraversal) {//1 + doDie();//2 + return false; + } + if (!mIsDrawing) { + destroyHardwareRenderer(); + } else { + Log.e(mTag, "Attempting to destroy the window while drawing!\n" + + " window=" + this + ", title=" + mWindowAttributes.getTitle()); + } + mHandler.sendEmptyMessage(MSG_DIE); + return true; + } +``` + +注释1处如果immediate为ture(需要立即执行),并且mIsInTraversal值为false则执行注释2处的代码,mIsInTraversal在执行ViewRootImpl的performTraversals方法时会被设置为true,在performTraversals方法执行完时被设置为false,因此注释1处可以理解为die方法需要立即执行并且此时ViewRootImpl不在执行performTraversals方法。注释2处的doDie方法如下所示。 + **frameworks/base/core/java/android/view/ViewRootImpl.java** + +```java +void doDie() { + //检查执行doDie方法的线程的正确性 + checkThread();//1 + if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface); + synchronized (this) { + if (mRemoved) {//2 + return; + } + mRemoved = true;//3 + if (mAdded) {//4 + dispatchDetachedFromWindow();//5 + } + if (mAdded && !mFirst) {//6 + destroyHardwareRenderer(); + if (mView != null) { + int viewVisibility = mView.getVisibility(); + boolean viewVisibilityChanged = mViewVisibility != viewVisibility; + if (mWindowAttributesChanged || viewVisibilityChanged) { + try { + if ((relayoutWindow(mWindowAttributes, viewVisibility, false) + & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) { + mWindowSession.finishDrawing(mWindow); + } + } catch (RemoteException e) { + } + } + mSurface.release(); + } + } + mAdded = false; + } + WindowManagerGlobal.getInstance().doRemoveView(this);//7 +} +``` + +注释1处用于检查执行doDie方法的线程的正确性,注释1的内部会判断执行doDie方法线程是否是创建V的原始线程,如果不是就会抛出异常,这是因为只有创建V的原始线程才能够操作V。注释2到注释3处的代码用于防止doDie方法被重复调用。注释4处V有子View就会调用dispatchDetachedFromWindow方法来销毁View。注释6处如果V有子View并且不是第一次被添加,就会执行后面的代码逻辑。注释7处的WindowManagerGlobal的doRemoveView方法,如下所示。 **frameworks/base/core/java/android/view/WindowManagerGlobal.java** + +```java +void doRemoveView(ViewRootImpl root) { + synchronized (mLock) { + final int index = mRoots.indexOf(root);//1 + if (index >= 0) { + mRoots.remove(index); + mParams.remove(index); + final View view = mViews.remove(index); + mDyingViews.remove(view); + } + } + if (ThreadedRenderer.sTrimForeground && ThreadedRenderer.isAvailable()) { + doTrimForeground(); + } + } +``` + +WindowManagerGlobal中维护了和 Window操作相关的三个列表,doRemoveView方法会从这三个列表中清除V对应的元素。注释1处找到V对应的ViewRootImpl在ViewRootImpl列表中的索引,接着根据这个索引从ViewRootImpl列表、布局参数列表和View列表中删除与V对应的元素。 我们接着回到ViewRootImpl的doDie方法,查看注释5处的dispatchDetachedFromWindow方法里做了什么: **frameworks/base/core/java/android/view/ViewRootImpl.java** + +```java +void dispatchDetachedFromWindow() { + ... + try { + mWindowSession.remove(mWindow); + } catch (RemoteException e) { + } + ... + } +``` + +dispatchDetachedFromWindow方法中主要调用了IWindowSession的remove方法,IWindowSession在Server端的实现为Session,Session的remove方法如下所示。 **frameworks/base/services/core/java/com/android/server/wm/Session.java** + +```java +public void remove(IWindow window) { + mService.removeWindow(this, window); + } +``` + +接着查看WMS的removeWindow方法: **frameworks/base/services/core/java/com/android/server/wm/WindowManagerService .java** + +```java +void removeWindow(Session session, IWindow client) { + synchronized(mWindowMap) { + WindowState win = windowForClientLocked(session, client, false);//1 + if (win == null) { + return; + } + win.removeIfPossible();//2 + } + } +``` + +注释1处用于获取Window对应的WindowState,WindowState用于保存窗口的信息,在WMS中它用来描述一个窗口。接着在注释2处调用WindowState的removeIfPossible方法,如下所示。 **frameworks/base/services/core/java/com/android/server/wm/WindowState.java** + +```java +Override +void removeIfPossible() { + super.removeIfPossible(); + removeIfPossible(false /*keepVisibleDeadWindow*/); +} +``` + +又会调用removeIfPossible方法,如下所示。 **frameworks/base/services/core/java/com/android/server/wm/WindowState.java** + +```java +private void removeIfPossible(boolean keepVisibleDeadWindow) { + ...条件判断过滤,满足其中一个条件就会return,推迟删除操作 + removeImmediately();//1 + if (wasVisible && mService.updateOrientationFromAppTokensLocked(false, displayId)) { + mService.mH.obtainMessage(SEND_NEW_CONFIGURATION, displayId).sendToTarget(); + } + mService.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, true /*updateInputWindows*/); + Binder.restoreCallingIdentity(origId); +} +``` + +removeIfPossible方法和它的名字一样,并不是直接执行删除操作,而是进行多个条件判断过滤,满足其中一个条件就会return,推迟删除操作。比如这时V正在运行一个动画,这时就得推迟删除操作,直到动画完成。通过这些条件判断过滤就会执行注释1处的removeImmediately方法: **frameworks/base/services/core/java/com/android/server/wm/WindowState.java** + +```java +@Override +void removeImmediately() { + super.removeImmediately(); + if (mRemoved) {//1 + if (DEBUG_ADD_REMOVE) Slog.v(TAG_WM, + "WS.removeImmediately: " + this + " Already removed..."); + return; + } + mRemoved = true;//2 + ... + mPolicy.removeWindowLw(this);//3 + disposeInputChannel(); + mWinAnimator.destroyDeferredSurfaceLocked(); + mWinAnimator.destroySurfaceLocked(); + mSession.windowRemovedLocked();//4 + try { + mClient.asBinder().unlinkToDeath(mDeathRecipient, 0); + } catch (RuntimeException e) { + } + mService.postWindowRemoveCleanupLocked(this);//5 +} +``` + +removeImmediately方法如同它的名字一样,用于立即进行删除操作。注释1处的mRemoved为true意味着正在执行删除Window操作,注释1到注释2处之间的代码用于防止重复删除操作。注释3处如果当前要删除的Window是StatusBar或者NavigationBar就会将这个Window从对应的控制器中删除。注释4处会将V对应的Session从WMS的`ArraySet mSessions`中删除并清除Session对应的SurfaceSession资源(SurfaceSession是SurfaceFlinger的一个连接,通过这个连接可以创建1个或者多个Surface并渲染到屏幕上 )。注释5处调用了WMS的postWindowRemoveCleanupLocked方法用于对V进行一些集中的清理工作,这里就不在继续深挖下去,有兴趣的同学可以自行查看源码。 + +Window的删除过程就讲到这里,虽然删除的操作逻辑比较复杂,但是可以简单的总结为以下4点: + +1. 检查删除线程的正确性,如果不正确就抛出异常。 +2. 从ViewRootImpl列表、布局参数列表和View列表中删除与V对应的元素。 +3. 判断是否可以直接执行删除操作,如果不能就推迟删除操作。 +4. 执行删除操作,清理和释放与V相关的一切资源。 + diff --git "a/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" "b/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" new file mode 100644 index 0000000..f01e10b --- /dev/null +++ "b/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" @@ -0,0 +1,9098 @@ +# Android音视频开发初级入门篇 + +## Android 音视频开发(一) : 通过三种方式绘制图片 + +在 [Android 音视频开发学习思路](http://www.cnblogs.com/renhui/p/7452572.html) 里面,我们写到了,想要逐步入门音视频开发,就需要一步步的去学习整理,并积累。本文是音视频开发积累的第一篇。 对应的要学习的内容是:在 Android 平台绘制一张图片,使用至少 3 种不同的 API,ImageView,SurfaceView,自定义 View。 + +### ImageView 绘制图片 + +这个想必做过Android开发的都知道如何去绘制了。很简单: + +``` +Bitmap bitmap = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().getPath() + File.separator + "11.jpg"); +imageView.setImageBitmap(bitmap); +``` + +很轻松,在界面上看到了我们绘制的图片。 + + + +### SurfaceView 绘制图片 + +这个比 ImageView 绘制图片稍微复杂一点点: + + + +``` +SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surface); +surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder surfaceHolder) { + + if (surfaceHolder == null) { + return; + } + + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setStyle(Paint.Style.STROKE); + + Bitmap bitmap = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().getPath() + File.separator + "11.jpg"); // 获取bitmap + Canvas canvas = surfaceHolder.lockCanvas(); // 先锁定当前surfaceView的画布 + canvas.drawBitmap(bitmap, 0, 0, paint); //执行绘制操作 + surfaceHolder.unlockCanvasAndPost(canvas); // 解除锁定并显示在界面上 + } + + @Override + public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { + + } + + @Override + public void surfaceDestroyed(SurfaceHolder surfaceHolder) { + + } +}); +``` + + + + + +### 自定义 View 绘制图片 + +这个有绘制自定义View经验的可以很轻松的完成,本人也简单整理过 [Android 自定义 View 绘制](http://www.cnblogs.com/renhui/p/7419827.html) 这一块的知识: + + + +``` +public class CustomView extends View { + + Paint paint = new Paint(); + Bitmap bitmap; + + public CustomView(Context context) { + super(context); + paint.setAntiAlias(true); + paint.setStyle(Paint.Style.STROKE); + bitmap = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().getPath() + File.separator + "11.jpg"); // 获取bitmap + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // 不建议在onDraw做任何分配内存的操作 + if (bitmap != null) { + canvas.drawBitmap(bitmap, 0, 0, paint); + } + } +} + +``` + +**注:别忘记了权限,\**否则是不会展示成功的。\**** + +``` + +``` + +这三种方式都成功了展示出来了,我们可以继续学习并整理后面的知识了 + +## Android 音视频开发(二):使用 AudioRecord 采集音频PCM并保存到文件 + +### AudioRecord API详解 + +AudioRecord是Android系统提供的用于实现录音的功能类。 + +要想了解这个类的具体的说明和用法,我们可以去看一下官方的文档: + +> AndioRecord类的主要功能是让各种JAVA应用能够管理音频资源,以便它们通过此类能够录制声音相关的硬件所收集的声音。此功能的实现就是通过”pulling”(读取)AudioRecord对象的声音数据来完成的。在录音过程中,应用所需要做的就是通过后面三个类方法中的一个去及时地获取AudioRecord对象的录音数据. AudioRecord类提供的三个获取声音数据的方法分别是read(byte[], int, int), read(short[], int, int), read(ByteBuffer, int). 无论选择使用那一个方法都必须事先设定方便用户的声音数据的存储格式。  开始录音的时候,AudioRecord需要初始化一个相关联的声音buffer, 这个buffer主要是用来保存新的声音数据。这个buffer的大小,我们可以在对象构造期间去指定。它表明一个AudioRecord对象还没有被读取(同步)声音数据前能录多长的音(即一次可以录制的声音容量)。声音数据从音频硬件中被读出,数据大小不超过整个录音数据的大小(可以分多次读出),即每次读取初始化buffer容量的数据。 + +实现Android录音的流程为: + +1. 构造一个AudioRecord对象,其中需要的最小录音缓存buffer大小可以通过*getMinBufferSize*方法得到。如果buffer容量过小,将导致对象构造的失败。 +2. 初始化一个buffer,该buffer大于等于AudioRecord对象用于写声音数据的buffer大小。 +3. 开始录音 +4. 创建一个数据流,一边从AudioRecord中读取声音数据到初始化的buffer,一边将buffer中数据导入数据流。 +5. 关闭数据流 +6. 停止录音 + +### 使用 AudioRecord 实现录音,并生成wav + +#### 创建一个AudioRecord对象 + +首先要声明一些全局的变量参数: + +``` +private AudioRecord audioRecord = null; // 声明 AudioRecord 对象 +private int recordBufSize = 0; // 声明recoordBufffer的大小字段 +``` + +获取buffer的大小并创建AudioRecord: + +``` +public void createAudioRecord() { +  recordBufSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, EncodingBitRate); //audioRecord能接受的最小的buffer大小 + audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, frequency, channelConfiguration, EncodingBitRate, recordBufSize); +} +``` + +#### 初始化一个buffer + +``` +byte data[] = new byte[recordBufSize]; +``` + +#### 开始录音 + +``` +audioRecord.startRecording(); +isRecording = true; +``` + + +#### 创建一个数据流,一边从AudioRecord中读取声音数据到初始化的buffer,一边将buffer中数据导入数据流。 + + + +``` +FileOutputStream os = null; + +try { + os = new FileOutputStream(filename); +} catch (FileNotFoundException e) { + e.printStackTrace(); +} +if (null != os) { + while (isRecording) { + read = audioRecord.read(data, 0, recordBufSize); +     // 如果读取音频数据没有出现错误,就将数据写入到文件 + if (AudioRecord.ERROR_INVALID_OPERATION != read) { + try { + os.write(data); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + try { + os.close(); + } catch (IOException e) { + e.printStackTrace(); + } +} +``` + + + +#### 关闭数据流 + +修改标志位:isRecording 为false,上面的while循环就自动停止了,数据流也就停止流动了,Stream也就被关闭了。 + +``` +isRecording = false; +``` + +#### 停止录音 + +**停止录音之后,注意要释放资源。** + +``` +if (null != audioRecord) { +  audioRecord.stop(); + audioRecord.release(); +  audioRecord = null; + recordingThread = null; +} +``` + +注:权限需求:**WRITE_EXTERNAL_STORAGE、RECORD_AUDIO** + +到现在基本的录音的流程就介绍完了。但是这时候,有人就提出问题来了: + +**1)、我按照流程,把音频数据都输出到文件里面了,停止录音后,打开此文件,发现不能播放,到底是为什么呢?** + +**答:按照流程走完了,数据是进去了,但是现在的文件里面的内容仅仅是最原始的音频数据,术语称为raw(中文解释是“原材料”或“未经处理的东西”),这时候,你让播放器去打开,它既不知道保存的格式是什么,又不知道如何进行解码操作。当然播放不了。** + +**2)、那如何才能在播放器中播放我录制的内容呢?** + +**答: 在文件的数据开头加入WAVE HEAD数据即可,也就是[文件头](https://baike.baidu.com/item/文件头)。只有加上文件头部的数据,播放器才能正确的知道里面的内容到底是什么,进而能够正常的解析并播放里面的内容。具体的头文件的描述,在**[Play a WAV file on an AudioTrack](http://mindtherobot.com/blog/580/android-audio-play-a-wav-file-on-an-audiotrack/)里面可以进行了解。 + +添加WAVE文件头的代码如下: + + + +``` +public class PcmToWavUtil { + + /** + * 缓存的音频大小 + */ + private int mBufferSize; + /** + * 采样率 + */ + private int mSampleRate; + /** + * 声道数 + */ + private int mChannel; + + + /** + * @param sampleRate sample rate、采样率 + * @param channel channel、声道 + * @param encoding Audio data format、音频格式 + */ + PcmToWavUtil(int sampleRate, int channel, int encoding) { + this.mSampleRate = sampleRate; + this.mChannel = channel; + this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, encoding); + } + + + /** + * pcm文件转wav文件 + * + * @param inFilename 源文件路径 + * @param outFilename 目标文件路径 + */ + public void pcmToWav(String inFilename, String outFilename) { + FileInputStream in; + FileOutputStream out; + long totalAudioLen; + long totalDataLen; + long longSampleRate = mSampleRate; + int channels = mChannel == AudioFormat.CHANNEL_IN_MONO ? 1 : 2; + long byteRate = 16 * mSampleRate * channels / 8; + byte[] data = new byte[mBufferSize]; + try { + in = new FileInputStream(inFilename); + out = new FileOutputStream(outFilename); + totalAudioLen = in.getChannel().size(); + totalDataLen = totalAudioLen + 36; + + writeWaveFileHeader(out, totalAudioLen, totalDataLen, + longSampleRate, channels, byteRate); + while (in.read(data) != -1) { + out.write(data); + } + in.close(); + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + + /** + * 加入wav文件头 + */ + private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen, + long totalDataLen, long longSampleRate, int channels, long byteRate) + throws IOException { + byte[] header = new byte[44]; + // RIFF/WAVE header + header[0] = 'R'; + header[1] = 'I'; + header[2] = 'F'; + header[3] = 'F'; + header[4] = (byte) (totalDataLen & 0xff); + header[5] = (byte) ((totalDataLen >> 8) & 0xff); + header[6] = (byte) ((totalDataLen >> 16) & 0xff); + header[7] = (byte) ((totalDataLen >> 24) & 0xff); + //WAVE + header[8] = 'W'; + header[9] = 'A'; + header[10] = 'V'; + header[11] = 'E'; + // 'fmt ' chunk + header[12] = 'f'; + header[13] = 'm'; + header[14] = 't'; + header[15] = ' '; + // 4 bytes: size of 'fmt ' chunk + header[16] = 16; + header[17] = 0; + header[18] = 0; + header[19] = 0; + // format = 1 + header[20] = 1; + header[21] = 0; + header[22] = (byte) channels; + header[23] = 0; + header[24] = (byte) (longSampleRate & 0xff); + header[25] = (byte) ((longSampleRate >> 8) & 0xff); + header[26] = (byte) ((longSampleRate >> 16) & 0xff); + header[27] = (byte) ((longSampleRate >> 24) & 0xff); + header[28] = (byte) (byteRate & 0xff); + header[29] = (byte) ((byteRate >> 8) & 0xff); + header[30] = (byte) ((byteRate >> 16) & 0xff); + header[31] = (byte) ((byteRate >> 24) & 0xff); + // block align + header[32] = (byte) (2 * 16 / 8); + header[33] = 0; + // bits per sample + header[34] = 16; + header[35] = 0; + //data + header[36] = 'd'; + header[37] = 'a'; + header[38] = 't'; + header[39] = 'a'; + header[40] = (byte) (totalAudioLen & 0xff); + header[41] = (byte) ((totalAudioLen >> 8) & 0xff); + header[42] = (byte) ((totalAudioLen >> 16) & 0xff); + header[43] = (byte) ((totalAudioLen >> 24) & 0xff); + out.write(header, 0, 44); + } +} +``` + +### 附言 + +Android SDK 提供了两套音频采集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风录入的音频数据进行编码压缩(如AMR、MP3等)并存成文件,而后者则更接近底层,能够更加自由灵活地控制,可以得到原始的一帧帧PCM音频数据。如果想简单地做一个录音机,录制成音频文件,则推荐使用 MediaRecorder,而如果需要对音频做进一步的算法处理、或者采用第三方的编码库进行压缩、以及网络传输等应用,则建议使用 AudioRecord,其实 MediaRecorder 底层也是调用了 AudioRecord 与 Android Framework 层的 AudioFlinger 进行交互的。直播中实时采集音频自然是要用`AudioRecord`了。 + +### 源码 + +https://github.com/renhui/AudioDemo + +## Android 音视频开发(三):使用 AudioTrack 播放PCM音频 + +### AudioTrack 基本使用 + +AudioTrack 类可以完成Android平台上音频数据的输出任务。AudioTrack有两种数据加载模式(MODE_STREAM和MODE_STATIC),对应的是数据加载模式和音频流类型, 对应着两种完全不同的使用场景。 + +- MODE_STREAM:在这种模式下,通过write一次次把音频数据写到AudioTrack中。这和平时通过write系统调用往文件中写数据类似,但这种工作方式每次都需要把数据从用户提供的Buffer中拷贝到AudioTrack内部的Buffer中,这在一定程度上会使引入延时。为解决这一问题,AudioTrack就引入了第二种模式。 +- MODE_STATIC:这种模式下,在play之前只需要把所有数据通过一次write调用传递到AudioTrack中的内部缓冲区,后续就不必再传递数据了。这种模式适用于像铃声这种内存占用量较小,延时要求较高的文件。但它也有一个缺点,就是一次write的数据不能太多,否则系统无法分配足够的内存来存储全部数据。 + +#### MODE_STATIC模式 + +MODE_STATIC模式输出音频的方式如下(**注意:如果采用STATIC模式,须先调用write写数据,然后再调用play。**): + + + +``` +public class AudioTrackPlayerDemoActivity extends Activity implements + OnClickListener { + + private static final String TAG = "AudioTrackPlayerDemoActivity"; + private Button button; + private byte[] audioData; + private AudioTrack audioTrack; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + super.setContentView(R.layout.main); + this.button = (Button) super.findViewById(R.id.play); + this.button.setOnClickListener(this); + this.button.setEnabled(false); + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + try { + InputStream in = getResources().openRawResource(R.raw.ding); + try { + ByteArrayOutputStream out = new ByteArrayOutputStream( + 264848); + for (int b; (b = in.read()) != -1;) { + out.write(b); + } + Log.d(TAG, "Got the data"); + audioData = out.toByteArray(); + } finally { + in.close(); + } + } catch (IOException e) { + Log.wtf(TAG, "Failed to read", e); + } + return null; + } + + @Override + protected void onPostExecute(Void v) { + Log.d(TAG, "Creating track..."); + button.setEnabled(true); + Log.d(TAG, "Enabled button"); + } + }.execute(); + } + + public void onClick(View view) { + this.button.setEnabled(false); + this.releaseAudioTrack(); + this.audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 44100, + AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, + audioData.length, AudioTrack.MODE_STATIC); + Log.d(TAG, "Writing audio data..."); + this.audioTrack.write(audioData, 0, audioData.length); + Log.d(TAG, "Starting playback"); + audioTrack.play(); + Log.d(TAG, "Playing"); + this.button.setEnabled(true); + } + + private void releaseAudioTrack() { + if (this.audioTrack != null) { + Log.d(TAG, "Stopping"); + audioTrack.stop(); + Log.d(TAG, "Releasing"); + audioTrack.release(); + Log.d(TAG, "Nulling"); + } + } + + public void onPause() { + super.onPause(); + this.releaseAudioTrack(); + } +} +``` + + + +#### MODE_STREAM模式 + +MODE_STREAM 模式输出音频的方式如下: + + + +``` +byte[] tempBuffer = new byte[bufferSize]; +int readCount = 0; +while (dis.available() > 0) { + readCount = dis.read(tempBuffer); + if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) { + continue; + } + if (readCount != 0 && readCount != -1) { + audioTrack.play(); + audioTrack.write(tempBuffer, 0, readCount); + } +} +``` + + + +### AudioTrack 详解 + +#### 音频流的类型 + +在AudioTrack构造函数中,会接触到AudioManager.STREAM_MUSIC这个参数。它的含义与Android系统对音频流的管理和分类有关。 + +Android将系统的声音分为好几种流类型,下面是几个常见的: + +· STREAM_ALARM:警告声 + +· STREAM_MUSIC:音乐声,例如music等 + +· STREAM_RING:铃声 + +· STREAM_SYSTEM:系统声音,例如低电提示音,锁屏音等 + +· STREAM_VOCIE_CALL:通话声 + +注意:上面这些类型的划分和音频数据本身并没有关系。例如MUSIC和RING类型都可以是某首MP3歌曲。另外,声音流类型的选择没有固定的标准,例如,铃声预览中的铃声可以设置为MUSIC类型。音频流类型的划分和Audio系统对音频的管理策略有关。 + + + +#### **Buffer分配和Frame的概念** + +在计算Buffer分配的大小的时候,我们经常用到的一个方法就是:getMinBufferSize。这个函数决定了应用层分配多大的数据Buffer。 + +``` +AudioTrack.getMinBufferSize(8000,//每秒8K个采样点 +   AudioFormat.CHANNEL_CONFIGURATION_STEREO,//双声道 + AudioFormat.ENCODING_PCM_16BIT); +``` + +从AudioTrack.getMinBufferSize开始追溯代码,可以发现在底层的代码中有一个很重要的概念:Frame(帧)。Frame是一个单位,用来描述数据量的多少。1单位的Frame等于1个采样点的字节数×声道数(比如PCM16,双声道的1个Frame等于2×2=4字节)。1个采样点只针对一个声道,而实际上可能会有一或多个声道。由于不能用一个独立的单位来表示全部声道一次采样的数据量,也就引出了Frame的概念。Frame的大小,就是一个采样点的字节数×声道数。另外,在目前的声卡驱动程序中,其内部缓冲区也是采用Frame作为单位来分配和管理的。 + +下面是追溯到的native层的方法: + + + +``` + // minBufCount表示缓冲区的最少个数,它以Frame作为单位 + uint32_t minBufCount = afLatency / ((1000 *afFrameCount)/afSamplingRate); + if(minBufCount < 2) minBufCount = 2;//至少要两个缓冲 + + //计算最小帧个数 + uint32_tminFrameCount = + (afFrameCount*sampleRateInHertz*minBufCount)/afSamplingRate; + //下面根据最小的FrameCount计算最小的缓冲大小 + intminBuffSize = minFrameCount //计算方法完全符合我们前面关于Frame的介绍 + * (audioFormat == javaAudioTrackFields.PCM16 ? 2 : 1) + * nbChannels; + + returnminBuffSize; +``` + + + +getMinBufSize会综合考虑硬件的情况(诸如是否支持采样率,硬件本身的延迟情况等)后,得出一个最小缓冲区的大小。一般我们分配的缓冲大小会是它的整数倍。 + +#### AudioTrack构造过程 + +每一个音频流对应着一个AudioTrack类的一个实例,每个AudioTrack会在创建时注册到 AudioFlinger中,由AudioFlinger把所有的AudioTrack进行混合(Mixer),然后输送到AudioHardware中进行播放,目前Android同时最多可以创建32个音频流,也就是说,Mixer最多会同时处理32个AudioTrack的数据流。 + +![img](https://images2017.cnblogs.com/blog/682616/201709/682616-20170903144420968-1755275961.png) + +### AudioTrack 与 MediaPlayer 的对比 + +播放声音可以用MediaPlayer和AudioTrack,两者都提供了Java API供应用开发者使用。虽然都可以播放声音,但两者还是有很大的区别的。 + +#### 区别 + +其中最大的区别是MediaPlayer可以播放多种格式的声音文件,例如MP3,AAC,WAV,OGG,MIDI等。MediaPlayer会在framework层创建对应的音频解码器。而AudioTrack只能播放已经解码的PCM流,如果对比支持的文件格式的话则是AudioTrack只支持wav格式的音频文件,因为wav格式的音频文件大部分都是PCM流。AudioTrack不创建解码器,所以只能播放不需要解码的wav文件。 + +#### 联系 + +MediaPlayer在framework层还是会创建AudioTrack,把解码后的PCM数流传递给AudioTrack,AudioTrack再传递给AudioFlinger进行混音,然后才传递给硬件播放,所以是MediaPlayer包含了AudioTrack。 + +#### SoundPool + +在接触Android音频播放API的时候,发现SoundPool也可以用于播放音频。下面是三者的使用场景:MediaPlayer 更加适合在后台长时间播放本地音乐文件或者在线的流式资源; SoundPool 则适合播放比较短的音频片段,比如游戏声音、按键声、铃声片段等等,它可以同时播放多个音频; 而 AudioTrack 则更接近底层,提供了非常强大的控制能力,支持低延迟播放,适合流媒体和VoIP语音电话等场景。 + +### 源码 + +[https://github.com/renhui/AudioDemo ](https://github.com/renhui/AudioDemo) + +## Android 音视频开发(四):使用 Camera API 采集视频数据 + +本文主要将的是:使用 Camera API 采集视频数据并保存到文件,分别使用 SurfaceView、TextureView 来预览 Camera 数据,取到 NV21 的数据回调。 + +注: 需要权限:**** + +### 预览 Camera 数据 + +做过Android开发的人一般都知道,有两种方法能够做到这一点:SurfaceView、TextureView。 + +下面是使用SurfaceView预览数据的方式: + + + +``` +SurfaceView surfaceView; +Camera camera; + +@Override +protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + surfaceView = (SurfaceView) findViewById(R.id.surface_view); + surfaceView.getHolder().addCallback(this); + + // 打开摄像头并将展示方向旋转90度 + camera = Camera.open(); + camera.setDisplayOrientation(90); + +} + +//------ Surface 预览 ------- +@Override +public void surfaceCreated(SurfaceHolder surfaceHolder) { + try { + camera.setPreviewDisplay(surfaceHolder); + camera.startPreview(); + } catch (IOException e) { + e.printStackTrace(); + } +} + + +@Override +public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int w, int h) { + +} + +@Override +public void surfaceDestroyed(SurfaceHolder surfaceHolder) { + camera.release(); +} +``` + + + +下面是使用TextureView预览数据的方式: + + + +``` + TextureView textureView; + Camera camera; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + textureView = (TextureView) findViewById(R.id.texture_view); + textureView.setSurfaceTextureListener(this);// 打开摄像头并将展示方向旋转90度 + camera = Camera.open(); + camera.setDisplayOrientation(90); + }  //------ Texture 预览 ------- + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) { + try { + camera.setPreviewTexture(surfaceTexture); + camera.startPreview(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) { + + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + camera.release(); + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + + } +``` + + + +### 取到 NV21 的数据回调 + +Android 中Google支持的 Camera Preview Callback的YUV常用格式有两种:一个是NV21,一个是YV12。Android一般默认使用YCbCr_420_SP的格式(NV21)。 + +我们可以配置数据回调的格式: + +``` +Camera.Parameters parameters = camera.getParameters(); +parameters.setPreviewFormat(ImageFormat.NV21); +camera.setParameters(parameters); +``` + +通过setPreviewCallback方法监听预览的回调: + +``` +camera.setPreviewCallback(new Camera.PreviewCallback() { + @Override + public void onPreviewFrame(byte[] bytes, Camera camera) { + + } +}); +``` + +这里面的Bytes的数据就是NV21格式的数据。 + +在后面的文章中,会对这些数据进行处理,来满足相关的需求场景。 + +一个音视频文件是由音频和视频组成的,我们可以通过MediaExtractor、MediaMuxer把音频或视频给单独抽取出来,抽取出来的音频和视频能单独播放; + +## Android 音视频开发(五):使用 MediaExtractor 和 MediaMuxer API 解析和封装 mp4 文件 + +### **MediaExtractor API介绍** + +**MediaExtractor的**作用是把音频和视频的数据进行分离。 + +**主要API介绍:** + +- setDataSource(String path):即可以设置本地文件又可以设置网络文件 +- getTrackCount():得到源文件通道数 +- getTrackFormat(int index):获取指定(index)的通道格式 +- getSampleTime():返回当前的时间戳 +- readSampleData(ByteBuffer byteBuf, int offset):把指定通道中的数据按偏移量读取到ByteBuffer中; +- advance():读取下一帧数据 +- release(): 读取结束后释放资源 + +使用示例: + + + +``` + MediaExtractor extractor = new MediaExtractor(); + extractor.setDataSource(...); + int numTracks = extractor.getTrackCount(); + for (int i = 0; i < numTracks; ++i) { + MediaFormat format = extractor.getTrackFormat(i); + String mime = format.getString(MediaFormat.KEY_MIME); + if (weAreInterestedInThisTrack) { + extractor.selectTrack(i); + } + } + ByteBuffer inputBuffer = ByteBuffer.allocate(...) + while (extractor.readSampleData(inputBuffer, ...) >= 0) { + int trackIndex = extractor.getSampleTrackIndex(); + long presentationTimeUs = extractor.getSampleTime(); + ... + extractor.advance(); + } + + extractor.release(); + extractor = null; +``` + + + + + +### MediaMuxer API介绍 + +MediaMuxer的作用是生成音频或视频文件;还可以把音频与视频混合成一个音视频文件。 + +**相关API介绍:** + +- MediaMuxer(String path, int format):path:输出文件的名称 format:输出文件的格式;当前只支持MP4格式; +- addTrack(MediaFormat format):添加通道;我们更多的是使用MediaCodec.getOutpurForma()或Extractor.getTrackFormat(int index)来获取MediaFormat;也可以自己创建; +- start():开始合成文件 +- writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo):把ByteBuffer中的数据写入到在构造器设置的文件中; +- stop():停止合成文件 +- release():释放资源 + + + +使用示例: + + + +``` +MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4); + // More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat() + // or MediaExtractor.getTrackFormat(). + MediaFormat audioFormat = new MediaFormat(...); + MediaFormat videoFormat = new MediaFormat(...); + int audioTrackIndex = muxer.addTrack(audioFormat); + int videoTrackIndex = muxer.addTrack(videoFormat); + ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize); + boolean finished = false; + BufferInfo bufferInfo = new BufferInfo(); + + muxer.start(); + while(!finished) { + // getInputBuffer() will fill the inputBuffer with one frame of encoded + // sample from either MediaCodec or MediaExtractor, set isAudioSample to + // true when the sample is audio data, set up all the fields of bufferInfo, + // and return true if there are no more samples. + finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo); + if (!finished) { + int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex; + muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo); + } + }; + muxer.stop(); + muxer.release(); +``` + + + + + +### 使用情境 + +#### 从MP4文件中提取视频并生成新的视频文件 + + + +``` +public class MainActivity extends AppCompatActivity { + + private static final String SDCARD_PATH = Environment.getExternalStorageDirectory().getPath(); + + private MediaExtractor mMediaExtractor; + private MediaMuxer mMediaMuxer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // 获取权限 + int checkWriteExternalPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); + int checkReadExternalPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);if (checkWriteExternalPermission != PackageManager.PERMISSION_GRANTED || + checkReadExternalPermission != PackageManager.PERMISSION_GRANTED) { + + ActivityCompat.requestPermissions(this, new String[]{ + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE}, 0); + } + + setContentView(R.layout.activity_main); + new Thread(new Runnable() { + @Override + public void run() { + try { + process(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }).start(); + } + + private boolean process() throws IOException { + mMediaExtractor = new MediaExtractor(); + mMediaExtractor.setDataSource(SDCARD_PATH + "/ss.mp4"); + + int mVideoTrackIndex = -1; + int framerate = 0; + for (int i = 0; i < mMediaExtractor.getTrackCount(); i++) { + MediaFormat format = mMediaExtractor.getTrackFormat(i); + String mime = format.getString(MediaFormat.KEY_MIME); + if (!mime.startsWith("video/")) { + continue; + } + framerate = format.getInteger(MediaFormat.KEY_FRAME_RATE); + mMediaExtractor.selectTrack(i); + mMediaMuxer = new MediaMuxer(SDCARD_PATH + "/ouput.mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + mVideoTrackIndex = mMediaMuxer.addTrack(format); + mMediaMuxer.start(); + } + + if (mMediaMuxer == null) { + return false; + } + + MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); + info.presentationTimeUs = 0; + ByteBuffer buffer = ByteBuffer.allocate(500 * 1024); + int sampleSize = 0; + while ((sampleSize = mMediaExtractor.readSampleData(buffer, 0)) > 0) { + + info.offset = 0; + info.size = sampleSize; + info.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME; + info.presentationTimeUs += 1000 * 1000 / framerate; + mMediaMuxer.writeSampleData(mVideoTrackIndex, buffer, info); + mMediaExtractor.advance(); + } + + mMediaExtractor.release(); + + mMediaMuxer.stop(); + mMediaMuxer.release(); + + return true; + } +} +``` + +## Android 音视频开发(六): MediaCodec API 详解 + +在学习了Android 音视频的基本的相关知识,并整理了相关的API之后,我们应该对基本的音视频有一定的轮廓了。 + +下面开始接触一个Android音视频中相当重要的一个API: **MediaCodec。**通过这个API,我们能够做很多Android音视频方面的工作,下面是我们学习这个API的时候,主要的方向: + +- 学习 MediaCodec API,完成音频 AAC 硬编、硬解 +- 学习 MediaCodec API,完成视频 H.264 的硬编、硬解 + +### MediaCodec 介绍 + +  MediaCodec类可以用于使用一些基本的多媒体编解码器(音视频编解码组件),它是Android基本的多媒体支持基础架构的一部分通常和 `MediaExtractor`, `MediaSync`, `MediaMuxer`, `MediaCrypto`, `MediaDrm`, `Image`, `Surface`, and `AudioTrack 一起使用。` + +一个编解码器可以处理输入的数据来产生输出的数据,编解码器使用一组输入和输出缓冲器来异步处理数据。你可以创建一个空的输入缓冲区,填充数据后发送到编解码器进行处理。编解码器使用输入的数据进行转换,然后输出到一个空的输出缓冲区。最后你获取到输出缓冲区的数据,消耗掉里面的数据,释放回编解码器。如果后续还有数据需要继续处理,编解码器就会重复这些操作。输出流程如下: + + ![img](https://images2017.cnblogs.com/blog/682616/201709/682616-20170912183105016-1579199179.png) + +**编解码器支持的数据类型:** + +  **编解码器能处理的数据类型为:** **压缩数据、原始音频数据和原始视频数据。**你可以通过ByteBuffers能够处理这三种数据,但是需要你提供一个Surface,用于对原始的视频数据进行展示,这样也能提高编解码的性能。Surface使用的是本地的视频缓冲区,这个缓冲区不映射或拷贝到ByteBuffers。这样的机制让编解码器的效率更高。通常在使用Surface的时候,无法访问原始的视频数据,但是你可以使用ImageReader访问解码后的原始视频帧。在使用ByteBuffer的模式下,您可以使用Image类和getInput/OutputImage(int)访问原始视频帧。 + +**编解码器的生命周期:** + +  主要的生命周期为:Stopped、Executing、Released。 + +- Stopped的状态下也分为三种子状态:Uninitialized、Configured、Error。 +- Executing的状态下也分为三种子状态:Flushed, Running、End-of-Stream。 + +下图是生命周期的说明图: + +![img](https://images2017.cnblogs.com/blog/682616/201709/682616-20170913105110891-222810539.png) + + + + 如图可以看到: + +1. 当创建编解码器的时候处于未初始化状态。首先你需要调用configure(…)方法让它处于Configured状态,然后调用start()方法让其处于Executing状态。在Executing状态下,你就可以使用上面提到的缓冲区来处理数据。 +2. Executing的状态下也分为三种子状态:Flushed, Running、End-of-Stream。在start() 调用后,编解码器处于Flushed状态,这个状态下它保存着所有的缓冲区。一旦第一个输入buffer出现了,编解码器就会自动运行到Running的状态。当带有end-of-stream标志的buffer进去后,编解码器会进入End-of-Stream状态,这种状态下编解码器不在接受输入buffer,但是仍然在产生输出的buffer。此时你可以调用flush()方法,将编解码器重置于Flushed状态。 +3. 调用stop()将编解码器返回到未初始化状态,然后可以重新配置。 完成使用编解码器后,您必须通过调用release()来释放它。 +4. 在极少数情况下,编解码器可能会遇到错误并转到错误状态。 这是使用来自排队操作的无效返回值或有时通过异常来传达的。 调用reset()使编解码器再次可用。 您可以从任何状态调用它来将编解码器移回未初始化状态。 否则,调用 release()动到终端释放状态。 + +### MediaCodec API 说明 + +MediaCodec可以处理具体的视频流,主要有这几个方法: + +- getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组 +- queueInputBuffer:输入流入队列 +- dequeueInputBuffer:从输入流队列中取数据进行编码操作 +- getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组 +- dequeueOutputBuffer:从输出队列中取出编码操作之后的数据 +- releaseOutputBuffer:处理完成,释放ByteBuffer数据 + +### MediaCodec 流控 + +#### 流控基本概念 + +流控就是流量控制。**为什么要控制,因为条件有限!**涉及到了 TCP 和视频编码: + +对 TCP 来说就是控制单位时间内发送数据包的数据量,对编码来说就是控制单位时间内输出数据的数据量。 + +- TCP 的限制条件是网络带宽,流控就是在避免造成或者加剧网络拥塞的前提下,尽可能利用网络带宽。带宽够、网络好,我们就加快速度发送数据包,出现了延迟增大、丢包之后,就放慢发包的速度(因为继续高速发包,可能会加剧网络拥塞,反而发得更慢)。 +- 视频编码的限制条件最初是解码器的能力,码率太高就会无法解码,后来随着 codec 的发展,解码能力不再是瓶颈,限制条件变成了传输带宽/文件大小,我们希望在控制数据量的前提下,画面质量尽可能高。 + +一般编码器都可以设置一个目标码率,但编码器的实际输出码率不会完全符合设置,因为在编码过程中实际可以控制的并不是最终输出的码率,而是编码过程中的一个量化参数(Quantization Parameter,QP),它和码率并没有固定的关系,而是取决于图像内容。 + +无论是要发送的 TCP 数据包,还是要编码的图像,都可能出现“尖峰”,也就是短时间内出现较大的数据量。TCP 面对尖峰,可以选择不为所动(尤其是网络已经拥塞的时候),这没有太大的问题,但如果视频编码也对尖峰不为所动,那图像质量就会大打折扣了。如果有几帧数据量特别大,但仍要把码率控制在原来的水平,那势必要损失更多的信息,因此图像失真就会更严重。 + +#### Android 硬编码流控 + +MediaCodec 流控相关的接口并不多,一是配置时设置目标码率和码率控制模式,二是动态调整目标码率(Android 19 版本以上)。 + +配置时指定目标码率和码率控制模式: + +``` +mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); +mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, +MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR); +mVideoCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); +``` + +码率控制模式有三种: + +- CQ 表示完全不控制码率,尽最大可能保证图像质量; +- CBR 表示编码器会尽量把输出码率控制为设定值,即我们前面提到的“不为所动”; +- VBR 表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低; + +动态调整目标码率: + +``` +Bundle param = new Bundle(); +param.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate); +mediaCodec.setParameters(param); +``` + +#### Android 流控策略选择 + +- 质量要求高、不在乎带宽、解码器支持码率剧烈波动的情况下,可以选择 CQ 码率控制策略。 +- VBR 输出码率会在一定范围内波动,对于小幅晃动,方块效应会有所改善,但对剧烈晃动仍无能为力;连续调低码率则会导致码率急剧下降,如果无法接受这个问题,那 VBR 就不是好的选择。 +- CBR 的优点是稳定可控,这样对实时性的保证有帮助。所以 WebRTC 开发中一般使用的是CBR。 + +## Android 音视频开发(七): 音视频录制流程总结 + +在前面我们学习和使用了[AudioRecord](http://www.cnblogs.com/renhui/p/7457321.html)、[AudioTrack](http://www.cnblogs.com/renhui/p/7463287.html)、[Camera](http://www.cnblogs.com/renhui/p/7472778.html)、[ MediaExtractor](http://www.cnblogs.com/renhui/p/7474096.html)、[MediaMuxer API](http://www.cnblogs.com/renhui/p/7474096.html)、[MediaCodec](http://www.cnblogs.com/renhui/p/7478527.html)。 学习和使用了上述的API之后,相信对Android系统的音视频处理有一定的经验和心得了。本文及后面的几篇文章做的事情就是将这些知识串联起来,做一些稍微复杂的事情。 + +### 流程分析 + +#### 需求说明 + +我们需要做的事情就是:**串联整个音视频录制流程,完成音视频的采集、编码、封包成 mp4 输出。** + +#### 实现方式 + +Android音视频采集的方法:预览用SurfaceView,视频采集用Camera类,音频采集用AudioRecord。 + +#### 数据处理思路 + +使用MediaCodec 类进行编码压缩,视频压缩为H.264,音频压缩为aac,使用MediaMuxer 将音视频合成为MP4。 + +### 实现过程 + +#### 收集Camera数据,并转码为H264存储到文件 + +在收集数据之前,对Camera设置一些参数,方便收集后进行数据处理: + +``` +Camera.Parameters parameters = camera.getParameters(); +parameters.setPreviewFormat(ImageFormat.NV21); +parameters.setPreviewSize(1280, 720); +``` + +然后设置PreviewCallback: + +``` +camera.setPreviewCallback(this); +``` + +就可以获取到Camera的原始NV21数据: + +``` +onPreviewFrame(byte[] bytes, Camera camera) +``` + +在创建一个H264Encoder类,在里面进行编码操作,并将编码后的数据存储到文件: + + + +``` +new Thread(new Runnable() { + + @Override + public void run() { + isRuning = true; + byte[] input = null; + long pts = 0; + long generateIndex = 0; + + while (isRuning) { + if (yuv420Queue.size() > 0) { + input = yuv420Queue.poll(); + byte[] yuv420sp = new byte[width * height * 3 / 2]; + // 必须要转格式,否则录制的内容播放出来为绿屏 + NV21ToNV12(input, yuv420sp, width, height); + input = yuv420sp; + } + if (input != null) { + try { + ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers(); + ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers(); + int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1); + if (inputBufferIndex >= 0) { + pts = computePresentationTime(generateIndex); + ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; + inputBuffer.clear(); + inputBuffer.put(input); + mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, System.currentTimeMillis(), 0); + generateIndex += 1; + } + + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC); + while (outputBufferIndex >= 0) { + ByteBuffer outputBuffer = outputBuffers[outputBufferIndex]; + byte[] outData = new byte[bufferInfo.size]; + outputBuffer.get(outData); + if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { + configbyte = new byte[bufferInfo.size]; + configbyte = outData; + } else if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_SYNC_FRAME) { + byte[] keyframe = new byte[bufferInfo.size + configbyte.length]; + System.arraycopy(configbyte, 0, keyframe, 0, configbyte.length); + System.arraycopy(outData, 0, keyframe, configbyte.length, outData.length); + outputStream.write(keyframe, 0, keyframe.length); + } else { + outputStream.write(outData, 0, outData.length); + } + + mediaCodec.releaseOutputBuffer(outputBufferIndex, false); + outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC); + } + + } catch (Throwable t) { + t.printStackTrace(); + } + } else { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + // 停止编解码器并释放资源 + try { + mediaCodec.stop(); + mediaCodec.release(); + } catch (Exception e) { + e.printStackTrace(); + } + + // 关闭数据流 + try { + outputStream.flush(); + outputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +}).start(); +``` + + + +当结束编码的时候,需要将相关的资源释放掉: + + + +``` +// 停止编解码器并释放资源 +try { + mediaCodec.stop(); + mediaCodec.release(); +} catch (Exception e) { + e.printStackTrace(); +} + +// 关闭数据流 +try { + outputStream.flush(); + outputStream.close(); +} catch (IOException e) { + e.printStackTrace(); +} +``` + + + +此时,我们做到了将视频内容采集-->编码-->存储文件。但这个仅仅是对[Android 音视频开发(四):使用 Camera API 采集视频数据](http://www.cnblogs.com/renhui/p/7472778.html)的延伸,但是很有必要。因为在前面学习了如何采集音频,如何使用MediaCodec去处理音视频,如何使用MediaMuxer去混合音视频。 + +**示例代码:https://github.com/renhui/AndroidRecorder/releases/tag/only_h264_video** + +下面我们在当前的的基础上继续完善,即将音视频采集并混合为音视频。 + +#### 音视频采集+混合,存储到文件 + +基本完成思路已经在2.1的结尾处坐了说明,下面贴一下demo的链接: + +**示例代码:https://github.com/renhui/AndroidRecorder/releases/tag/h264_video_audio** + +# Android音视频开发中级进阶篇 + +## OpenGL ES + +### Android OpenGL ES 开发(一): OpenGL ES 介绍 + +#### 简介OpenGL ES + +谈到OpenGL ES,首先我们应该先去了解一下Android的基本架构,基本架构下图: +![img](https://images2017.cnblogs.com/blog/682616/201712/682616-20171206183313409-1958232808.png) +在这里我们可以找到Libraries里面有我们目前要接触的库,即OpenGL ES。 + +根据上图可以知道Android 目前是支持使用开放的图形库的,特别是通过OpenGL ES API来支持高性能的2D和3D图形。OpenGL是一个跨平台的图形API。为3D图形处理硬件指定了一个标准的软件接口。OpenGL ES 是适用于嵌入式设备的OpenGL规范。 + +Android 支持OpenGL ES API版本的详细状态是: + +> - OpenGL ES 1.0 和 1.1 能够被Android 1.0及以上版本支持 +> - OpenGL ES 2.0 能够被Android 2.2及更高版本支持 +> - OpenGL ES 3.0 能够被Android 4.3及更高版本支持 +> - OpenGL ES 3.1 能够被Android 5.0及以上版本支持 + +#### 基本介绍 + +Android 能够通过framework框架提供的API或者NDK来支持OpenGL。本文重点介绍框架提供的接口来使用OpenGL的方式,有关于NDK方面的信息,可以自行去官方文档进行了解。 + +在Android框架里面两个基本的类允许你使用OpenGL ES API创建和操作图形: GLSurfaceView 和 GLSurfaceView.Renderer。如果您的目标是在Android程序中使用OpenGL,那么首先需要做的事情就是了解这两个类。 + +##### GLSurfaceView + +这是一个视图类,你可以使用OpenGL API来绘制和操作图形对象,这一点在功能上很类似于SurfaceView。你可以通过创建一个SurfaceView的实例并添加你的渲染器来使用这个类。但是如果想要捕捉触摸屏的事件,则应该扩展GLSurfaceView以实现触摸监听器。关于实现触摸监听器的方式,我们会在后面的文章中进行讲解。 + +##### GLSurfaceView.Renderer + +此接口定义了在GLSurfaceView中绘制图形所需的方法。您必须将此接口的实现作为单独的类提供,并使用GLSurfaceView.setRenderer()将其附加到您的GLSurfaceView实例。 +GLSurfaceView.Renderer要求实现以下方法: + +> - onSurfaceCreated():创建GLSurfaceView时,系统调用一次该方法。使用此方法执行只需要执行一次的操作,例如设置OpenGL环境参数或初始化OpenGL图形对象。 +> - onDrawFrame():系统在每次重画GLSurfaceView时调用这个方法。使用此方法作为绘制(和重新绘制)图形对象的主要执行方法。 +> - onSurfaceChanged():当GLSurfaceView的发生变化时,系统调用此方法,这些变化包括GLSurfaceView的大小或设备屏幕方向的变化。例如:设备从纵向变为横向时,系统调用此方法。我们应该使用此方法来响应GLSurfaceView容器的改变。 + +### Android OpenGL ES开发(二) : OpenGL ES 环境搭建 + +#### 环境搭建目的 + +为了在Android应用程序中使用OpenGL ES绘制图形,必须要为他们创建一个视图容器。其中最直接或者最常用的方式就是实现一个GLSurfaceView和一个GLSurfaceView.Renderer。GLSurfaceView是用OpenGL绘制图形的视图容器,GLSurfaceView.Renderer控制在该视图内绘制的内容。 + +下面将讲解如何使用GLSurfaceView 和 GLSurfaceView.Renderer 在一个简单的应用程序的Activity上面做一个最小的实现。 + +#### 在Manifest中声明OpenGL ES使用 + +为了让你的应用程序能够使用OpenGL ES 2.0的API,你必须添加以下声明到manifest: + +```xml + +``` + +如果你的应用程序需要使用纹理压缩,你还需要声明你的应用程序需要支持哪种压缩格式,以便他们安装在兼容的设备上。 + +```xml + + +``` + +关于更多的纹理压缩格式的知识,可以到 https://developer.android.com/guide/topics/graphics/opengl.html###textures 做进一步的了解。 + +#### 创建一个Activity 用于展示OpenGL ES 图形 + +使用OpenGL ES的应用程序的Activity和其他应用程的Activity一样,不同的地方在于你设置的Activity的布局。在许多使用OpenGL ES的app中,你可以添加TextView,Button和ListView,还可以添加GLSurfaceView。 + +下面的代码展示了使用GLSurfaceView做为主视图的基本实现: + +```java +public class OpenGLES20Activity extends Activity { + + private GLSurfaceView mGLView; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Create a GLSurfaceView instance and set it + // as the ContentView for this Activity. + mGLView = new MyGLSurfaceView(this); + setContentView(mGLView); + } +} +``` + +注意:OpenGL ES 2.0 需要的Android版本是2.2及以上,请确保你的Android项目针对的版本是否符合。 + +#### 创建GLSurfaceView对象 + +GLSurfaceView是一个特殊的View,通过这个View你可以绘制OpenGL图像。但是View本身没有做太多的事情,主要的绘制是通过设置在View里面的GLSurfaceView.Renderer 来控制的。实际上,创建这个对象的代码是很少的,你能会想尝试跳过extends的操作,只去创建一个没有被修改的GLSurfaceView实例,但是不建议这样去做。因为在某些情况下,你需要扩展这个类来捕获触摸的事件,捕获触摸的事件的方式会在后面的文章里面做介绍。 +GLSurfaceView的基本代码很少,为了快速的实现,通常会在使用它的Activity中创建一个内部类来做实现: + +```java +class MyGLSurfaceView extends GLSurfaceView { + + private final MyGLRenderer mRenderer; + + public MyGLSurfaceView(Context context){ + super(context); + + // Create an OpenGL ES 2.0 context + setEGLContextClientVersion(2); + + mRenderer = new MyGLRenderer(); + + // Set the Renderer for drawing on the GLSurfaceView + setRenderer(mRenderer); + } +} +``` + +你可以通过设置GLSurfaceView.RENDERMODE_WHEN_DIRTY来让你的GLSurfaceView监听到数据变化的时候再去刷新,即修改GLSurfaceView的渲染模式。这个设置可以防止重绘GLSurfaceView,直到你调用了requestRender(),这个设置在默写层面上来说,对你的APP是更有好处的。 + +#### 创建一个Renderer类 + +实现了GLSurfaceView.Renderer 类才是真正算是开始能够在应用中使用OpenGL ES。这个类控制着与它关联的GLSurfaceView 绘制的内容。在renderer 里面有三个方法能够被Android系统调用,以便知道在GLSurfaceView绘制什么以及如何绘制 + +> - onSurfaceCreated() - 在View的OpenGL环境被创建的时候调用。 +> - onDrawFrame() - 每一次View的重绘都会调用 +> - onSurfaceChanged() - 如果视图的几何形状发生变化(例如,当设备的屏幕方向改变时),则调用此方法。 + +下面是使用OpenGL ES 渲染器的基本实现,仅仅做的事情就是在GLSurfaceView绘制一个黑色背景。 + +```java +public class MyGLRenderer implements GLSurfaceView.Renderer { + + public void onSurfaceCreated(GL10 unused, EGLConfig config) { + // Set the background frame color + GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + } + + public void onDrawFrame(GL10 unused) { + // Redraw background color + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + } + + public void onSurfaceChanged(GL10 unused, int width, int height) { + GLES20.glViewport(0, 0, width, height); + } +} +``` + +#### 总结 + +上述的内容就是基本的OpenGL ES基本的环境配置,本文的代码仅仅是创建一个简单的Android应用然后使用OpenGL展示一个黑板。虽然没有做其他更加有趣的事情,但是,通过创建这些类,你应该已经拥有了使用OpenGL绘制图形元素的基础了。 + +注:你可能很好奇为什么在使用OpenGL ES 2.0的API的时候会看到GL10的参数,因为这些方法签名被简单地用于2.0 API这样可以保持Android框架代码的简单。 + +如果你熟悉OpenGL的API,现在你应该可以在你的APP里面创建一个OpenGL ES的环境,并开始进行画图了。但是如果需要更多的帮助来使用OpenGL,就请期待下面的文章吧。 + +### Android OpenGL ES 开发(三):OpenGL ES定义形状 + +在上篇文章,我们能够配置好基本的Android OpenGL 使用的环境。但是如果我们不了解OpenGL ES如何定义图像的一些基本知识就使用OpenGL ES进行绘图还是有点棘手的。所以能够在OpenGL ES的View里面定义要绘制的形状是进行高端绘图操作的第一步。 +本文主要做的事情就是为了讲解Android设备屏幕相关的OpenGL ES坐标系统,定义形状,形状面的基础知识,以及定义三角形和正方形。 + +#### 定义三角形 + +OpenGL ES允许你使用三维空间坐标系定义绘制的图像,所以你在绘制一个三角形之前必须要先定义它的坐标。在OpenGL中,这样做的典型方法是为坐标定义浮点数的顶点数组。 +为了获得最大的效率,可以将这些坐标写入ByteBuffer,并传递到OpenGL ES图形管道进行处理。 + +```java +public class Triangle { + + private FloatBuffer vertexBuffer; + + // number of coordinates per vertex in this array + static final int COORDS_PER_VERTEX = 3; + static float triangleCoords[] = { // in counterclockwise order: + 0.0f, 0.622008459f, 0.0f, // top + -0.5f, -0.311004243f, 0.0f, // bottom left + 0.5f, -0.311004243f, 0.0f // bottom right + }; + + // Set color with red, green, blue and alpha (opacity) values + float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f }; + + public Triangle() { + // initialize vertex byte buffer for shape coordinates + ByteBuffer bb = ByteBuffer.allocateDirect( + // (number of coordinate values * 4 bytes per float) + triangleCoords.length * 4); + // use the device hardware's native byte order + bb.order(ByteOrder.nativeOrder()); + + // create a floating point buffer from the ByteBuffer + vertexBuffer = bb.asFloatBuffer(); + // add the coordinates to the FloatBuffer + vertexBuffer.put(triangleCoords); + // set the buffer to read the first coordinate + vertexBuffer.position(0); + } +} +``` + +默认情况下,OpenGL ES采用坐标系,[0,0,0](X,Y,Z)指定GLSurfaceView框架的中心,[1,1,0]是框架的右上角,[ - 1,-1,0]是框架的左下角。 有关此坐标系的说明,请参阅OpenGL ES开发人员指南。 + +请注意,此图形的坐标以逆时针顺序定义。 绘图顺序非常重要,因为它定义了哪一面是您通常想要绘制的图形的正面,以及背面。关于这块相关的更多的内容,可以去查看一下相关的OpenGL ES 文档。 + +#### 定义正方形 + +可以看到,在OpenGL里面定义一个三角形很简单。但是如果你想要得到一个更复杂一点的东西呢?比如一个正方形?能够找到很多办法来作到这一点,但是在OpenGL里面绘制这个图形的方式是将两个三角形画在一起。 +![img](https://images2017.cnblogs.com/blog/682616/201712/682616-20171208130636999-609916321.png) +同样,你应该以逆时针的顺序为这两个代表这个形状的三角形定义顶点,并将这些值放在一个ByteBuffer中。 为避免定义每个三角形共享的两个坐标两次,请使用图纸列表告诉OpenGL ES图形管道如何绘制这些顶点。 这是这个形状的代码: + +```java +public class Square { + + private FloatBuffer vertexBuffer; + private ShortBuffer drawListBuffer; + + // number of coordinates per vertex in this array + static final int COORDS_PER_VERTEX = 3; + static float squareCoords[] = { + -0.5f, 0.5f, 0.0f, // top left + -0.5f, -0.5f, 0.0f, // bottom left + 0.5f, -0.5f, 0.0f, // bottom right + 0.5f, 0.5f, 0.0f }; // top right + + private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices + + public Square() { + // initialize vertex byte buffer for shape coordinates + ByteBuffer bb = ByteBuffer.allocateDirect( + // (### of coordinate values * 4 bytes per float) + squareCoords.length * 4); + bb.order(ByteOrder.nativeOrder()); + vertexBuffer = bb.asFloatBuffer(); + vertexBuffer.put(squareCoords); + vertexBuffer.position(0); + + // initialize byte buffer for the draw list + ByteBuffer dlb = ByteBuffer.allocateDirect( + // (### of coordinate values * 2 bytes per short) + drawOrder.length * 2); + dlb.order(ByteOrder.nativeOrder()); + drawListBuffer = dlb.asShortBuffer(); + drawListBuffer.put(drawOrder); + drawListBuffer.position(0); + } +} +``` + +这个例子让你了解用OpenGL创建更复杂的形状的过程。 一般来说,您使用三角形的集合来绘制对象。下面的文章里面,将讲述如何在屏幕上绘制这些形状。 + +### Android OpenGL ES开发(四) : OpenGL ES绘制形状 + +在上文中,我们使用OpenGL定义了能够被绘制出来的形状了,现在我们想绘制出来它们。使用OpenGLES 2.0来绘制形状会比你想象的需要更多的代码。因为OpenGL的API提供了大量的对渲染管线的控制能力。 + +本文就将讲述如何使用OpenGL ES 2.0 API来绘制出来我们上节定义的形状。 + +#### 初始化形状 + +在你做任何绘制操作之前,你必须要初始化并加载你准备绘制的形状。除非形状的结构(指原始的坐标)在执行过程中发生改变,你都应该在你的Renderer的方法onSurfaceCreated()中进行内存和效率方面的初始化工作。 + +```java +public class MyGLRenderer implements GLSurfaceView.Renderer { + + ... + private Triangle mTriangle; + private Square mSquare; + + public void onSurfaceCreated(GL10 unused, EGLConfig config) { + ... + + // initialize a triangle + mTriangle = new Triangle(); + // initialize a square + mSquare = new Square(); + } + ... +} +``` + +#### 绘制形状 + +使用OpenGLES 2.0画一个定义好的形状需要比较多的代码,因为你必须为图形渲染管线提供一大堆信息。特别的,你必须定义以下几个东西: + +> - Vertex Shader - 用于渲染形状的顶点的OpenGLES 图形代码。 +> - Fragment Shader - 用于渲染形状的外观(颜色或纹理)的OpenGLES 代码。 +> - Program - 一个OpenGLES对象,包含了你想要用来绘制一个或多个形状的shader。 + +你至少需要一个vertexshader来绘制一个形状和一个fragmentshader来为形状上色。这些形状必须被编译然后被添加到一个OpenGLES program中,program之后被用来绘制形状。下面是一个展示如何定义一个可以用来绘制形状的基本shader的例子: + +```java +public class Triangle { + + private final String vertexShaderCode = + "attribute vec4 vPosition;" + + "void main() {" + + " gl_Position = vPosition;" + + "}"; + + private final String fragmentShaderCode = + "precision mediump float;" + + "uniform vec4 vColor;" + + "void main() {" + + " gl_FragColor = vColor;" + + "}"; + + ... +} +``` + +Shader们包含了OpenGLShading Language (GLSL)代码,必须在使用前编译。要编译这些代码,在你的Renderer类中创建一个工具类方法: + +```java +public static int loadShader(int type, String shaderCode){ + + // create a vertex shader type (GLES20.GL_VERTEX_SHADER) + // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) + int shader = GLES20.glCreateShader(type); + + // add the source code to the shader and compile it + GLES20.glShaderSource(shader, shaderCode); + GLES20.glCompileShader(shader); + + return shader; +} +``` + +为了绘制你的形状,你必须编译shader代码,添加它们到一个OpenGLES program 对象然后链接这个program。在renderer对象的构造器中做这些事情,从而只需做一次即可。 + +注:编译OpenGLES shader们和链接linkingprogram们是很耗CPU的,所以你应该避免多次做这些事。如果在运行时你不知道shader的内容,你应该只创建一次code然后缓存它们以避免多次创建。 + +```java +public class Triangle() { + ... + + private final int mProgram; + + public Triangle() { + ... + + int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER, + vertexShaderCode); + int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER, + fragmentShaderCode); + + // create empty OpenGL ES Program + mProgram = GLES20.glCreateProgram(); + + // add the vertex shader to program + GLES20.glAttachShader(mProgram, vertexShader); + + // add the fragment shader to program + GLES20.glAttachShader(mProgram, fragmentShader); + + // creates OpenGL ES program executables + GLES20.glLinkProgram(mProgram); + } +} +``` + +此时,你已经准备好增加真正的绘制调用了。需要为渲染管线指定很多参数来告诉它你想画什么以及如何画。因为绘制操作因形状而异,让你的形状类包含自己的绘制逻辑是个很好主意。 + +创建一个draw()方法负责绘制形状。下面的代码设置位置和颜色值到形状的vertexshader和fragmentshader,然后执行绘制功能: + +```java +private int mPositionHandle; +private int mColorHandle; + +private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX; +private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex + +public void draw() { + // Add program to OpenGL ES environment + GLES20.glUseProgram(mProgram); + + // get handle to vertex shader's vPosition member + mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); + + // Enable a handle to the triangle vertices + GLES20.glEnableVertexAttribArray(mPositionHandle); + + // Prepare the triangle coordinate data + GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, + vertexStride, vertexBuffer); + + // get handle to fragment shader's vColor member + mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor"); + + // Set color for drawing the triangle + GLES20.glUniform4fv(mColorHandle, 1, color, 0); + + // Draw the triangle + GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount); + + // Disable vertex array + GLES20.glDisableVertexAttribArray(mPositionHandle); +} +``` + +一旦完成了所有这些代码,绘制该对象只需要在渲染器的onDrawFrame()方法中调用draw()方法: + +```java +public void onDrawFrame(GL10 unused) { + ... + + mTriangle.draw(); +} +``` + +当你运行程序的时候,你就应该看到以下的内容: +When you run the application, it should look something like this: +![img](https://images2017.cnblogs.com/blog/682616/201712/682616-20171208133537421-1902369457.png) + +此例子中的代码还有很多问题。首先,它不会打动你和你的朋友。其次,三角形会在你从竖屏变为横屏时被压扁。三角形变形的原因是其顶点们没有跟据屏幕的宽高比进行修正。而且这里展示出来的三角形是静止的,这样的图形是有点无聊的,在“添加动画”的文章中,我们会使用OpenGL ES 的视图管线来旋转此形状。 + +### Android OpenGL ES开发(五) : OpenGL ES使用投影和相机视图 + +OpenGL ES环境允许你以更接近于你眼睛看到的物理对象的方式来显示你绘制的对象。物理查看的模拟是通过对你所绘制的对象的坐标进行数学变换完成的: + +> - Projection — 这个变换是基于他们所显示的GLSurfaceView的宽和高来调整绘制对象的坐标的。没有这个计算变换,通过OpenGL绘制的形状会在不同显示窗口变形。这个投影变化通常只会在OpenGL view的比例被确定或者在你渲染器的onSurfaceChanged()方法中被计算。想要了解更多的关于投影和坐标映射的相关信息,请看绘制对象的坐标映射。 + +> - Camera View — 这个换是基于虚拟的相机的位置来调整绘制对象坐标的。需要着重注意的是,OpenGL ES并没有定义一个真实的相机对象,而是提供一个实用方法,通过变换绘制对象的显示来模拟一个相机。相机视图变换可能只会在你的GLSurfaceView被确定时被计算,或者基于用户操作或你应用程序的功能来动态改变。 + +本课程描述怎样创建投影和相机视图并将其应用的到你的GLSurfaceView的绘制对象上。 + +#### 定义投影 + +投影变化的数据是在你GLSurfaceView.Renderer类的onSurfaceChanged()方法中被计算的。下面的示例代码是获取GLSurfaceView的高和宽,并通过Matrix.frustumM()方法用它们填充到投影变换矩阵中。 + +```java +// mMVPMatrix is an abbreviation for "Model View Projection Matrix" +private final float[] mMVPMatrix = new float[16]; +private final float[] mProjectionMatrix = new float[16]; +private final float[] mViewMatrix = new float[16]; + +@Override +public void onSurfaceChanged(GL10 unused, int width, int height) { + GLES20.glViewport(0, 0, width, height); + + float ratio = (float) width / height; + + // this projection matrix is applied to object coordinates + // in the onDrawFrame() method + Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7); +} +``` + +上面的代码填充有一个投影矩阵mProjectionMatrix,mProjectionMatrix可以在onFrameDraw()方法中与下一部分的相机视图结合在一起。 + +注意:如果仅仅只把投影矩阵应用的到你绘制的对象中,通常你只会得到一个非常空的显示。一般情况下,你还必须为你要在屏幕上显示的任何内容应用相机视图。 + +#### 定义相机视图 + +通过在你的渲染器中添加相机视图变换作为你绘制过程的一部分来完成你的绘制图像的变换过程。在下面的代码中,通过Matrix.setLookAtM()方法计算相机视图变换,然后将其与之前计算出的投影矩阵结合到一起。合并后的矩阵接下来会传递给绘制的图形。 + +```java +@Override +public void onDrawFrame(GL10 unused) { + ... + // Set the camera position (View matrix) + Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f); + + // Calculate the projection and view transformation + Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0); + + // Draw shape + mTriangle.draw(mMVPMatrix); +} +``` + +#### 应用投影和相机变换 + +为了使用在上一部分内容中展示的投影和相机视图变换的合并矩阵,首先要在之前Triangle类中定义的定点着色器代码中添加一个矩阵变量: + +```java +public class Triangle { + + private final String vertexShaderCode = + // This matrix member variable provides a hook to manipulate + // the coordinates of the objects that use this vertex shader + "uniform mat4 uMVPMatrix;" + + "attribute vec4 vPosition;" + + "void main() {" + + // the matrix must be included as a modifier of gl_Position + // Note that the uMVPMatrix factor *must be first* in order + // for the matrix multiplication product to be correct. + " gl_Position = uMVPMatrix * vPosition;" + + "}"; + + // Use to access and set the view transformation + private int mMVPMatrixHandle; + + ... +} +``` + +下一步,修改你的图形对象的draw()方法来接收联合变换矩阵,并将它们应用到图形中: + +```java +public void draw(float[] mvpMatrix) { // pass in the calculated transformation matrix + ... + + // get handle to shape's transformation matrix + mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); + + // Pass the projection and view transformation to the shader + GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0); + + // Draw the triangle + GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount); + + // Disable vertex array + GLES20.glDisableVertexAttribArray(mPositionHandle); +} +``` + +一旦你正确的计算并应用投影和相机视图变换,你的绘图对象将会以正确的比例绘制,它看起来应该像下面这样: + +![img](https://images2017.cnblogs.com/blog/682616/201712/682616-20171208151548156-932896321.png) + +现在你已经有一个可以以正确比例显示图形的应用了。后面的章节,我们可以了解如何为你的图形添加运动了。 + +### Android openGL ES开发(六): OpenGL ES添加运动效果 + +在屏幕上绘制图形只是OpenGL的相当基础的特点,你也可以用其他的Android图形框架类来实现这些,包括Canvas和Drawable对象。OpenGL ES为在三维空间中移动和变换提供了额外的功能,并提供了创建引人注目的用户体验的独特方式。 +在本文中,你将进一步使用OpenGL ES学习怎样为你的图形添加一个旋转动作。 + +#### 旋转一个图形 + +用OpenGL ES 2.0来旋转一个绘制对象是相对简单的。在你的渲染器中,添加一个新的变换矩阵(旋转矩阵),然后把它与你的投影与相机视图变换矩阵合并到一起: + +```java +private float[] mRotationMatrix = new float[16]; +public void onDrawFrame(GL10 gl) { + float[] scratch = new float[16]; + + ... + + // Create a rotation transformation for the triangle + long time = SystemClock.uptimeMillis() % 4000L; + float angle = 0.090f * ((int) time); + Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f); + + // Combine the rotation matrix with the projection and camera view + // Note that the mMVPMatrix factor *must be first* in order + // for the matrix multiplication product to be correct. + Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0); + + // Draw triangle + mTriangle.draw(scratch); +} +``` + +如果做了这些改变后你的三角形还没有旋转,请确保你是否注释掉了GLSurfaceView.RENDERMODE_WHEN_DIRTY设置项,这将在下一部分讲到。 + +#### 允许连续渲染 + +如果你勤恳地遵循本系列课程的示例代码到这个点,请确保你注释了设置只有当dirty的时候才渲染的渲染模式这一行,否则OpenGL旋转图形,只会递增角度然后等待来自GLSurfaceView容器的对requestRender()方法的调用: + +```java +public MyGLSurfaceView(Context context) { + ... + // Render the view only when there is a change in the drawing data. + // To allow the triangle to rotate automatically, this line is commented out: + //setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); +} +``` + +除非你的对象改变没有用户交互,否则通常打开这个标志是个好主意。准备好取消注释这行代码,因为下一节内容将使这个调用再次适用。 + +### Android openGL ES开发(七) : OpenGL ES 响应触摸事件 + +像旋转三角形一样,通过预设程序来让对象移动对于吸引注意是很有用的,但是如果你想让你的OpenGL图形有用户交互呢?让你的OpenGL ES应用有触摸交互的关键是,扩展你的GLSurfaceView的实现重载onTouchEvent()方法来监听触摸事件。 +本节内容将向你展示如何监听触摸事件来让用户旋转一个图形。 + +#### 设置触摸事件 + +为了你的OpenGL ES应用能够响应触摸事件,你必须在你的GLSurfaceView中实现onTouchEvent()方法,下面的实现例子展示了怎样监听MotionEvent.ACTION_MOVE事件,并将该事件转换成图形的旋转角度。 + +```java +private final float TOUCH_SCALE_FACTOR = 180.0f / 320; +private float mPreviousX; +private float mPreviousY; + +@Override +public boolean onTouchEvent(MotionEvent e) { + // MotionEvent reports input details from the touch screen + // and other input controls. In this case, you are only + // interested in events where the touch position changed. + + float x = e.getX(); + float y = e.getY(); + + switch (e.getAction()) { + case MotionEvent.ACTION_MOVE: + + float dx = x - mPreviousX; + float dy = y - mPreviousY; + + // reverse direction of rotation above the mid-line + if (y > getHeight() / 2) { + dx = dx * -1 ; + } + + // reverse direction of rotation to left of the mid-line + if (x < getWidth() / 2) { + dy = dy * -1 ; + } + + mRenderer.setAngle( + mRenderer.getAngle() + + ((dx + dy) * TOUCH_SCALE_FACTOR)); + requestRender(); + } + + mPreviousX = x; + mPreviousY = y; + return true; +} +``` + +需要注意的是,计算完旋转角度后,需要调用requestRender()方法来告诉渲染器是时候渲染帧画面了。在本例子中这种方法是最高效的,因为除非旋转有改变,否则帧画面不需要重绘。然而除非你还用setRenderMode()方法要求渲染器只有在数据改变时才进行重绘,否则这对性能没有任何影响。因此,确保渲染器中的下面这行是取消注释的: + +```java +public MyGLSurfaceView(Context context) { + ... + // Render the view only when there is a change in the drawing data + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); +} +``` + +#### 暴露旋转角度 + +上面的例程代码中需要你通过在渲染器中添加共有的成员来暴露旋转角度。当渲染代码是在独立于你应用程序的主用户界面线程的单独线程执行的时候,你必须声明这个共有变量是volatile类型的。下面的代码声明了这个变量并且暴露了它的getter和setter方法对: + +```java +public class MyGLRenderer implements GLSurfaceView.Renderer { + ... + + public volatile float mAngle; + + public float getAngle() { + return mAngle; + } + + public void setAngle(float angle) { + mAngle = angle; + } +} +``` + +#### 应用旋转 + +为了应用触摸输入产生的旋转,先注释掉产生角度的代码,并添加一个右触摸事件产生的角度mAngle: + +```java +public void onDrawFrame(GL10 gl) { + ... + float[] scratch = new float[16]; + + // Create a rotation for the triangle + // long time = SystemClock.uptimeMillis() % 4000L; + // float angle = 0.090f * ((int) time); + Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f); + + // Combine the rotation matrix with the projection and camera view + // Note that the mMVPMatrix factor *must be first* in order + // for the matrix multiplication product to be correct. + Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0); + + // Draw triangle + mTriangle.draw(scratch); +} +``` + +当你完成上面介绍的步骤,运行你的程序,然后在屏幕上拖拽你的手指来旋转这个三角形。 +![img](https://images2017.cnblogs.com/blog/682616/201712/682616-20171208151758015-1271629213.png) + +### Android OpenGL ES开发(八) :OpenGL ES 着色器语言GLSL + +前面的文章主要是整理的Android 官方文档对OpenGL ES支持的介绍。通过之前的文章,我们基本上可以完成的基本的形状的绘制。 + +这是本人做的整理笔记: https://github.com/renhui/OpenGLES20Study + +目前到这里第一阶段的学习,也就是基本的图形绘制,基本的交互的实现。 + +> - 平面绘制:三角形、正方形、在相机视角下的三角形、彩色三角形 +> - 立体绘制:正方体、圆柱体、圆锥体、球体 +> - 基本交互:手绘点、旋转三角形 + +知道了基本的图形绘制,也知道了基本的交互的实现,现在可能大多数人还是对整个实现的流程有点懵,最主要的地方可能就是对顶点着色器和片元着色器了。前面的使用过程中,我们大概也对着色器语言有一定的了解了,但是在前面我们使用的着色器代码还是很简单的,做的事情也是很有限的,后面的开发过程中,我们用到的着色器会越来越复杂,So,这里我们想一下着色器语言GLSL。 + +我们知道,在OpenGL ES中着色器分为顶点着色器和片元着色器。顶点着色器是针对每个顶点执行一次,用于确定顶点的位置。片元着色器是针对每个片元,片元我们可以理解为每个像素,用于确定每个片元(像素)的颜色。 + +#### GLSL 简介 + +GLSL又叫OpenGL着色语言(OpenGL Shading Language),是用来在OpenGL中着色编程的语言,是一种面向过程的语言,基本的语法和C/C++基本相同,他们是在图形卡的GPU (Graphic Processor Unit图形处理单元)上执行的,代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程性。比如:视图转换、投影转换等。GLSL(GL Shading Language)的着色器代码分成2个部分:Vertex Shader(顶点着色器)和Fragment(片断着色器)。 + +在前面的学习中,我们基本上使用的都是非常简单的着色器,基本上没有使用过GLSL的内置函数,但是在后面我们完成其他的功能的时候应该就会用到这些内置函数了。 + +#### GLSL 基础 + +GLSL 虽然很类似于C/C++,但是它和C/C++还是有很大的不同的,比如,没有double,long等类型,没有union、enum、unsigned以及位运算等特性。 + +##### 基本数据类型 + +GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构体、数组、空类型七种类型: + +###### 标量: + +标量表示的是只有大小没有方向的量,在GLSL中标量只有bool、int和float三种。对于int,和C一样,可以写为十进制(16)、八进制(020)或者十六进制(0x10)。对于标量的运算,我们最需要注意的是精度,防止溢出问题。 + +###### 向量: + +向量我们可以看做是数组,在GLSL通常用于储存颜色、坐标等数据,针对维数,可分为二维、三维和四位向量。针对存储的标量类型,可以分为bool、int和float。共有vec2、vec3、vec4,ivec2、ivec3、ivec4、bvec2、bvec3和bvec4九种类型,数组代表维数、i表示int类型、b表示bool类型。需要注意的是,GLSL中的向量表示竖向量,所以与矩阵相乘进行变换时,矩阵在前,向量在后(与DirectX正好相反)。向量在GPU中由硬件支持运算,比CPU快的多。 + +> - 作为颜色向量时,用rgba表示分量,就如同取数组的中具体数据的索引值。三维颜色向量就用rgb表示分量。比如对于颜色向量vec4 color,color[0]和color.r都表示color向量的第一个值,也就是红色的分量。其他相同。 +> - 作为位置向量时,用xyzw表示分量,xyz分别表示xyz坐标,w表示向量的模。三维坐标向量为xyz表示分量,二维向量为xy表示分量。 +> - 作为纹理向量时,用stpq表示分量,三维用stp表示分量,二维用st表示分量。 + +###### 矩阵: + +在GLSL中矩阵拥有2*2、3*3、4*4三种类型的矩阵,分别用mat2、mat3、mat4表示。我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值。 + +###### 采样器: + +采样器是专门用来对纹理进行采样工作的,在GLSL中一般来说,一个采样器变量表示一副或者一套纹理贴图。所谓的纹理贴图可以理解为我们看到的物体上的皮肤。 + +###### 结构体: + +和C语言中的结构体相同,用struct来定义结构体,关于结构体参考C语言中的结构体。 + +###### 数组: + +数组知识也和C中相同,不同的是数组声明时可以不指定大小,但是建议在不必要的情况下,还是指定大小的好。 + +###### 空类型: + +空类型用void表示,仅用来声明不返回任何值得函数。 + +数据声明示例: + +```xml +float a=1.0; +int b=1; +bool c=true; +vec2 d=vec2(1.0,2.0); +vec3 e=vec3(1.0,2.0,3.0) +vec4 f=vec4(vec3,1.2); +vec4 g=vec4(0.2); //相当于vec(0.2,0.2,0.2,0.2) +vec4 h=vec4(a,a,1.3,a); +mat2 i=mat2(0.1,0.5,1.2,2.4); +mat2 j=mat2(0.8); //相当于mat2(0.8,0.8,0.8,0.8) +mat3 k=mat3(e,e,1.2,1.6,1.8); +``` + +##### 运算符 + +GLSL中的运算符有(越靠前,运算优先级越高): + +1. 索引:[] +2. 前缀自加和自减:++,– +3. 一元非和逻辑非:~,! +4. 加法和减法:+,- +5. 等于和不等于:==,!= +6. 逻辑异或:^^ +7. 三元运算符号,选择:?: +8. 成员选择与混合:. +9. 后缀自加和自减:++,– +10. 乘法和除法:*,/ +11. 关系运算符:>,<,=,>=,<=,<> +12. 逻辑与:&& +13. 逻辑或:|| +14. 赋值预算:=,+=,-=,*=,/= + +##### 类型转换 + +GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如float a=1;就是一种错误的写法,必须严格的写成float a=1.0,也不可以强制转换,即float a=(float)1;也是错误的写法,但是可以用内置函数来进行转换,如float a=float(1);还有float a=float(true);(true为1.0,false为0.0)等,值得注意的是,低精度的int不能转换为低精度的float。 + +##### 限定符 + +在之前的博客中也提到了,GLSL中的限定符号主要有: + +> - attritude:一般用于各个顶点各不相同的量。如顶点颜色、坐标等。 +> - uniform:一般用于对于3D物体中所有顶点都相同的量。比如光源位置,统一变换矩阵等。 +> - varying:表示易变量,一般用于顶点着色器传递到片元着色器的量。 +> - const:常量。 +> 限定符与java限定符类似,放在变量类型之前,并且只能用于全局变量。在GLSL中,没有默认限定符一说。 + +##### 流程控制 + +GLSL中的流程控制与C中基本相同,主要有: + +> - if(){}、if(){}else{}、if(){}else if(){}else{} +> - while(){}和do{}while() +> - for(;😉{} +> - break和continue + +##### 函数 + +GLSL中也可以定义函数,定义函数的方式也与C语言基本相同。函数的返回值可以是GLSL中的除了采样器的任意类型。对于GLSL中函数的参数,可以用参数用途修饰符来进行修饰,常用修饰符如下: + +> - in:输入参数,无修饰符时默认为此修饰符。 +> - out:输出参数。 +> - inout:既可以作为输入参数,又可以作为输出参数。 + +##### 浮点精度 + +与顶点着色器不同的是,在片元着色器中使用浮点型时,必须指定浮点类型的精度,否则编译会报错。精度有三种,分别为: + +> - lowp:低精度。8位。 +> - mediump:中精度。10位。 +> - highp:高精度。16位。 + +不仅仅是float可以制定精度,其他(除了bool相关)类型也同样可以,但是int、采样器类型并不一定要求指定精度。加精度的定义如下: + +``` +uniform lowp float a=1.0; +varying mediump vec4 c; +``` + +当然,也可以在片元着色器中设置默认精度,只需要在片元着色器最上面加上precision <精度> <类型>即可制定某种类型的默认精度。其他情况相同的话,精度越高,画质越好,使用的资源也越多。 + +##### 程序结构 + +前面几篇博客都有使用到着色器,我们对着色器的程序结构也应该有一定的了解。也许一直沉浸在Android应用开发,没有了解C开发的朋友,对这种结构并不熟悉。GLSL程序的结构和C语言差不多,main()方法表示入口函数,可以在其上定义函数和变量,在main中可以引用这些变量和函数。定义在函数体以外的叫做全局变量,定义在函数体内的叫做局部变量。与高级语言不通的是,变量和函数在使用前必须声明,不能再使用的后面声明变量或者函数。 + +#### GLSL 内建变量 + +在着色器中我们一般都会声明变量来在程序中使用,但是着色器中还有一些特殊的变量,不声明也可以使用。这些变量叫做内建变量。內建变量,相当于着色器硬件的输入和输出点,使用者利用这些输入点输入之后,就会看到屏幕上的输出。通过输出点可以知道输出的某些数据内容。当然,实际上肯定不会这样简单,这么说只是为了帮助理解。在顶点着色器中的内建变量和片元着色器的内建变量是不相同的。着色器中的内建变量有很多,在此,我们只列出最常用的集中内建变量。 + +##### 顶点着色器的内建变量 + +输入变量: + +> - gl_Position:顶点坐标 +> - gl_PointSize:点的大小,没有赋值则为默认值1,通常设置绘图为点绘制才有意义。\ + +##### 片元着色器的内建变量 + +输入变量: + +> - gl_FragCoord:当前片元相对窗口位置所处的坐标。 +> - gl_FragFacing:bool型,表示是否为属于光栅化生成此片元的对应图元的正面。 +> 输出变量: +> - gl_FragColor:当前片元颜色 +> - gl_FragData:vec4类型的数组。向其写入的信息,供渲染管线的后继过程使用。 + +#### 常用内置函数 + +##### 常见函数 + +> - radians(x):角度转弧度 +> - degrees(x):弧度转角度 +> - sin(x):正弦函数,传入值为弧度。相同的还有cos余弦函数、tan正切函数、asin反正弦、acos反余弦、atan反正切 +> - pow(x,y):xy +> - exp(x):ex +> - exp2(x):2x +> - log(x):logex +> - log2(x):log2x +> - sqrt(x):x√ +> - inversesqr(x):1x√ +> - abs(x):取x的绝对值 +> - sign(x):x>0返回1.0,x<0返回-1.0,否则返回0.0 +> - ceil(x):返回大于或者等于x的整数 +> - floor(x):返回小于或者等于x的整数 +> - fract(x):返回x-floor(x)的值 +> - mod(x,y):取模(求余) +> - min(x,y):获取xy中小的那个 +> - max(x,y):获取xy中大的那个 +> - mix(x,y,a):返回x∗(1−a)+y∗a +> - step(x,a):x< a返回0.0,否则返回1.0 +> - smoothstep(x,y,a):a < x返回0.0,a>y返回1.0,否则返回0.0-1.0之间平滑的Hermite插值。 +> - dFdx(p):p在x方向上的偏导数 +> - dFdy(p):p在y方向上的偏导数 +> - fwidth(p):p在x和y方向上的偏导数的绝对值之和 + +##### 几何函数 + +> - length(x):计算向量x的长度 +> - distance(x,y):返回向量xy之间的距离 +> - dot(x,y):返回向量xy的点积 +> - cross(x,y):返回向量xy的差积 +> - normalize(x):返回与x向量方向相同,长度为1的向量 + +##### 矩阵函数 + +> - matrixCompMult(x,y):将矩阵相乘 +> - lessThan(x,y):返回向量xy的各个分量执行x< y的结果,类似的有greaterThan,equal,notEqual +> - lessThanEqual(x,y):返回向量xy的各个分量执行x<= y的结果,类似的有类似的有greaterThanEqual +> - any(bvec x):x有一个元素为true,则为true +> - all(bvec x):x所有元素为true,则返回true,否则返回false +> - not(bvec x):x所有分量执行逻辑非运算 + +##### 纹理采样函数 + +纹理采样函数有texture2D、texture2DProj、texture2DLod、texture2DProjLod、textureCube、textureCubeLod及texture3D、texture3DProj、texture3DLod、texture3DProjLod等。 + +> - texture表示纹理采样,2D表示对2D纹理采样,3D表示对3D纹理采样 +> - Lod后缀,只适用于顶点着色器采样 +> - Proj表示纹理坐标st会除以q + +纹理采样函数中,3D在OpenGLES2.0并不是绝对支持。我们再次暂时不管3D纹理采样函数。重点只对texture2D函数进行说明。texture2D拥有三个参数,第一个参数表示纹理采样器。第二个参数表示纹理坐标,可以是二维、三维、或者四维。第三个参数加入后只能在片元着色器中调用,且只对采样器为mipmap类型纹理时有效。 + +### Android OpenGL ES开发(九): OpenGL ES纹理贴图 + +#### 概念 + +一般说来,纹理是表示物体表面的一幅或几幅二维图形,也称纹理贴图(texture)。当把纹理按照特定的方式映射到物体表面上的时候,能使物体看上去更加真实。当前流行的图形系统中,纹理绘制已经成为一种必不可少的渲染方法。在理解纹理映射时,可以将纹理看做应用在物体表面的像素颜色。在真实世界中,纹理表示一个对象的颜色、图案以及触觉特征。纹理只表示对象表面的彩色图案,它不能改变对象的几何形式。更进一步的说,它只是一种高强度的计算行为。 +概括为一句就是:纹理贴图就是把一个纹理(对于2D贴图,可以简单的理解为图片),按照所期望的方式显示在诸多三角形组成的物体的表面。 + +#### 原理 + +首先介绍一下纹理映射时的坐标系,纹理映射的坐标系和顶点着色器的坐标系是不一样的。 +纹理坐标用浮点数来表示,范围一般从0.0到1.0,左上角坐标为(0.0,0.0),右上角坐标为(1.0,0.0),左下角坐标为(0.0,1.0),右下角坐标为(1.0,1.0),具体如下: +![img](https://images2017.cnblogs.com/blog/682616/201712/682616-20171229180426367-783239921.jpg) + +顶点着色器的坐标系如下: +![img](https://images2017.cnblogs.com/blog/682616/201712/682616-20171229175417835-1380156398.png) + +将纹理映射到右边的两个三角形上(也就是一个矩形),需要将纹理坐标指定到正确的顶点上,才能使纹理正确的显示,否则显示出来的纹理会无法显示,或者出现旋转、翻转、错位等情况。 +将右图顶点按照V2V1V4V3传入,以三角形条带方式绘制,则纹理坐标应按照V2V1V4V3传入。如果按照V3V4V1V2传入,会得到一个旋转了180度的纹理。如果按照V4V3V2V1传入,则会得到一个左右翻转的纹理。 + +#### 显示纹理图片 + +我们可以根据以下步骤利用OpenGL ES显示一张图片: + +##### 修改着色器 + +首先,我们需要修改我们的着色器,将顶点着色器修改为: + +```java +attribute vec4 vPosition; +attribute vec2 vCoordinate; +uniform mat4 vMatrix; + +varying vec2 aCoordinate; + +void main(){ + gl_Position=vMatrix*vPosition; + aCoordinate=vCoordinate; +} +``` + +可以看到,顶点着色器中增加了一个vec2变量,并将这个变量传递给了片元着色器,这个变量就是纹理坐标。接着我们修改片元着色器为: + +```java +precision mediump float; + +uniform sampler2D vTexture; +varying vec2 aCoordinate; + +void main(){ + gl_FragColor=texture2D(vTexture,aCoordinate); +} +``` + +片元着色器中,增加了一个sampler2D的变量,sampler2D我们在前一篇博客GLSL语言基础中提到过,是GLSL的变量类型之一的取样器。texture2D也有提到,它是GLSL的内置函数,用于2D纹理取样,根据纹理取样器和纹理坐标,可以得到当前纹理取样得到的像素颜色。 + +##### 设置顶点坐标和纹理坐标 + +根据纹理映射原理中的介绍,我们将顶点坐标设置为: + +```java +private final float[] sPos={ + -1.0f,1.0f, //左上角 + -1.0f,-1.0f, //左下角 + 1.0f,1.0f, //右上角 + 1.0f,-1.0f //右下角 + }; +``` + +相应的,对照顶点坐标,我们可以设置纹理坐标为: + +```java +private final float[] sCoord={ + 0.0f,0.0f, + 0.0f,1.0f, + 1.0f,0.0f, + 1.0f,1.0f, + }; +``` + +##### 计算变换矩阵 + +按照上步设置顶点坐标和纹理坐标,大多数情况下我们得到的一定是一张拉升或者压缩的图片。为了让图片完整的显示,且不被拉伸和压缩,我们需要向绘制等腰直角三角形一样,计算一个合适的变换矩阵,传入顶点着色器,代码如下: + +```java +@Override +public void onSurfaceChanged(GL10 gl, int width, int height) { + GLES20.glViewport(0,0,width,height); + + int w=mBitmap.getWidth(); + int h=mBitmap.getHeight(); + float sWH=w/(float)h; + float sWidthHeight=width/(float)height; + if(width>height){ + if(sWH>sWidthHeight){ + Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight*sWH,sWidthHeight*sWH, -1,1, 3, 7); + }else{ + Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight/sWH,sWidthHeight/sWH, -1,1, 3, 7); + } + }else{ + if(sWH>sWidthHeight){ + Matrix.orthoM(mProjectMatrix, 0, -1, 1, -1/sWidthHeight*sWH, 1/sWidthHeight*sWH,3, 7); + }else{ + Matrix.orthoM(mProjectMatrix, 0, -1, 1, -sWH/sWidthHeight, sWH/sWidthHeight,3, 7); + } + } + //设置相机位置 + Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 7.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f); + //计算变换矩阵 + Matrix.multiplyMM(mMVPMatrix,0,mProjectMatrix,0,mViewMatrix,0); +} +``` + +mMVPMatrix即为我们所需要的变换矩阵。 + +##### 显示图片 + +然后我们需要做的,就和之前绘制正方形一样容易了。和之前不同的是,在绘制之前,我们还需要将纹理和纹理坐标传入着色器: + +```java +@Override +public void onDrawFrame(GL10 gl) { + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT|GLES20.GL_DEPTH_BUFFER_BIT); + GLES20.glUseProgram(mProgram); + onDrawSet(); + GLES20.glUniformMatrix4fv(glHMatrix,1,false,mMVPMatrix,0); + GLES20.glEnableVertexAttribArray(glHPosition); + GLES20.glEnableVertexAttribArray(glHCoordinate); + GLES20.glUniform1i(glHTexture, 0); + textureId=createTexture(); + //传入顶点坐标 + GLES20.glVertexAttribPointer(glHPosition,2,GLES20.GL_FLOAT,false,0,bPos); + //传入纹理坐标 + GLES20.glVertexAttribPointer(glHCoordinate,2,GLES20.GL_FLOAT,false,0,bCoord); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4); +} + +public abstract void onDrawSet(); +public abstract void onDrawCreatedSet(int mProgram); + +private int createTexture(){ + int[] texture=new int[1]; + if(mBitmap!=null&&!mBitmap.isRecycled()){ + //生成纹理 + GLES20.glGenTextures(1,texture,0); + //生成纹理 + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture[0]); + //设置缩小过滤为使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色 + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST); + //设置放大过滤为使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色 + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR); + //设置环绕方向S,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合 + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE); + //设置环绕方向T,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合 + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE); + //根据以上指定的参数,生成一个2D纹理 + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0); + return texture[0]; + } + return 0; +} +``` + +这样我们就可以显示出我们需要显示的图片,并且保证它完整的居中显示而且不会变形了。 + +### Android OpenGL ES 开发(十):通过GLES20与着色器交互 + +#### 获取着色器程序内成员变量的id(句柄、指针) + +GLES20.glGetAttribLocation方法:获取着色器程序中,指定为attribute类型变量的id。 +GLES20.glGetUniformLocation方法:获取着色器程序中,指定为uniform类型变量的id。 + +如: + +```java +// 获取指向着色器中aPosition的index +maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition"); +// 获取指向着色器中uMVPMatrix的index +muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); +``` + +#### 向着色器传递数据 + +使用上一节获取的指向着色器相应数据成员的各个id,就能将我们自己定义的顶点数据、颜色数据等等各种数据传递到着色器当中了。 + +```java +// 使用shader程序 +GLES20.glUseProgram(mProgram); +// 将最终变换矩阵传入shader程序 +GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0); +// 设置缓冲区起始位置 +mRectBuffer.position(0); +// 顶点位置数据传入着色器 +GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, 20, mRectBuffer); +// 顶点颜色数据传入着色器中 +GLES20.glVertexAttribPointer(maColorHandle, 4, GLES20.GL_FLOAT, false, 4*4, mColorBuffer); +// 顶点坐标传递到顶点着色器 +GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 20, mRectBuffer); +// 允许使用顶点坐标数组 +GLES20.glEnableVertexAttribArray(maPositionHandle); +// 允许使用顶点颜色数组 +GLES20.glDisableVertexAttribArray(maColorHandle); +// 允许使用定点纹理数组 +GLES20.glEnableVertexAttribArray(maTextureHandle); +// 绑定纹理 +GLES20.glActiveTexture(GLES20.GL_TEXTURE0); +GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture); +// 图形绘制 +GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 4); +``` + +##### 定义顶点属性数组 + +```java +void glVertexAttribPointer (int index, int size, int type, boolean normalized, int stride, Buffer ptr ) +``` + +参数含义: +index 指定要修改的顶点着色器中顶点变量id; +size 指定每个顶点属性的组件数量。必须为1、2、3或者4。如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a)); +type 指定数组中每个组件的数据类型。可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT; +normalized 指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE); +stride 指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。如果normalized被设置为GL_TRUE,意味着整数型的值会被映射至区间[-1,1](https://www.cnblogs.com/renhui/p/有符号整数),或者区间[0,1](无符号整数),反之,这些值会被直接转换为浮点值而不进行归一化处理; +ptr 顶点的缓冲数据。 + +##### 启用或者禁用顶点属性数组 + +调用GLES20.glEnableVertexAttribArray和GLES20.glDisableVertexAttribArray传入参数index。 + +```java +GLES20.glEnableVertexAttribArray(glHPosition); +GLES20.glEnableVertexAttribArray(glHCoordinate); +``` + +如果启用,那么当GLES20.glDrawArrays或者GLES20.glDrawElements被调用时,顶点属性数组会被使用。 + +##### 选择活动纹理单元。 + +```java +void glActiveTexture (int texture) +``` + +texture指定哪一个纹理单元被置为活动状态。texture必须是GL_TEXTUREi之一,其中0 <= i < GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS,初始值为GL_TEXTURE0。 +GLES20.glActiveTexture()确定了后续的纹理状态改变影响哪个纹理,纹理单元的数量是依据该纹理单元所被支持的具体实现。 + + + +## OpenSL ES + +### Android OpenSL ES 开发:Android OpenSL 介绍和开发流程说明 + +#### Android OpenSL ES 介绍 + +OpenSL ES (Open Sound Library for Embedded Systems)是无授权费、跨平台、针对嵌入式系统精心优化的硬件音频加速API。它为嵌入式移动多媒体设备上的本地应用程序开发者提供标准化, 高性能,低响应时间的音频功能实现方法,并实现软/硬件音频性能的直接跨平台部署,降低执行难度,促进高级音频市场的发展。简单来说OpenSL ES是一个嵌入式跨平台免费的音频处理库。 + +Android的OpenSL ES库是在NDK的platforms文件夹对应android平台先相应cpu类型里面,如: + +![img](https://images2018.cnblogs.com/blog/682616/201809/682616-20180903171214077-956579084.png) + +#### Android OpenSL ES 开发流程 + +OpenSL ES 的开发流程主要有如下6个步骤: + +**1、 创建接口对象** + +**2、设置混音器** + +**3、创建播放器(录音器)** + +**4、设置缓冲队列和回调函数** + +**5、设置播放状态** + +**6、启动回调函数** + +注明:其中第4步和第6步是OpenSL ES 播放PCM等数据格式的音频是需要用到的。 + +在使用OpenSL ES的API之前,需要引入OpenSL ES的头文件,代码如下: + +``` +#include +#include +``` + +由于是在Native层使用该特性,所需需要在Android.mk中增加链接选项,以便在链接阶段使用到系统系统的OpenSL ES的so库: + +``` +LOCAL_LDLIBS += -lOepnSLES +``` + +我们知道OpenSL ES提供的是基于C语言的API,但是它是基于对象和接口的方式提供的,会采用面向对象的思想开发API。因此我们先来了解一下OpenSL ES中对象和接口的概念: + +- 对象:对象是对一组资源及其状态的抽象,每个对象都有一个在其创建时指定的类型,类型决定了对象可以执行的任务集,对象有点类似于C++中类的概念。 +- 接口:接口是对象提供的一组特征的抽象,这些抽象会为开发者提供一组方法以及每个接口的类型功能,在代码中,接口的类型由接口ID来标识。 + +需要重点理解的是,一个对象在代码中其实是没有实际的表示形式的,可以通过接口来改变对象的状态以及使用对象提供的功能。对象有可以有一个或者多个接口的实例,但是接口实例肯定只属于一个对象。 + +如果明白了OpenSL ES 中对象和接口的概念,那么下面我们就继续看看,在代码中是如何使用它们的。 + +上面我们也提到过,对象是没有实际的代码表示形式的,对象的创建也是通过接口来完成的。通过获取对象的方法来获取出对象,进而可以访问对象的其他的接口方法或者改变对象的状态,**下面是使用对象和接口的相关说明。** + +##### OpenSL ES 开发最重要的接口类 SLObjectItf + + 通过SLObjectItf接口类我们可以创建所需要的各种类型的类接口,比如: + +- 创建引擎接口对象:SLObjectItf engineObject +- 创建混音器接口对象:SLObjectItf outputMixObject +- 创建播放器接口对象:SLObjectItf playerObject + +以上等等都是通过SLObjectItf来创建的。 + +##### SLObjectItf 创建的具体的接口对象实例 + +OpenSL ES中也有具体的接口类,比如(引擎:SLEngineItf,播放器:SLPlayItf,声音控制器:SLVolumeItf等等)。 + +##### 创建引擎并实现 + +OpenSL ES中开始的第一步都是声明SLObjectItf接口类型的引擎接口对象engineObject,然后用方法slCreateEngine创建一个引擎接口对象;创建好引擎接口对象后,需要用SLObjectItf的Realize方法来实现engineObject;最后用SLObjectItf的GetInterface方法来初始化SLEngnineItf对象实例。如: + +``` +SLObjectItf engineObject = NULL;//用SLObjectItf声明引擎接口对象 +SLEngineItf engineEngine = NULL;//声明具体的引擎对象实例 + +void createEngine() +{ + SLresult result;//返回结果 + result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);//第一步创建引擎 + result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);//实现(Realize)engineObject接口对象 + result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);//通过engineObject的GetInterface方法初始化engineEngine +} +``` + +##### 利用引擎对象创建其他接口对象 + +其他接口对象(SLObjectItf outputMixObject,SLObjectItf playerObject)等都是用引擎接口对象创建的(具体的接口对象需要的参数这里就说了,可参照ndk例子里面的),如: + +``` +//混音器 +SLObjectItf outputMixObject = NULL;//用SLObjectItf创建混音器接口对象 +SLEnvironmentalReverbItf outputMixEnvironmentalReverb = NULL;////创建具体的混音器对象实例 + +result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, mids, mreq);//利用引擎接口对象创建混音器接口对象 +result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);//实现(Realize)混音器接口对象 +result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB, &outputMixEnvironmentalReverb);//利用混音器接口对象初始化具体混音器实例 + +//播放器 +SLObjectItf playerObject = NULL;//用SLObjectItf创建播放器接口对象 +SLPlayItf playerPlay = NULL;//创建具体的播放器对象实例 + +result = (*engineEngine)->CreateAudioPlayer(engineEngine, &playerObject, &audioSrc, &audioSnk, 3, ids, req);//利用引擎接口对象创建播放器接口对象 +result = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);//实现(Realize)播放器接口对象 +result = (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerPlay);//初始化具体的播放器对象实例 +``` + +**最后就是使用创建好的具体对象实例来实现具体的功能。** + +#### OpenSL ES 使用示例 + +首先导入OpenSL ES和其他必须的库: + +``` +-lOpenSLES -landroid +``` + +##### 播放assets文件 + +创建引擎——>创建混音器——>创建播放器——>设置播放状态 + +``` +JNIEXPORT void JNICALL +Java_com_renhui_openslaudio_MainActivity_playAudioByOpenSL_1assets(JNIEnv *env, jobject instance, jobject assetManager, jstring filename) { + + release(); + const char *utf8 = (*env)->GetStringUTFChars(env, filename, NULL); + + // use asset manager to open asset by filename + AAssetManager* mgr = AAssetManager_fromJava(env, assetManager); + AAsset* asset = AAssetManager_open(mgr, utf8, AASSET_MODE_UNKNOWN); + (*env)->ReleaseStringUTFChars(env, filename, utf8); + + // open asset as file descriptor + off_t start, length; + int fd = AAsset_openFileDescriptor(asset, &start, &length); + AAsset_close(asset); + + SLresult result; + + + //第一步,创建引擎 + createEngine(); + + //第二步,创建混音器 + const SLInterfaceID mids[1] = {SL_IID_ENVIRONMENTALREVERB}; + const SLboolean mreq[1] = {SL_BOOLEAN_FALSE}; + result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, mids, mreq); + (void)result; + result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE); + (void)result; + result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB, &outputMixEnvironmentalReverb); + if (SL_RESULT_SUCCESS == result) { + result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(outputMixEnvironmentalReverb, &reverbSettings); + (void)result; + } + //第三步,设置播放器参数和创建播放器 + // 1、 配置 audio source + SLDataLocator_AndroidFD loc_fd = {SL_DATALOCATOR_ANDROIDFD, fd, start, length}; + SLDataFormat_MIME format_mime = {SL_DATAFORMAT_MIME, NULL, SL_CONTAINERTYPE_UNSPECIFIED}; + SLDataSource audioSrc = {&loc_fd, &format_mime}; + + // 2、 配置 audio sink + SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject}; + SLDataSink audioSnk = {&loc_outmix, NULL}; + + // 创建播放器 + const SLInterfaceID ids[3] = {SL_IID_SEEK, SL_IID_MUTESOLO, SL_IID_VOLUME}; + const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; + result = (*engineEngine)->CreateAudioPlayer(engineEngine, &fdPlayerObject, &audioSrc, &audioSnk, 3, ids, req); + (void)result; + + // 实现播放器 + result = (*fdPlayerObject)->Realize(fdPlayerObject, SL_BOOLEAN_FALSE); + (void)result; + + // 得到播放器接口 + result = (*fdPlayerObject)->GetInterface(fdPlayerObject, SL_IID_PLAY, &fdPlayerPlay); + (void)result; + + // 得到声音控制接口 + result = (*fdPlayerObject)->GetInterface(fdPlayerObject, SL_IID_VOLUME, &fdPlayerVolume); + (void)result; + + //第四步,设置播放状态 + if (NULL != fdPlayerPlay) { + + result = (*fdPlayerPlay)->SetPlayState(fdPlayerPlay, SL_PLAYSTATE_PLAYING); + (void)result; + } + + //设置播放音量 (100 * -50:静音 ) + (*fdPlayerVolume)->SetVolumeLevel(fdPlayerVolume, 20 * -50); + +} +``` + +##### 播放pcm文件 + +(集成到ffmpeg时,也是播放ffmpeg转换成的pcm格式的数据),这里为了模拟是直接读取的pcm格式的音频文件。 + +###### 创建播放器和混音器 + +``` +//第一步,创建引擎 + createEngine(); + + //第二步,创建混音器 + const SLInterfaceID mids[1] = {SL_IID_ENVIRONMENTALREVERB}; + const SLboolean mreq[1] = {SL_BOOLEAN_FALSE}; + result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, mids, mreq); + (void)result; + result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE); + (void)result; + result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB, &outputMixEnvironmentalReverb); + if (SL_RESULT_SUCCESS == result) { + result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties( + outputMixEnvironmentalReverb, &reverbSettings); + (void)result; + } + SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject}; + SLDataSink audioSnk = {&outputMix, NULL}; +``` + +###### 设置pcm格式的频率位数等信息并创建播放器 + +``` +// 第三步,配置PCM格式信息 + SLDataLocator_AndroidSimpleBufferQueue android_queue={SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2}; + SLDataFormat_PCM pcm={ + SL_DATAFORMAT_PCM,//播放pcm格式的数据 + 2,//2个声道(立体声) + SL_SAMPLINGRATE_44_1,//44100hz的频率 + SL_PCMSAMPLEFORMAT_FIXED_16,//位数 16位 + SL_PCMSAMPLEFORMAT_FIXED_16,//和位数一致就行 + SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//立体声(前左前右) + SL_BYTEORDER_LITTLEENDIAN//结束标志 + }; + SLDataSource slDataSource = {&android_queue, &pcm}; + + + const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME}; + const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; + + result = (*engineEngine)->CreateAudioPlayer(engineEngine, &pcmPlayerObject, &slDataSource, &audioSnk, 3, ids, req); + //初始化播放器 + (*pcmPlayerObject)->Realize(pcmPlayerObject, SL_BOOLEAN_FALSE); + +// 得到接口后调用 获取Player接口 + (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_PLAY, &pcmPlayerPlay); +``` + +###### 设置缓冲队列和回调函数 + +``` +// 注册回调缓冲区 获取缓冲队列接口 + (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_BUFFERQUEUE, &pcmBufferQueue); + //缓冲接口回调 + (*pcmBufferQueue)->RegisterCallback(pcmBufferQueue, pcmBufferCallBack, NULL); +``` + +回调函数: + +``` +void * pcmBufferCallBack(SLAndroidBufferQueueItf bf, void * context) +{ + //assert(NULL == context); + getPcmData(&buffer); + // for streaming playback, replace this test by logic to find and fill the next buffer + if (NULL != buffer) { + SLresult result; + // enqueue another buffer + result = (*pcmBufferQueue)->Enqueue(pcmBufferQueue, buffer, 44100 * 2 * 2); + // the most likely other result is SL_RESULT_BUFFER_INSUFFICIENT, + // which for this code example would indicate a programming error + } +} +``` + +读取pcm格式的文件: + +``` +void getPcmData(void **pcm) +{ + while(!feof(pcmFile)) + { + fread(out_buffer, 44100 * 2 * 2, 1, pcmFile); + if(out_buffer == NULL) + { + LOGI("%s", "read end"); + break; + } else{ + LOGI("%s", "reading"); + } + *pcm = out_buffer; + break; + } +} +``` + +###### 设置播放状态并手动开始调用回调函数 + +``` +// 获取播放状态接口 + (*pcmPlayerPlay)->SetPlayState(pcmPlayerPlay, SL_PLAYSTATE_PLAYING); + +// 主动调用回调函数开始工作 + pcmBufferCallBack(pcmBufferQueue, NULL); +``` + +注意: + +在回调函数中result = (*pcmBufferQueue)->Enqueue(pcmBufferQueue, buffer, 44100 * 2 * 2),最后的“44100*2*2”是buffer的大小,因为我这里是指定了没读取一次就从pcm文件中读取了“44100*2*2”个字节,所以可以正常播放,如果是利用ffmpeg来获取pcm数据源,那么实际大小要根据每个AVframe的具体大小来定,这样才能正常播放出声音!(44100 * 2 * 2 表示:44100是频率HZ,2是立体声双通道,2是采用的16位采样即2个字节,所以总的字节数就是:44100 * 2 * 2) + +### Android OpenSL ES 开发:使用 OpenSL 播放 PCM 数据 + +OpenSL ES 是基于NDK也就是c语言的底层开发音频的公开API,通过使用它能够做到标准化, 高性能,低响应时间的音频功能实现方法。 + +这次是使用OpenSL ES来做一个音乐播放器,它能够播放m4a、mp3文件,并能够暂停和调整音量。 + +播放音乐需要做一些步骤: + +#### 创建声音引擎 + +首先创建声音引擎的对象接口 + +``` +result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL); +``` + +然后实现它 + +``` +result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); +``` + +从声音引擎的对象中抓取声音引擎 + +``` +result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine); +``` + +创建"输出混音器" + +``` +result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, ids, req); +``` + +实现输出混合音 + +``` +result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE); +``` + +#### 创建声音播放器 + +创建和实现播放器 + +``` +// realize the player +result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE); +assert(SL_RESULT_SUCCESS == result); +(void)result; + +// get the play interface +result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay); +assert(SL_RESULT_SUCCESS == result); +(void)result; +``` + +#### 设置播放缓冲 + +数据格式配置 + +``` +SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, 1, SL_SAMPLINGRATE_8, + SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, + SL_SPEAKER_FRONT_CENTER, SL_BYTEORDER_LITTLEENDIAN}; +``` + +数据定位器 就是定位要播放声音数据的存放位置,分为4种:内存位置,输入/输出设备位置,缓冲区队列位置,和midi缓冲区队列位置。 +数据定位器配置 + +``` +SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2}; +``` + +得到了缓存队列接口,并注册 + +``` +// get the buffer queue interface +result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, + &bqPlayerBufferQueue); +assert(SL_RESULT_SUCCESS == result); +(void)result; + +// register callback on the buffer queue +result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, NULL); +assert(SL_RESULT_SUCCESS == result); +(void)result; +``` + +#### 获得其他接口用来控制播放 + +得到声音特效接口 + +``` +// get the effect send interface +result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_EFFECTSEND, + &bqPlayerEffectSend); +assert(SL_RESULT_SUCCESS == result); +(void)result; +``` + +得到音量接口 + +``` +// get the volume interface +result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume); +assert(SL_RESULT_SUCCESS == result); +(void)result; + +// set the player's state to playing +result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING); +assert(SL_RESULT_SUCCESS == result); +(void)result; +``` + +#### 提供播放数据 + +打开音乐文件 + +``` +// convert Java string to UTF-8 +const char *utf8 = (*env)->GetStringUTFChars(env, filename, NULL); +assert(NULL != utf8); + +// use asset manager to open asset by filename +AAssetManager* mgr = AAssetManager_fromJava(env, assetManager); +assert(NULL != mgr); +AAsset* asset = AAssetManager_open(mgr, utf8, AASSET_MODE_UNKNOWN); + +// release the Java string and UTF-8 +(*env)->ReleaseStringUTFChars(env, filename, utf8); + +// the asset might not be found +if (NULL == asset) { + return JNI_FALSE; +} + +// open asset as file descriptor +off_t start, length; +int fd = AAsset_openFileDescriptor(asset, &start, &length); +assert(0 <= fd); +AAsset_close(asset); +``` + +设置播放数据 + +``` +SLDataLocator_AndroidFD loc_fd = {SL_DATALOCATOR_ANDROIDFD, fd, start, length}; +SLDataFormat_MIME format_mime = {SL_DATAFORMAT_MIME, NULL, SL_CONTAINERTYPE_UNSPECIFIED}; +SLDataSource audioSrc = {&loc_fd, &format_mime}; +``` + +#### 播放音乐 + +播放音乐只需要通过播放接口改变播放状态就可以了,暂停也是,停止也是,但是暂停必须之前的播放缓存做了才行,否则那暂停就相当于停止了 + +``` +result = (*fdPlayerPlay)->SetPlayState(fdPlayerPlay, isPlaying ? SL_PLAYSTATE_PLAYING : SL_PLAYSTATE_PAUSED); +``` + +#### 调解音量 + +``` +SLVolumeItf getVolume() +{ +if (fdPlayerVolume != NULL) +return fdPlayerVolume; +else +return bqPlayerVolume; +} + +void Java_com_renhui_openslaudio_MainActivity_setVolumeAudioPlayer(JNIEnv env, jclass clazz, +jint millibel) +{ +SLresult result; +SLVolumeItf volumeItf = getVolume(); +if (NULL != volumeItf) { +result = (volumeItf)->SetVolumeLevel(volumeItf, millibel); +assert(SL_RESULT_SUCCESS == result); +(void)result; +} +} +``` + +#### 释放资源 + +关闭app时释放占用资源 + +``` +void Java_com_renhui_openslaudio_MainActivity_shutdown(JNIEnv* env, jclass clazz) +{ + +// destroy buffer queue audio player object, and invalidate all associated interfaces +if (bqPlayerObject != NULL) { + (*bqPlayerObject)->Destroy(bqPlayerObject); + bqPlayerObject = NULL; + bqPlayerPlay = NULL; + bqPlayerBufferQueue = NULL; + bqPlayerEffectSend = NULL; + bqPlayerMuteSolo = NULL; + bqPlayerVolume = NULL; +} + +// destroy file descriptor audio player object, and invalidate all associated interfaces +if (fdPlayerObject != NULL) { + (*fdPlayerObject)->Destroy(fdPlayerObject); + fdPlayerObject = NULL; + fdPlayerPlay = NULL; + fdPlayerSeek = NULL; + fdPlayerMuteSolo = NULL; + fdPlayerVolume = NULL; +} + +// destroy output mix object, and invalidate all associated interfaces +if (outputMixObject != NULL) { + (*outputMixObject)->Destroy(outputMixObject); + outputMixObject = NULL; + outputMixEnvironmentalReverb = NULL; +} + +// destroy engine object, and invalidate all associated interfaces +if (engineObject != NULL) { + (*engineObject)->Destroy(engineObject); + engineObject = NULL; + engineEngine = NULL; +} +} +``` + +### 参考源码 + +https://github.com/renhui/OpenSL_Audio + +### Android OpenSL ES 开发:Android OpenSL 录制 PCM 音频数据 + +#### 实现说明 + +OpenSL ES的录音要比播放简单一些,在创建好引擎后,再创建好录音接口基本就可以录音了。在这里我们做的是流式录音,所以需要用至少2个buffer来缓存录制好的PCM数据,这里我们可以动态创建一个二维数组,里面有2个buffer,然后每次录音取出一个,录制好后再写入文件就可以了,2个buffer依次来存储PCM数据,这样就可以连续录制流式音频数据了,二维数组里面自己维护了一个索引,来标识当前处于哪个buffer录制状态,暴露给外部的只是调用方法而已,细节对外也是隐藏的。 + +#### 编码实现 + +##### 编写缓存buffer队列:RecordBuffer.h、RecordBuffer.cpp + +``` +#ifndef OPENSLRECORD_RECORDBUFFER_H +#define OPENSLRECORD_RECORDBUFFER_H + +class RecordBuffer { + +public: + short **buffer; + int index = -1; +public: + RecordBuffer(int buffersize); + ~RecordBuffer(); + /** + * 得到一个新的录制buffer + * @return + */ + short* getRecordBuffer(); + /** + * 得到当前录制buffer + * @return + */ + short* getNowBuffer(); +}; + +#endif //OPENSLRECORD_RECORDBUFFER_H +``` + +``` +#include "RecordBuffer.h" + +RecordBuffer::RecordBuffer(int buffersize) { + buffer = new short *[2]; + for(int i = 0; i < 2; i++) + { + buffer[i] = new short[buffersize]; + } +} + +RecordBuffer::~RecordBuffer() { +} + +short *RecordBuffer::getRecordBuffer() { + index++; + if(index > 1) + { + index = 0; + } + return buffer[index]; +} + +short *RecordBuffer::getNowBuffer() { + return buffer[index]; +} +``` + +这个队列其实就是PCM存储的buffer,getRecordBuffer()为即将要录入PCM数据的buffer,getNowBuffer()是当前录制好的PCM数据的buffer,可以写入文件,即我们得到的PCM数据。 + +##### 使用OpenSL ES录制PCM数据 + +过程分为:创建引擎->初始化IO设备(自动检测麦克风等音频输入设备)->设置缓存队列->设置录制PCM数据规格->设置录音器接口->设置队列接口并设置录音状态为录制->开始录音。 + +``` +const char *path = env->GetStringUTFChars(path_, 0); + /** + * PCM文件 + */ + pcmFile = fopen(path, "w"); + /** + * PCMbuffer队列 + */ + recordBuffer = new RecordBuffer(RECORDER_FRAMES * 2); + SLresult result; + /** + * 创建引擎对象 + */ + result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL); + result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); + result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine); + + /** + * 设置IO设备(麦克风) + */ + SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, + SL_DEFAULTDEVICEID_AUDIOINPUT, NULL}; + SLDataSource audioSrc = {&loc_dev, NULL}; + /** + * 设置buffer队列 + */ + SLDataLocator_AndroidSimpleBufferQueue loc_bq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2}; + /** + * 设置录制规格:PCM、2声道、44100HZ、16bit + */ + SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, 2, SL_SAMPLINGRATE_44_1, + SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, + SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, SL_BYTEORDER_LITTLEENDIAN}; + SLDataSink audioSnk = {&loc_bq, &format_pcm}; + + const SLInterfaceID id[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE}; + const SLboolean req[1] = {SL_BOOLEAN_TRUE}; + + /** + * 创建录制器 + */ + result = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject, &audioSrc, + &audioSnk, 1, id, req); + if (SL_RESULT_SUCCESS != result) { + return; + } + result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + return; + } + result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord); + result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, + &recorderBufferQueue); + finished = false; + result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(), + recorderSize); + result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, bqRecorderCallback, NULL); + LOGD("开始录音"); + /** + * 开始录音 + */ + (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING); + env->ReleaseStringUTFChars(path_, path); +``` + +录音回调如下: + +``` +void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context) +{ + // for streaming recording, here we would call Enqueue to give recorder the next buffer to fill + // but instead, this is a one-time buffer so we stop recording + LOGD("record size is %d", recorderSize); + + fwrite(recordBuffer->getNowBuffer(), 1, recorderSize, pcmFile); + + if(finished) + { + (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED); + fclose(pcmFile); + LOGD("停止录音"); + } else{ + (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(), + recorderSize); + } +} +``` + +这样就完成了OPenSL ES的PCM音频数据录制,我们这里拿到了录制的PCM数据可以用mediacodec或ffmpeg来编码成aac格式的音频,也可以直接用推流到服务器来实现音频直播。 + +完整代码如下: + +``` +#include +#include +#include "AndroidLog.h" +#include "RecordBuffer.h" +#include "unistd.h" + +extern "C" +{ +#include +#include +} + +//引擎接口 +static SLObjectItf engineObject = NULL; +//引擎对象 +static SLEngineItf engineEngine; + +//录音器接口 +static SLObjectItf recorderObject = NULL; +//录音器对象 +static SLRecordItf recorderRecord; +//缓冲队列 +static SLAndroidSimpleBufferQueueItf recorderBufferQueue; + +//录制大小设为4096 +#define RECORDER_FRAMES (2048) +static unsigned recorderSize = RECORDER_FRAMES * 2; + +//PCM文件 +FILE *pcmFile; +//录音buffer +RecordBuffer *recordBuffer; + +bool finished = false; + +void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context) +{ + // for streaming recording, here we would call Enqueue to give recorder the next buffer to fill + // but instead, this is a one-time buffer so we stop recording + LOGD("record size is %d", recorderSize); + + fwrite(recordBuffer->getNowBuffer(), 1, recorderSize, pcmFile); + + if(finished) + { + (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED); + fclose(pcmFile); + LOGD("停止录音"); + } else{ + (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(), + recorderSize); + } +} + +extern "C" +JNIEXPORT void JNICALL +Java_com_renhui_openslrecord_MainActivity_rdSound(JNIEnv *env, jobject instance, jstring path_) { + const char *path = env->GetStringUTFChars(path_, 0); + /** + * PCM文件 + */ + pcmFile = fopen(path, "w"); + /** + * PCMbuffer队列 + */ + recordBuffer = new RecordBuffer(RECORDER_FRAMES * 2); + SLresult result; + /** + * 创建引擎对象 + */ + result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL); + result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); + result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine); + + /** + * 设置IO设备(麦克风) + */ + SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, + SL_DEFAULTDEVICEID_AUDIOINPUT, NULL}; + SLDataSource audioSrc = {&loc_dev, NULL}; + /** + * 设置buffer队列 + */ + SLDataLocator_AndroidSimpleBufferQueue loc_bq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2}; + /** + * 设置录制规格:PCM、2声道、44100HZ、16bit + */ + SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, 2, SL_SAMPLINGRATE_44_1, + SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, + SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, SL_BYTEORDER_LITTLEENDIAN}; + SLDataSink audioSnk = {&loc_bq, &format_pcm}; + + const SLInterfaceID id[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE}; + const SLboolean req[1] = {SL_BOOLEAN_TRUE}; + + /** + * 创建录制器 + */ + result = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject, &audioSrc, + &audioSnk, 1, id, req); + if (SL_RESULT_SUCCESS != result) { + return; + } + result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + return; + } + result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord); + result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, + &recorderBufferQueue); + finished = false; + result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(), + recorderSize); + result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, bqRecorderCallback, NULL); + LOGD("开始录音"); + /** + * 开始录音 + */ + (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING); + env->ReleaseStringUTFChars(path_, path); +}extern "C" +JNIEXPORT void JNICALL +Java_com_renhui_openslrecord_MainActivity_rdStop(JNIEnv *env, jobject instance) { + + // TODO + if(recorderRecord != NULL) + { + finished = true; + } +} +``` + +#### 验证录制成果 + +有两种方法: + +1. 使用[Android OpenSL ES 开发:使用 OpenSL 播放 PCM 数据](https://www.cnblogs.com/renhui/p/9565464.html)的demo进行播放。 + +2. 使用 ffplay 命令播放,命令为:ffplay -f s16le -ar 44100 -ac 2 temp.pcm (命令由来:在录制代码里的参数为录制规格:PCM、2声道、44100HZ、16bit) + +#### 参考源码 + +https://github.com/renhui/OpenSLRecord + +### Android OpenSL ES 开发:OpenSL ES利用SoundTouch实现PCM音频的变速和变调 + +#### 缘由 + +OpenSL ES 学习到现在已经知道 OpenSL ES 不仅能播放和录制PCM音频数据,还能改变声音大小、设置左声道或右声道播放、还能变速播放,可谓是播放音频的王者。但是变速有一点不好的就是,虽然播放音频的速度变了,但是相应的音调也随之变了,这样的用户体验就不那么好了。所以就想到了用开源的SoundTouch来实现PCM音频变速和变调,OpenSL ES只是单纯的播放PCM数据就可以了。 + +#### 实现 + +##### 移植SoundTouch(Android) + +下载[SoundTouch](http://www.surina.net/soundtouch/sourcecode.html)源码,当前最新是:v2.0.1 + +在项目jni文件夹中创建include和SoundTouch文件夹,并把下载好的SoundTouch里面的include和SoundTouch的源码拷贝进去就可以了,目录结构如下: + +![img](https://images2018.cnblogs.com/blog/682616/201809/682616-20180910162406056-410041990.png) + +##### 用SoundTouch转码PCM源文件 + + 因为SoundTouch默认是float(32bit)格式的数据,这里需要先改成short(16bit)的格式。打开STTypes.h文件,修改如下代码: + +![img](https://images2018.cnblogs.com/blog/682616/201809/682616-20180910170320053-2104394898.png) + +再注释掉下面这句,不然编译不通过(for x86模拟器): + +![img](https://images2018.cnblogs.com/blog/682616/201809/682616-20180910170347782-384105003.png) + +这样SoundTouch里面处理PCM数据就是用的16bit的数据了。 + +##### SoundTouch使用流程 + +###### 添加命名空间,并创建SoundTouch指针变量 + +``` +using namespace soundtouch; +SoundTouch *soundTouch; +``` + +###### 设置SoundTouch参数 + +``` + soundTouch = new SoundTouch(); + soundTouch->setSampleRate(44100);//设置采样率,此处为44100,根据实际情况可变 + soundTouch->setChannels(2);//声道,此处为立体声 + soundTouch->setPitch(1);//变调不变速,如0.5、1.0、1.5等 + soundTouch->setTempo(1);//变速不变调,如0.5、1.0、2.0等 +``` + +###### 向SoundTouch中传入获取到的PCM数据,使用:putSamples函数 + +``` +size = fread(pcm_buffer, 1, 4096 * 2, pcmFile); +soundTouch->putSamples((const SAMPLETYPE *) pcm_buffer, size / 4); +``` + +这里,pcm_buffer是u_int16_t *类型的,也就是说和SoundTouch处理的PCM数据位数是一致的(16bit),所以可以直接传入SoundTouch中。putSamples的第一个参数就是PCM数据指针,第二个参数是采样点的个数,由于是2声道16bit(2byte),所以PCM数据的采样点个数为:num = 大小(size)/ (2 * 2)。 + +###### 获取SoundTouch输出的PCM数据:使用receiveSamples函数 + +``` +num = soundTouch->receiveSamples(sd_buffer, size / 4); +``` + +这里,receiveSamples的第一个参数是SoundTouch(变速或变调)处理后的PCM数据存放的内存地址,第二个参数是可能的最大采样个数,可以和putSamples保持一致,其中sd_buffer是SAMPLETYPE * 类型的,记得要提前分配好内存大小,最后返回值就是SoundTouch处理后的PCM里面所包含的采样个数,由于可能有缓存,所以应循环读取receiveSamples,直到返回值为0为止。 + +###### OpenSL ES播放SoundTouch处理后的PCM音频数据 + +``` +(*pcmBufferQueue)->Enqueue(pcmBufferQueue, sd_buffer, size * 4); +``` + +由于size是采样个数,所以sd_buffer的大小是:size * 2(声道) * 2(16bit==2字节)。 + +这样,我们听到的声音就是通过SoundTouch转码过后的了,如:变速不变调,变调不变速,变速又变调都可以自己设置。 + +#### 思维发散 + +##### FFmpeg解码得到的PCM数据(uint_8 *)利用SoundTouch转码 + +这里要处理的就是把uint_8 *(8bit)的数据转换成short(16bit)的数据格式。这里其实就是做bit的位运算,原理如下如: + +![img](https://images2018.cnblogs.com/blog/682616/201809/682616-20180910172752184-541558858.png) + +转换代码如下: + +``` +for (int i = 0; i < size / 2 + 1; i++) + { + sd_buffer[i] = (pcm_buffer[i * 2] | (pcm_buffer[i * 2 + 1] << 8)); + } + soundTouch->putSamples((const SAMPLETYPE *) pcm_buffer, size / 4); +``` + +后续操作和16bit的一样不变。 + +#### 总结 + +虽然是简单的移植SoundTouch到Android来播放PCM数据,但是还是让我们了解到了数据在内存中怎么排列的,然后可以怎么操作最小单位的bit来达到我们的要求。 + +#### 参考源码 + +[SoundTouch_OpenSL_Android ](https://github.com/wanliyang1990/SoundTouch_OpenSL_Android ) + +# Android音视频开发高级探究篇 + +## 音视频编解码技术 + +### 音视频编解码技术(一):MPEG-4/H.264 AVC 编解码标准 + +#### H264 概述 + +H.264,通常也被称之为H.264/AVC(或者H.264/MPEG-4 AVC或MPEG-4/H.264 AVC) + +##### H.264视频编解码的意义 + +H.264的出现就是为了创建比以前的视频压缩标准更高效的压缩标准,使用更好高效的视频压缩算法来压缩视频的占用空间,提高存储和传输的效率,在获得有效的压缩效果的同时,使得压缩过程引起的失真最小。MPEG-4 AVC和H.264 是目前较为主流的编码标准。主要定义了两方面的内容:视频数据压缩形式的编码表示和用重建视频信息的语法来描述编码方法。目的是为了保证兼容的编码器能够成功的交互工作,同时也允许制造厂商自由的开发具有竞争力的创新产品。制造厂商只需要注意的事情就是能够获得和标准中采用的方法同样的结果。 + +##### H.264编解码的理论依据 + +提到H.264编解码,我们先简单说一下视频压缩算法。视频压缩算法是通过去除时间、空间的冗余来实现的。在一段时间内,相邻的图像的像素、亮度与色温的差别很小,我们没比要对每一个图像进行完成的编码,而是可以选取这段时间的第一张图(也就是第一帧)作为完整的编码,而后面一段时间的图像只需要记录与第一张图(第一帧)在像素、亮度、色温等方面的差别数据即可。通过去除不同类型的冗余,可以明显的压缩数据,代价就是一部分信息失真。 + +H.264编解码在整个视频数据处理过程中,属于视频数据处理的编解码层,具体的可以查看本人总结的编解码流程图中的解码部分:[Thinking-in-AV](https://github.com/renhui/Thinking-in-AV/blob/master/音视频编解码/音视频解码流程概览.png)[/](https://github.com/renhui/Thinking-in-AV/blob/master/音视频编解码/音视频解码流程概览.png)[音视频编解码](https://github.com/renhui/Thinking-in-AV/blob/master/音视频编解码/音视频解码流程概览.png)[/](https://github.com/renhui/Thinking-in-AV/blob/master/音视频编解码/音视频解码流程概览.png)[音视频解码流程概览.png](https://github.com/renhui/Thinking-in-AV/blob/master/音视频编解码/音视频解码流程概览.png)。编码部分将流程反过来进行理解即可。 + +#### H.264相关概念 + +##### H.264 的基本单位 + +在H.264定义的结构中,一个视频图像编码后的数据叫做一帧。 一帧是由一个或多个片(slice)组成的,一个片是由一个或多个宏块(MB)组成的(宏块是H264编码的基本单位),一个宏块是由16x16的yuv数据组成的。 + +##### 帧类型 + +在H.264的协议中,定义了三类帧,分别是I帧、B帧和P帧。其中I帧就是之前我们所说的一个完整的图像帧,而B帧和P帧对应的就是之前说的不对全部图像做编码的帧。B帧和P帧的差别在于,P帧是参考之前的I帧生成的,B帧是参考前后的图像帧生成的。 + +在视频画面播放过程中,若I帧丢失了,则后面的P帧也就随着解不出来,就会出现视频画面黑屏的现象;若P帧丢失了,则视频画面会出现花屏、马赛克等现象。 + +##### GOP(画面组) + +一个GOP(Group Of Picture)就是一组连续的画面。GOP结构一般有两个数字,其中一个是GOP的长度(即两个I帧之间的B帧和P帧数),另一个数字为I帧和P帧之间的间隔距离(即B帧数)。在一个GOP内I帧解码不依赖任何的其它帧,P帧解码则依赖前面的I帧或P帧,B帧解码依赖前面的I帧或P帧及其后最近的一个P帧。 + +注意:在码率不变的前提下,GOP值越大,P、B帧的数量会越多,平均每个I、P、B帧所占用的字节数就越多,也就更容易获取较好的图像质量;Reference越大,B帧的数量越多,同理也更容易获得较好的图像质量。但是通过提高GOP值来提高图像质量是有限度的。H264编码器在遇到场景切换的情况时,会自动强制插入一个I帧,此时实际的GOP值被缩短了。另一方面,在一个GOP中,P、B帧是由I帧预测得到的,当I帧的图像质量比较差时,会影响到一个GOP中后续P、B帧的图像质量,直到下一个GOP开始才有可能得以恢复,所以GOP值也不宜设置过大。同时,由于P、B帧的复杂度大于I帧,所以过多的P、B帧会影响编码效率,使编码效率降低。另外,过长的GOP还会影响Seek操作的响应速度,由于P、B帧是由前面的I或P帧预测得到的,所以Seek操作需要直接定位,解码某一个P或B帧时,需要先解码得到本GOP内的I帧及之前的N个预测帧才可以,GOP值越长,需要解码的预测帧就越多,seek响应的时间也越长。 + +##### IDR 帧 + +GOP中的I帧又分为普通I帧和IDR帧,IDR帧就是GOP的第一个I帧,这样区分视为了方便控制编码和解码的流程。 IDR帧一定是I帧,但是I帧不一定是IDR帧。 + +IDR帧因为附带SPS、PPS等信息,解码器在收到 IDR 帧时,需要做的工作就是:把所有的 PPS 和 SPS 参数进行更新。 + +可以看出来IDR帧的作用是让解码器立刻刷新相关数据信息,避免出现较大的解码错误问题。 + +引入IDR帧机制是为了解码的重同步,当解码器解码到 IDR帧时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现错误,在这里可以获得重新同步的机会。IDR帧之后的帧永远不会使用IDR帧之前的数据来解码。 + +#### H.264 压缩方式 + +##### H.264 压缩算法 + +H264 的核心压缩算法是帧内压缩和帧间压缩,帧内压缩是生成I帧的算法,帧间压缩是生成B帧和P帧的算法。 +帧内(Intraframe)压缩的原理是:当压缩一帧图像时,仅考虑本帧的数据而不考虑相邻帧之间的冗余信息,一般采用有损压缩算法,由于帧内压缩是编码一个完整的图像,所以可以独立的解码、显示。帧内压缩率一般不高。 +帧间(Interframe)压缩的原理是:相邻几帧的数据有很大的相关性,或者说前后两帧信息变化很小的特点。连续的视频其相邻帧之间具有冗余信息,根据这一特性,压缩相邻帧之间的冗余量就可以进一步提高压缩量,减小压缩比。 + +而帧间压缩也称为时间压缩(Temporalcompression),它通过比较时间轴上不同帧之间的数据进行压缩。帧间压缩是无损的,它通过比较本帧与相邻帧之间的差异,仅记录本帧与其相邻帧的差值,这样可以大大减少数据量。 + +##### H.264压缩方式说明 + +H.264压缩视频数据时的具体方式如下: + +a). 分组,也就是将一系列变换不大的图像归为一个组,即一个GOP; + +b). 定义帧,将每组的图像帧归分为I帧、P帧和B帧三种类型; + +c). 预测帧, 以I帧做为基础帧,以I帧预测P帧,再由I帧和P帧预测B帧; + +d). 数据传输, 最后将I帧数据与预测的差值信息进行存储和传输。 + +#### H.264 分层结构 + +H.264的主要目标是为了有高的视频压缩比和良好的网络亲和性,H264将系统框架分为两个层面,分别是视频编码层面(VCL)和网络抽象层面(NAL)。 + +##### VLC层(Video Coding Layer) + +VLC层:包括核心压缩引擎和块,宏块和片的语法级别定义,设计目标是尽可能地独立于网络进行高效的编码; + +##### NAL层(Network Abstraction Layer) + +NAL层:负责将VCL产生的比特字符串适配到各种各样的网络和多元环境中,覆盖了所有片级以上的语法级别。 + +##### NALU (NAL Unit) + +H.264原始码流(裸流)是由一个接一个NALU组成,结构如下图,一个NALU = 一组对应于视频编码的NALU头部信息 + 一个原始字节序列负荷(RBSP,Raw Byte Sequence Payload)。 + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190219215612752-1340206683.png) + +一个原始的H.264 NALU 单元常由 [StartCode] [NALU Header] [NALU Payload] 三部分组成。 + +###### Start Code + +Start Code 用于标示这是一个NALU 单元的开始,必须是”00 00 00 01” 或”00 00 01”。 + +###### NAL Header + +NAL Header由三部分组成,forbidden_bit(1bit),nal_reference_bit(2bits)(优先级),nal_unit_type(5bits)(类型)。 + +###### RBSP(Raw Byte Sequence Payload)) + +下图是RBSP的序列的样例及相关类型参数的描述表: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190219220845324-241099332.png) + +SPS是序列参数集,包含的是针对一连续编码视频序列的参数,如标识符 seq_parameter_set_id、帧数及 POC 的约束、参考帧数目、解码图像尺寸和帧场编码模式选择标识等等。 + +PPS是图像参数集,对应的是一个序列中某一幅图像或者某几幅图像,其参数如标识符 pic_parameter_set_id、可选的 seq_parameter_set_id、熵编码模式选择标识、片组数目、初始量化参数和去方块滤波系数调整标识等等。 + +参数集是一个独立的数据单位,不依赖于参数集之外的其他句法元素。一个参数集不对应某一个特定的图像或者序列,同一个序列参数集可以被一个或者多个图像参数集引用。同理,一个图像参数集也可以被一个或者多个图像引用。只有在编码器认为需要更新参数集的内容时,才会发出新的参数集。 + +#### H.264 局限性 + +随着数字视频应用产业链的快速发展,视频应用向以下几个方向发展的趋势愈加明显: + +(1) 高清晰度(HigherDefinition):数字视频的应用格式从720P向1080P全面升级,而且现在4K的数字视频格式也已经成为常见。 + +(2) 高帧率(Higherframe rate ):数字视频帧率从30fps向60fps、120fps甚至240fps的应用场景升级; + +(3) 高压缩率(HigherCompression rate ):传输带宽和存储空间一直是视频应用中最为关键的资源,因此,在有限的空间和管道中获得最佳的视频体验一直是用户的不懈追求。 + +但是面对视频应用不断向高清晰度、高帧率、高压缩率方向发展的趋势,当前主流的视频压缩标准协议H.264的局限性不断凸显。主要体现在: + +(1) 宏块个数的爆发式增长,会导致用于编码宏块的预测模式、运动矢量、参考帧索引和量化级等宏块级参数信息所占用的码字过多,用于编码残差部分的码字明显减少。 + +(2) 由于分辨率的大大增加,单个宏块所表示的图像内容的信息大大减少,这将导致相邻的4 x 4或8 x 8块变换后的低频系数相似程度也大大提高,导致出现大量的冗余。 + +(3) 由于分辨率的大大增加,表示同一个运动的运动矢量的幅值将大大增加,H.264中采用一个运动矢量预测值,对运动矢量差编码使用的是哥伦布指数编码,该编码方式的特点是数值越小使用的比特数越少。因此,随着运动矢量幅值的大幅增加,H.264中用来对运动矢量进行预测以及编码的方法压缩率将逐渐降低。 + +(4) H.264的一些关键算法例如采用CAVLC和CABAC两种基于上下文的熵编码方法、deblock滤波等都要求串行编码,并行度比较低。针对GPU/DSP/FPGA/ASIC等并行化程度非常高的CPU,H.264的这种串行化处理越来越成为制约运算性能的瓶颈。 + +于是面向更高清晰度、更高帧率、更高压缩率视频应用的HEVC(H.265)协议标准应运而生。H.265在H.264标准2~4倍的复杂度基础上,将压缩效率提升一倍以上。 + +(注意:实际使用过程中,不能忽视265专利费用这个重要的问题。专利问题参考:[H.265成超级提款机 一场围绕专利授权的战争已经爆发](https://www.evolife.cn/html/2015/83717.html)) + +### 音视频编解码技术(二):AAC 音频编码技术 + +#### AAC编码概述 + +AAC是高级音频编码(Advanced Audio Coding)的缩写,出现于1997年,最初是基于MPEG-2的音频编码技术,目的是取代MP3格式。2000年,MPEG-4标准出台,AAC重新集成了其它技术包括SBR或PS特性,目前AAC可以定义为⼀种由 MPEG-4 标准定义的有损音频压缩格式 + + + +#### AAC编码规格简述 + +AAC共有9种规格,以适应不同的场合的需要: + + MPEG-2 AAC LC 低复杂度规格(Low Complexity) 注:比较简单,没有增益控制,但提高了编码效率,在中等码率的编码效率以及音质方面,都能找到平衡点 + + MPEG-2 AAC Main 主规格 + + MPEG-2 AAC SSR 可变采样率规格(Scaleable Sample Rate) + + MPEG-4 AAC LC 低复杂度规格(Low Complexity)---现在的手机比较常见的MP4文件中的音频部份就包括了该规格音频文件 + + MPEG-4 AAC Main 主规格 注:包含了除增益控制之外的全部功能,其音质最好 + + MPEG-4 AAC SSR 可变采样率规格(Scaleable Sample Rate) + + MPEG-4 AAC LTP 长时期预测规格(Long Term Predicition) + + MPEG-4 AAC LD 低延迟规格(Low Delay) + + MPEG-4 AAC HE 高效率规格(High Efficiency)---这种规格适合用于低码率编码,有Nero ACC 编码器支持 + +流行的Nero AAC编码程序只支持LC,HE,HEv2这三种规格,编码后的AAC音频,规格显示都是LC。HE其实就是AAC(LC)+ SBR技术,HEv2就是AAC(LC)+ SBR + PS技术; + +这里再说明一下HE和HEv2的相关内容: + +**HE:**HE-AAC v1(又称AACPlusV1,SBR),用容器的方法实现了AAC(LC)+SBR技术。SBR其实代表的是Spectral Band Replication(频段复制)。简要叙述一下,音乐的主要频谱集中在低频段,高频段幅度很小,但很重要,决定了音质。如果对整个频段编码,若是为了保护高频就会造成低频段编码过细以致文件巨大;若是保存了低频的主要成分而失去高频成分就会丧失音质。SBR把频谱切割开来,低频单独编码保存主要成分,高频单独放大编码保存音质,“统筹兼顾”了,在减少文件大小的情况下还保存了音质,完美的化解这一矛盾。 + +**HEv2:**用容器的方法包含了HE-AAC v1和PS技术。PS指“parametric stereo”(参数立体声)。原来的立体声文件文件大小是一个声道的两倍。但是两个声道的声音存在某种相似性,根据香农信息熵编码定理,相关性应该被去掉才能减小文件大小。所以PS技术存储了一个声道的全部信息,然后,花很少的字节用参数描述另一个声道和它不同的地方。 + + + +#### AAC编码的特点 + + (1). AAC是一种高压缩比的音频压缩算法,但它的压缩比要远超过较老的音频压缩算法,如AC-3、MP3等。并且其质量可以同未压缩的CD音质相媲美。 + + (2). 同其他类似的音频编码算法一样,AAC也是采用了变换编码算法,但AAC使用了分辨率更高的滤波器组,因此它可以达到更高的压缩比。 + + (3). AAC使用了临时噪声重整、后向自适应线性预测、联合立体声技术和量化哈夫曼编码等最新技术,这些新技术的使用都使压缩比得到进一步的提高。 + + (4). AAC支持更多种采样率和比特率、支持1个到48个音轨、支持多达15个低频音轨、具有多种语言的兼容能力、还有多达15个内嵌数据流。 + + (5). AAC支持更宽的声音频率范围,最高可达到96kHz,最低可达8KHz,远宽于MP3的16KHz-48kHz的范围。 + + (6). 不同于MP3及WMA,AAC几乎不损失声音频率中的甚高、甚低频率成分,并且比WMA在频谱结构上更接近于原始音频,因而声音的保真度更好。 + + (7). AAC采用优化的算法达到了更高的解码效率,解码时只需较少的处理能力。 + + + +#### AAC音频文件格式 + + + +##### ACC 音频文件格式类型 + +AAC的音频文件格式有ADIF & ADTS: + +**ADIF**:Audio Data Interchange Format 音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行,这种格式常用在磁盘文件中。 + +**ADTS**:Audio Data Transport Stream 音频数据传输流。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始。它的特征类似于mp3数据流格式。 + +简单说,ADTS可以在任意帧解码,也就是说它每一帧都有头信息。ADIF只有一个统一的头,所以必须得到所有的数据后解码。这两种的header的格式也是不同的,一般编码后的和抽取出的都是ADTS格式的音频流。 + +AAC的ADIF文件格式如下: + +| header() | raw_data_stream() | +| -------- | ----------------- | + + AAC的ADTS文件中一帧的格式如下: + +| ... | syncword | header() | error_check() | raw_data_block() | ... | +| ---- | -------- | -------- | ------------- | ---------------- | ---- | + + ADTS格式中两边的空白矩形表示当前一帧前后的数据。 + + + +##### ADIF 的 Header 结构 + +ADIF 的头信息如下图: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190221153841841-285646563.png) + + + + + + + + + + + + + + + + + +ADIF头信息位于AAC文件的起始处,接下来就是连续的 Raw Data Blocks。 + + + +##### ADTS 的 Header 头结构 + +一个 AAC 原始数据块长度是可变的,对原始帧加上 ADTS 头的封装,就形成了 ADTS 帧。ADTS 头中相对重要的信息有:采样率,声道数,帧长度 ,每一个带 ADTS 头信息的 AAC 流会清晰的告诉解码器它需要的这些信息,解码器才能解析读取。一般情况下 ADTS 的头信息都是 7 个字节,分为 2 部分: + + - adts_fixed_header(); —— 固定头信息,头信息中的每一帧都相同. + + - adts_variable_header(); —— 可变头信息,头信息则在帧与帧之间可变. + + ADTS 的固定头信息: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190221154236137-64996062.png) + + + + + + + + + + + + + + + +**Syncword:** 总是`0xFFF,`代表一个ADTS帧的开始, 用于同步,解码器可通过`0xFFF`确定每个ADTS的开始位置.因为它的存在,解码可以在这个流中任何位置开始, 即可以在任意帧解码**。 + +**ID:**MPEG Version: 0 for MPEG-4, 1 for MPEG-2 + +**Layer:**always: '00' + +**Protection_absent:**Warning, set to 1 if there is no CRC and 0 if there is CRC + +**Profile:**表示使用哪个级别的AAC,如profile的值等于 Audio Object Type的值减1,即profile = MPEG-4 Audio Object Type - 1 + +**sampling_frequency_index**: 采样率的下标 + + + +**channel_configuration**:声道数. 比如`2`表示立体声双声道. + +**aac_frame_length:** 一个ADTS帧的长度包括ADTS头和AAC原始流. + +**adts_buffer_fullness:**0x7FF 说明是码率可变的码流. + +**number_of_raw_data_blocks_in_frame:**表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧. + +在实际开发AAC编解码的时候,尤其是封装ADTS帧的时候,如何设置相关的Header的值,可以参考如下wiki内容: + +- https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio + +- https://wiki.multimedia.cx/index.php/ADTS + +**注意:**ACC LC和HE在采样率设置方面不同,LC格式的为正常索引,HE格式的索引为除2后对应的采样索引,这是因为:HE使用了SBR技术,即 Spectral Band Replication(频段复制),所以存储同样的音频内容,HE文件较小。使用时采样率为LC的一半。 + + + +ADTS的可变头信息: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190221154248708-1617715923.png) + + + + + + + + + + + +(1)Syncword 存在的目的是为了找出帧头在比特流中的位置,ADTS格式的帧头同步字为12比特的“1111 1111 1111”. + +(2)ADTS的头信息为两部分组成,其一为固定头信息,紧接着是可变头信息。固定头信息中的数据每一帧都相同,而可变头信息则在帧与帧之间可变。 + + + +##### AAC文件处理流程 + + (1). 判断文件格式,确定为ADIF或ADTS + + (2). 若为ADIF,解ADIF头信息,跳至第6步。 + + (3). 若为ADTS,寻找同步头。 + + (4). 解ADTS帧头信息。 + + (5). 若有错误检测,进行错误检测。 + + (6). 解块信息。 + + (7). 解元素信息。 + +**注意:**有时候在处理AAC音频流的时候 (比如:把 AAC 音频的 ES 流从 FLV 封装格式中抽出来送给硬件解码器),编码后的 AAC 文件在PC或者手机上不能播放,导致播放错误,很大可能的原因是 AAC 文件的每一帧缺少 ADTS 头信息文件的包装拼接,这时需要加上头文件 ADTS 即可。 + + + +#### 开源AAC解码器 + +a). 开源AAC解码器faad官方网站:http://www.audiocoding.com/ + +b). faad2源代码(VC工程)下载地址:http://download.csdn.net/detail/leixiaohua1020/6374877 + +## 流媒体协议 + +### 流媒体协议(一):HLS 协议 + +#### HLS 概述 + +HLS 全称是 HTTP Live Streaming,是一个由 Apple 公司提出的基于 HTTP 的媒体流传输协议,用于实时音视频流的传输。目前HLS协议被广泛的应用于视频点播和直播领域。 + +##### 原理介绍: + +通过将整条流切割成一个小的可以通过 HTTP 下载的媒体文件, 然后提供一个配套的媒体列表文件, 提供给客户端, 让客户端顺序地拉取这些媒体文件播放, 来实现看上去是在播放一条流的效果.由于传输层协议只需要标准的 HTTP 协议, HLS 可以方便的透过防火墙或者代理服务器, 而且可以很方便的利用 CDN 进行分发加速, 并且客户端实现起来也很方便. + +##### 整体架构 + +HLS的架构分为三部分:Server,CDN,Client 。即服务器、分发组件和客户端。 + +下面是 HLS 整体架构图: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190212111853680-1444421868.png) + +服务器用于接收媒体输入流,对它们进行编码,封装成适合于分发的格式,然后准备进行分发。 + +分发组件为标准的 Web 服务器。它们用于接收客户端请求,传递处理过的媒体,把资源和客户端联系起来。 + +客户端软件决定请求何种合适的媒体,下载这些资源,然后把它们重新组装成用户可以观看的连续流。 + +#### HLS 播放 + +##### 播放未加密HLS + +HLS格式的视频,只有安卓4.0以上才支持,目前基本4.0一下的机子基本可以考虑不兼容了,所以为了减少工作量,这里继续使用MediaPlayer来进行播放。 +HLS格式的视频,通过一个m3u8文件,然后里面包含若干个TS文件片段,这里有个苹果的官方的一个例子: +http://devimages.apple.com/iphone/samples/bipbop/gear1/prog_index.m3u8 +里面的内容为: + +```xml +####EXTM3U +####EXT-X-TARGETDURATION:10 +####EXT-X-MEDIA-SEQUENCE:0 +####EXTINF:10, no desc +fileSequence0.ts +####EXTINF:10, no desc +fileSequence1.ts +####EXTINF:10, no desc +fileSequence2.ts +####EXTINF:10, no desc +fileSequence3.ts +####EXTINF:10, no desc +fileSequence4.ts +####EXTINF:10, no desc +fileSequence5.ts +####EXTINF:10, no desc +fileSequence6.ts +####EXTINF:10, no desc +fileSequence7.ts +``` + +我们可以看到里面他又一个一个ts视频片段,这个一个一个视频片段就是我们需要的播放,那么他是如何被播放器识别播放的呢。 +其实上面的这些关键的字段都是约定好的,MediaPlayer会去按照规定好的字段去解析这个m3u8文件,然后拼接成最终的播放地址进行播放。 + +实现这种未加密的缓存还是比较好实现的,大概可以分为这几步: +1.我们首先按照特定的格式去解析这m3u8文件。 +2.按照解析出来的ts文件按照我们知道的规则组拼起来,其下载这些ts文件,存放在手机的sd卡 +3.我们需要在本地搭建一个本地http服务器,我们之前本打算搭建一个https,但是由于生成的证书是自己生成导致播放器不去访问本地的服务器。 +4.本地服务器我们通过过滤特定的接口名字,来实现根据不同ts名字返回不同的视频文件(这里最好生成和原始的ts文件的名字一样) +5.我们如何知道播放器播完一段视频呢,因为他是一段一段播放的,所以这里就需要我们在本地生成一份本地指向我们本地服务器的m3u8文件,直接播放 + +##### 播放加密HLS + +看下加密的m3u8文件的格式: + +```xml +####EXTM3U +####EXT-X-VERSION:3 +####EXT-X-KEY:METHOD=AES-128,URI="http://xxxxxx:5555//test/1102/test/segments.key" +####EXT-X-MEDIA-SEQUENCE:0 +####EXT-X-ALLOW-CACHE:YES +####EXT-X-TARGETDURATION:19 +####EXTINF:13.966667, +http://xxxxxx:5555/test/1102/test/segments0.ts +####EXTINF:10.000000, +http://xxxxxx:5555/test/1102/test/segments1.ts +####EXTINF:10.000000, +http://xxxxxx:5555/test/1102/test/segments2.ts +####EXTINF:10.000000, +http://xxxxxx.cn:5555/test/1102/test/segments3.ts +####EXTINF:10.000000, +http://xxxxxxn.cn:5555/test/1102/test/segments4.ts +####EXTINF:7.033333, +http://xxxxxx:5555/test/1102/test/segments5.ts +####EXTINF:10.000000, +``` + +我们看到了多了个字段EXT-X-KEY,这个也是m3u8给规定好的加密字段,如果包含这个字段播放器就会先去请求这个key,然后拿这个这个key去访问加密的TS视频就可以播放了。 其实看到这我们就因该有思路怎么去做,加密的缓存播放了。 + +实现播放加密缓存的思路: +1.我们首先按照特定的格式去解析这m3u8文件。 +2.按照解析出来的ts文件按照我们知道的规则组拼起来,其下载这些ts文件,存放在手机的sd卡,这些下载下来的TS视频文件是播放不了的,再把正确的key下载下来。 +3.我们需要在本地搭建一个本地http服务器,我们之前本打算搭建一个https,但是由于生成的证书是自己生成导致播放器不去访问本地的服务器。 +4.本地服务器我们通过过滤特定的接口名字,来实现根据不同ts名字返回不同的视频文件(这里最好生成和原始的ts文件的名字一样) + +#### HLS 协议总结 + +##### 优点: + +1. 客户端支持简单, 只需要支持 HTTP 请求即可, HTTP 协议无状态, 只需要按顺序下载媒体片段即可. +2. 使用 HTTP 协议网络兼容性好, HTTP 数据包也可以方便地通过防火墙或者代理服务器, CDN 支持良好. +3. Apple 的全系列产品支持,不需要安装任何插件就可以原生支持播放 HLS, 目前Android 也加入了对 HLS 的支持. +4. 自带多码率自适应机制。 + +##### 缺点: + +1. 相比 RTMP 这类长连接协议, 延时较高, 难以用到互动直播场景. +2. 对于点播服务来说, 由于 TS 切片通常较小, 海量碎片在文件分发, 一致性缓存, 存储等方面都有较大挑战. + +##### 改进 + +由于客户端每次请求 TS 或 M3U8 有可能一个新的连接请求, 无法有效的标识客户端, 一旦出现问题, 基本无法有效的定位问题。 +一般工业级的服务器都会对传统的 HLS 做一些改进,常见优化是对每个M3U8文件增加Session来标识一条 HLS 连接。 +不管通过哪种方式, 最终我们都能通过一个唯一的 id 来标识一条流, 这样在排查问题时就可以根据这个 id 来定位播放过程中的问题. + +### 流媒体协议(二):RTMP协议 + +#### 概念与摘要 + +RTMP协议从属于应用层,被设计用来在适合的传输协议(如TCP)上复用和打包多媒体传输流(如音频、视频和互动内容)。RTMP提供了一套全双工的可靠的多路复用消息服务,类似于TCP协议[RFC0793],用来在一对结点之间并行传输带时间戳的音频流,视频流,数据流。通常情况下,不同类型的消息会被分配不同的优先级,当网络传输能力受限时,优先级用来控制消息在网络底层的排队顺序。 + +#### RTMP块流 + +实时消息传递协议块流(RTMP块流)。它作为一款高级多媒体流协议提供了流的多路复用和打包服务。RTMP块流被设计用来传输实时消息协议,它可以使用任何协议来发送消息流。每个消息都包含时间戳和有效类型标识。RTMP块流和RTMP适用于各种视听传播的应用程序,包括一对一的,和一对多的视频直播、点播服务、互动会议应用程序。 + +当使用一个可靠的传输协议如TCP[RFC0793]时,RTMP块流提供了一种可以在多个流中,基于时间戳的端到端交付所有消息的方法。RTMP块流不提供任何优先级或类似形式的控制,但可以使用更高级别的协议来提供这样的优先级。例如,一个视频服务器可以根据发送的时间或确认每个消息的时间,来决定为一个网络差的用户丢弃视频信息,以确保音频信息的及时接收。 + +RTMP块流不仅包含了自己的协议控制信息,同时也提供了一个更高级别的协议机制,用来嵌入用户控制信息。 + + + +##### 消息格式 + +消息格式可以被分割成多个块,用来在更高的协议中支持多路复用。在创建块消息格式时,应该包含以下字段: + +**时间戳** +消息的时间戳。这个字段占用4字节。 + +**长度** +消息的有效长度。如果消息头不能被忽略,它应该包括长度。这个字段在块头中占用3字节。 + +**类型ID** +各种类型的协议控制消息的ID。这些消息使用RTMP块流协议和更高级别的协议来传输信息。所有其他类型的ID可以用在高级协议,这对于RTMP块流来说,是不透明的。事实上,RTMP块流中没有要求使用这些值作为类型;所有(无协议的)消息可能是相同的类型,或者应用程序使用这个字段来区分多个连接,而不是类型。这个字段在块头中占用1字节。 + +**消息流ID** +消息流ID可以是任意值。当同一个块流被复用到不同的消息流中时,可以通过消息流ID来区分它们。另外,对于RTMP块流而言,这是一个不透明值。该字段占用4字节,使用小端序。 + + + +##### 握手 + +RTMP连接从握手开始。它包含三个固定大小的块,不像其他的协议,是由头部大小可变的块组成的。 + +客户端(初始化连接的一端)和服务端发送同样的三个块。为了方便描述,客户端发送的三个块命名为C0,C1,C2;服务端发送的三个块命名为S0,S1,S2。   + +###### 握手序列 + +客户端通过发送C0和C1消息来启动握手过程。客户端必须接收到S1消息,然后发送C2消息。客户端必须接收到S2消息,然后发送其他数据。 + +服务端必须接收到C0或者C1消息,然后发送S0和S1消息。服务端必须接收到C1消息,然后发送S2消息。服务端必须接收到C2消息,然后发送其他数据。 + +###### C0和S0格式 + +C0和S0包由一个字节组成,下面是C0/S0包内的字段: + +| ``1``
``2``
``3``
``4``
``5`` | 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+
丨 version 丨
+-+-+-+-+-+-+-+
C0 and S0 bits | +| -------------------------------------------- | :----------------------------------------------------------: | + +**版本(8比特)** +在C0包内,这个字段代表客户端请求的RTMP版本号。在S0包内,这个字段代表服务端选择的RTMP版本号。此文档使用的版本是3。版本0-2用在早期的产品中,现在已经被弃用;版本4-31被预留用于后续产品;版本32-255(为了区分RTMP协议和文本协议,文本协议通常以可打印字符开始)不允许使用。如果服务器无法识别客户端的版本号,应该回复版本3。客户端可以选择降低到版本3,或者中止握手过程。 + +###### C1和S1格式 + +C1和S1包长度为1536字节,包含以下字段: + +| ``1``
``2``
``3``
``4``
``5``
``6``
``7``
``8``
``9``
``10``
``11``
``12``
``13``
``14`` | 0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
丨 time (4 bytes) 丨
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
丨 zero (4 bytes) 丨
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
丨 random bytes 丨
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
丨 random bytes 丨
丨 (cont) 丨
丨 .... 丨
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
C1 and S1 bits | +| :----------------------------------------------------------- | :----------------------------------------------------------: | + +**时间(4字节)** +本字段包含一个时间戳,客户端应该使用此字段来标识所有流块的时刻。时间戳取值可以为零或其他任意值。为了同步多个块流,客户端可能希望多个块流使用相同的时间戳。 + +**零(4字节)** +本字段必须为零。 + +**随机数据(1528字节)** +本字段可以包含任意数据。由于握手的双方需要区分另一端,此字段填充的数据必须足够随机(以防止与其他握手端混淆)。不过没必要为此使用加密数据或动态数据。 + +###### C2和S2格式 + +C2和S2包长度为1536字节,作为C1和S1的回应,包含以下字段: + +| ``1``
``2``
``3``
``4``
``5``
``6``
``7``
``8``
``9``
``10``
``11``
``12``
``13``
``14`` | 0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
丨 time (4 bytes) 丨
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
丨 time2 (4 bytes) 丨
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
丨 random echo 丨
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
丨 random echo 丨
丨 (cont) 丨
丨 .... 丨
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
C2 and S2 bits | +| :----------------------------------------------------------- | :----------------------------------------------------------: | + +**时间(4字节)** +本字段必须包含对端发送的时间戳。 + +**时间(4字节)** +本字段必须包含时间戳,取值为接收对端发送过来的握手包的时刻。 + +**随机数据(1528字节)** +本字段必须包含对端发送过来的随机数据。握手的双方可以使用时间1和时间2字段来估算网络连接的带宽和/或延迟,但是不一定有用。 + +#### RMTP握手 + + + +##### 握手过程示意图 + +| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 | +------------------+ +------------------+
丨Client丨TCP/IP Network丨Server丨
+------------------+ 丨 +------------------+
丨 丨 丨
Uninitialized 丨 Uninitialized
丨 C0 丨 丨
丨---------------------> 丨 C0 丨
丨 丨---------------------->丨
丨 C1 丨 丨
丨---------------------->丨 S0 丨
丨 丨<----------------------丨
丨 丨 S1 丨
Version sent 丨<----------------------丨
丨 S0 丨 丨
丨<----------------------丨 丨
丨 S1 丨 丨
丨<----------------------丨 Version sent
丨 丨 C1 丨
丨 丨---------------------->丨
丨 C2 丨 丨
丨---------------------->丨 S2 丨
丨 丨<----------------------丨
Ack sent 丨 Ack Sent
丨 S2 丨 丨
丨<----------------------丨 丨
丨 丨 C2 丨
丨 丨---------------------->丨
Handshake Done 丨 Handshake Done
丨 丨 丨
Pictorial Representation of Handshake
握手示意图 | +| ------------------------------------------------------------ | :----------------------------------------------------------: | + +下面是握手示意图中提到的状态: + +**未初始化** +协议版本号在此阶段发送。客户端和服务器均处于未初始化状态。客户端发送携带协议版本号的C0包。如果服务器支持此版本,回复S0和S1包。如果服务器不支持此版本,使用适当的动作回复。在RTMP协议中,此动作是中止连接。 +*注: 在”C0和S0格式”章节中提及,如果服务器不支持客户端的版本号,可以选择降到版本3或中止。* + +**发送版本** +客户端和服务器双方在未初始化状态后,会进入发送版本状态。之后,客户端等待S1包,服务器等待C1包。待接收到数据包,客户端发送C2包,服务器发送S2包。然后,双方都进入答复状态。客户端等待C2的答复,服务器等待S2的答复。 + +**握手完成** +客户端和服务器交换消息。 + +## 多媒体文件格式 + +### 多媒体文件格式(一):MP4 格式 + +在互联网常见的格式中,跨平台最好的应该就属MP4文件了。因为MP4文件既可以在PC平台的Flashplayer中播放,又可以在移动平台的Android、iOS等平台中进行播放,而且使用系统默认的播放器即可以播放。 + +MP4格式是最常见的多媒体文件格式。 + +#### MP4 格式标准介绍 + +MP4格式标准为ISO-14496 Part 12、ISO-14496 Part 14,标准内容不是很多,下面我们来介绍一下格式标准中一些重要的信息。 + +MP4是一种描述较为全面的容器格式,被认为可以在其中嵌入任何形式的数据,各种编码的视频、音频等都不在话下,常见的大部分的MP4文件存放的AVC(H.264)或MPEG-4(Part 2)编码的视频和AAC编码的音频。MP4格式的官方文件后缀名是“.mp4”,还有其他的以mp4为基础进行的扩展或者是阉割版的格式,如:M4V, 3GP, F4V等。 + +MP4是由一个个“Box”组成的,大Box中存放小Box,一级嵌套一级来存放媒体信息。下面我们来楚关于Box的几个概念: + +- MP4文件由许多个Box与FullBox组成。 +- 每个Box由Header和Data两部分组成。 +- FullBox是Box的扩展,其在Box结构的基础上,在Header中增加8位version标志和24的flags标志。 +- Header包含了整个Box的长度的大小(size)和类型(type),当size等于0时,代表这个Box是文件的最后一个Box。当size等于1时,说明Box长度需要更多的位来描述,在后面会自定义一个64位的largesize用来描述Box的长度。当type等于uuid时,说明这个Box中的数据是用户自定义扩展类型。 +- Data为Box的实际数据,可以是纯数据,也可以是更多的子Box。 +- 当一个Box中Data是一系列的子Box时,这个Box又可以称为Container(容器)Box。 + +**MP4常用参考标准Box排列方式**:https://github.com/renhui/Thinking-in-AV/tree/master/多媒体格式/MP4。 + +介绍了MP4的格式标准后,下面我们来介绍是三个MP4分析工具,为后续理解MP4文件一些关键信息做辅助工具。 + +#### MP4分析工具 + +可以用来分析MP4封装格式的工具比较多,除了FFmpeg、FFprobe之外,还有一些常用的工具,如Elecard StreamEye、mp4box、mp4info等;下面简单介绍一下这几款常用的工具: + +##### Elecard StreamEye + +Elecard StreamEye是一款非常强大的视频信息查看工具,能够查看帧的排列信息,将I帧、P帧、B帧以不同颜色的柱状展现出来,而且柱的长短将根据帧的大小展示。还能够通过Elecard StreamEye分析MP4的封装的内容信息,包括流信息、宏块的信息、文件头顶额信息、图像的信息以及文件的信息等。还能根据每一帧的顺序逐帧查看,可以看到每一帧的详细信息与状态。 + +示例如图: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190201113259539-809694731.png) + +##### mp4box + +mp4box 是GPAC项目中的一个组件,可以通过mp4box针对媒体文件进行合成、拆解等操作。 + +官网地址:https://gpac.wp.imt.fr/mp4box/。 + +其使用时的常用命令如下: + + + +``` + 1) mp4box -h + 查看mp4box中的所有帮助信息 + + 2) mp4box -h general + 查看mp4box中的通用帮助信息 + + 3) mp4box -info test.mp4 + 查看test.mp4文件是否有问题 + + 4) mp4box -add test.mp4 test-new.mp4 + 修复test.mp4文件格式不标准的问题,并把新文件保存在test-new.mp4中 + + 5) mp4box -inter 10000 test-new.mp4 + 解决开始播放test-new.mp4卡一下的问题,为HTTP下载快速播放有效,10000ms + + 6) mp4box -add file.avi new_file.mp4 + 把avi文件转换为mp4文件 + + 7) mp4box -hint file.mp4 + 为RTP准备,此指令将为文件创建RTP提示跟踪信息。这使得经典的流媒体服务器像darwinstreamingserver或QuickTime的流媒体服务器通过RTSP/RTP传输文件 + + 8) mp4box -cat test1.mp4 -cat test2.mp4 -new test.mp4 + 把test1.mp4和test2.mp4合并到一个新的文件test.mp4中,要求编码参数一致 + + 9) mp4box -force-cat test1.mp4 -force-cat test2.mp4 -new test.mp4 + 把test1.mp4和test2.mp4强制合并到一个新的文件test.mp4中,有可能不能播放 + + 10) mp4box -add video1.264 -cat video2.264 -cat video3.264 -add audio1.aac -cat audio2.aac -cat audio3.aac -new muxed.mp4 -fps 24 + 合并多段音视频并保持同步 + + 11) mp4box -split *time_sec* test.mp4 + 切取test.mp4中的前面time_sec秒的视频文件 + + 12) mp4box -split-size *size *test.mp4 + 切取前面大小为size KB的视频文件 + + 13) mp4box -split-chunk *S:E* test.mp4 + 切取起始为S少,结束为E秒的视频文件 + + 14) mp4box -add 1.mp4####video -add 2.mp4####audio -new test.mp4 + test.mp4由1.mp4中的视频与2.mp4中的音频合并生成 +``` + + + +而通过mp4box也可以查看mp4的信息,其输出内容格式非常类似ffprobe查看的信息,不过想对ffprobe更完善。 + +##### mp4info + + mp4info是一个不错的MP4分析工具,而且是可视化的工具,可以将MP4中的各个Box解析出来,并将其中的数据展现出来。分析MP4文件内容时使用mp4info将会更方便。 + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190201134614477-1081933213.png) + +结合着此工具,理解MP4的Box会更方便,更直观。 + +#### MP4格式重要Box + +##### ftyp(**File Type Box**) + +该Box有且只有1个,并且只能被包含在文件层,而不能被其他Box包含。该Box应该被放在文件的最开始,指示该MP4文件应用的相关信息。 + +“ftyp” body依次包括1个32位的major brand(4个字符),1个32位的minor version(整数)和1个以32位(4个字符)为单位元素的数组Compatible Brands。 + +##### moov**(Movie Box)** + +该box包含了文件媒体的metadata信息,“moov”是一个container box,具体内容信息由子box诠释。同File Type Box一样,该box有且只有一个,且只被包含在文件层。一般情况下,“moov”会紧随“ftyp”出现。 + +moov定义了一个MP4文件中的数据信息,类型是moov,是一个容器Atom,其至少必须包含一下三种Atom中的一种:mvhd标签、cmov标签、rmra标签。 + +- mvhd标签:Movie Header Atom,存放未压缩过的影片信息的头容器。 +- cmov标签:Compressed Movie Atom,压缩鬼哦的电影信息容器,此容器不常用。 +- rmra标签:Reference Movie Atom,参考电影信息容器,此容器不常用。 + +一般情况下,“moov”中会包含1个“mvhd”和若干个“trak”。其中“mvhd”为Header Box,一般作为“moov”的第一个子Box出现(对于其他Container Box来说,Header Box都应作为首个子box出现)。“trak”包含了一个track的相关信息,是一个Container Box。 + +##### trak(Track Box) + +“trak”也是一个container box,其子box包含了该track的媒体数据引用和描述(hint track除外)。一个MP4文件中的媒体可以包含多个track,且至少有一个track,这些track之间彼此独立,有自己的时间和空间信息。“trak”必须包含一个“tkhd”和一个“mdia”,此外还有很多可选的box(略)。其中“tkhd”为track header box,“mdia”为media box,该box是一个包含一些track媒体数据信息box的container box。 + +##### mdat(Meida Data Box) + +该box包含于文件层,可以有多个,也可以没有(当媒体数据全部为外部文件引用时),用来存储媒体数据。数据直接跟在box type字段后面,具体数据结构的意义需要参考metadata(主要在sample table中描述)。 + +##### free或skip(Free Space Box) + +“free”中的内容是无关紧要的,可以被忽略。该box被删除后,不会对播放产生任何影响。 + +##### stbl(Sample Table Box) + +“stbl”几乎是普通的MP4文件中最复杂的一个box了,首先需要回忆一下sample的概念。sample是媒体数据存储的单位,存储在media的chunk中,chunk和sample的长度均可互不相同,如下图所示。 + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190201151819385-350707816.png) + +普通MP4文件的结构重要的部分就讲完了,理解起来可能比较乱,下面这张图是常见的box的树结构图,可以用来大致了解MP4文件的构造。 + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190201151957496-1694061990.png) + +在MP4文件中,Box的结构与上图中所描述的一般没太大的差别。 + +#### MP4格式 与 FFmpeg实战 + +##### 在FFmpeg中的输出MP4的Demuxer信息 + +使用命令行 ffmpeg -h demuxder=mp4 查看MP4文件的Demuxer信息: + +``` +Demuxer mov,mp4,m4a,3gp,3g2,mj2 [QuickTime / MOV]: +Common extensions: mov,mp4,m4a,3gp,3g2,mj2. +``` + +##### 通过FFmepg faststart参数的使用,来理解mdat和moov的顺序的意义 + +正常情况下,ffmpeg生成的moov是在mdat写完成后再写入的。 + +下面是一个例子: + +``` +ffmpeg -i 好汉歌.flv -c copy -f mp4 好汉歌.mp4 +``` + +使用mp4info查看容器出现的顺序,如图: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190201155442402-1993684819.png) + +可以看出moov box是在mdat的下面。这时,我们可以使用faststart将上图的moov移动到mdat前面。 + +使用如下命令行: + +``` +ffmpeg -i 好汉歌.flv -c copy -f mp4 -movflags faststart 好汉歌.mp4 +``` + +然后使用mp4info查看MP4的容器顺序,就可以看到moov被移动到mdat前面了。如下图所示: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190201155935403-681188948.png) + +因为MP4的标准中描述的moov与mdat的存放位置前后并没有强制要求,所有有些时候moov这个Box在mdat的后面,有时候在mdat的前面。 + +在互联网的视频点播中,如果希望MP4文件被快速打开,则需要moov存放在mdat的前面;如果放在后面,则需要将MP4文件下载完成后才可以进行播放。 + +### 多媒体文件格式(二):FLV 格式 + +在网络的直播与点播场景中,FLV也是一种常见的格式,FLV是Adobe发布的一种可以作为直播也可以作为点播的封装格式,其封装格式非常简单,均以FLVTAG的形式存在,并且每一个TAG都是独立存在的,接下来就详细介绍一下FLV标准。 + +#### FLV 格式标准介绍 + +FLV包括文件头(File Header)和文件体(File Body)两部分,其中文件体由一系列的Tag组成。FLV文件的结构如下图: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190202192253797-739598791.png) + +##### 文件头 Header + +Header 部分记录了FLV的类型、版本等信息,是FLV的开头。一般差不多占9bytes。具体格式如下: + +1. 文件标识(3B):总是为”FLV”, 0x46 0x4c 0x56 + +2. 版本(1B):目前为0x01 + +3. 流信息(1B):文件的标志位说明。前5位保留,必须为0;第6位为音频Tag:1表示有音频;第七位保留,为0; 第8位为视频Tag:1表示有视频 + +4. Header长度(4B):整个Header的长度,一般为9(版本为0x01时);大于9表示下面还有扩展信息。即0x00000009。 + +下图是使用工具FlvAnalyzer获取到的FLV的Header的详细信息: + + ![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190203213848012-650203367.png) + +##### 文件体 FLV Body + +文件体由一系列的Tag组成。 + +其中,每个Tag前面还包含了Previous Tag Size字段,表示前面一个Tag的大小。Tag的类型可以是视频、音频和Script,每个Tag只能包含以上三种类型的数据中的一种。 + +下图是使用FlvAnalyzer获取到的Body信息: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190203214313883-27152075.png) + +##### Tag + +每个Tag由也是由两部分组成的:Tag Header和Tag Data。Tag Header里存放的是当前Tag的类型、数据区(Tag Data)长度等信息,具体如下: + +Tag类型(1):0x08:音频; 0x09:视频; 0x12:脚本; 其他:保留 + +数据区长度(3):数据区的长度 + +时间戳(3):整数,单位是毫秒。对于脚本型的tag总是0 (CTS) + +时间戳扩展(1):将时间戳扩展为4bytes,代表高8位。很少用到 + +StreamsID(3):总是0 + +数据区(由数据区长度决定):数据实体 + +下面是三个Tag类型说明: + +- Audio Tag Data结构(音频类型) :音频Tag Data区域开始的第一个字节包含了音频数据的参数信息,从第二个字节开始为音频流数据。 +- video Tag Data结构(视频类型):视频Tag Data开始的第一个字节包含视频数据的参数信息,从第二个字节开始为视频流数据。 +- Script Tag Data结构(脚本类型、帧类型):该类型Tag又被称为MetaData Tag,存放一些关于FLV视频和音频的元信息,比如:duration、width、height等。通常该类型Tag会作为FLV文件的第一个tag,并且只有一个,跟在File Header后。 + +#### FLV 分析工具 + +在上节的内容中,我们介绍了FLV的格式信息,同时也提到了FlvAnalyzer工具,下面我们就介绍两个工具,帮助大家整理和学习FLV相关知识: + +##### FlvAnalyzer + +通过FlvAnalyzer可以很清晰的看到FLV文件的基本结构,这样能够结合上面了解的FLV的知识,更清晰的查看FLV的格式及结构。 + +工具地址:https://github.com/renhui/Thinking-in-AV/blob/master/多媒体格式/FLV/FlvAnalyzer.exe + +工具使用如图: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190203210649720-1097359176.png) + +左侧树状结构显示flv的信息,可以清楚了解flv文件的结构; + +点击左侧节点,右侧显示对应hex与ascii信息,这样就不必打开二进制编辑器了; + +通过此工具可以查看audio tag与video tag各个字节(精确到bit)的详细信息,了解每个tag是如何构造的,同时右下角黑色输出框显示某个值的意义; + +##### FLV Format Analysis 工具 + +此工具是雷霄骅整理flvparse的开源代码,制作的flvformatanalysis工具,此工具可以用来帮助学习FLV封装格式结构。此外它还支持分离FLV中的视频流和音频流。 + +工具地址:https://github.com/renhui/Thinking-in-AV/blob/master/多媒体格式/FLV/SpecialFFLV.exe + +工具使用如图: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190203211948716-676353218.png) + +#### FLV格式 与 FFmpeg 实战 + +##### 使用FFmpeg生成带关键索引信息的FLV + +在网络视频点播文件为FLV格式文件时,人们经常用工具先对FLV文件进行一次转换,主要是将FLV文件中的关键帧建立一个索引,并将索引写到Metadata头中,这个步骤用FFmpeg可以实现,使用参数add_keyframe_index即可: + +``` +ffmpeg -i 好汉歌.mp4 -c copy -f flv -flvflags add_keyframe_index out.flv +``` + +生成FLV包含了关键帧索引信息,这些关键帧索引信息并不是FLV的标准字段,但是我们在实际应用中,特别是现在直播的应用中,我们往往需要向FLV格式中写入关键帧索引,并将这些索引文件写在Metadata 中,这些我们再次播放的时候,可以很快通过这些关键帧索引站到对应的位置,然后准确快速渲染播放。 + +##### 使用ffprobe查看FLV关键帧索引相关信息 + +除了在第二节介绍的两个工具,我们也可以使用ffprobe来解析FLV文件,并且还能将关键帧索引的相关信息打印出来,命令如下: + +``` +ffprobe -v trace -i out.flv +``` + +输出如下: + + + +``` +[NULL @ 0x7fc669002a00] Opening 'out.flv' for reading +[file @ 0x7fc667f00480] Setting default whitelist 'file,crypto' +Probing flv score:100 size:2048 +Probing mp3 score:1 size:2048 +[flv @ 0x7fc669002a00] Format flv probed with size=2048 and score=100 +[flv @ 0x7fc669002a00] Before avformat_find_stream_info() pos: 13 bytes read:32768 seeks:0 nb_streams:0 +[flv @ 0x7fc669002a00] type:18, size:1184, last:-1, dts:0 pos:21 +[flv @ 0x7fc669002a00] keyframe stream hasn't been created +[flv @ 0x7fc669002a00] type:9, size:45, last:-1, dts:0 pos:1220 +[flv @ 0x7fc669002a00] keyframe filepositions = 1296 times = 0 +[flv @ 0x7fc669002a00] keyframe filepositions = 159283 times = 3000 +[flv @ 0x7fc669002a00] keyframe filepositions = 258004 times = 4000 +[flv @ 0x7fc669002a00] keyframe filepositions = 272776 times = 4000 +[flv @ 0x7fc669002a00] keyframe filepositions = 405340 times = 6000 +[flv @ 0x7fc669002a00] keyframe filepositions = 1215104 times = 16000 +[flv @ 0x7fc669002a00] keyframe filepositions = 2529035 times = 26000 +[flv @ 0x7fc669002a00] keyframe filepositions = 3198814 times = 36000 +[flv @ 0x7fc669002a00] keyframe filepositions = 3623757 times = 41000 +[flv @ 0x7fc669002a00] keyframe filepositions = 4882191 times = 51000 +[flv @ 0x7fc669002a00] keyframe filepositions = 5951597 times = 61000 +[flv @ 0x7fc669002a00] keyframe filepositions = 6256906 times = 63000 +[flv @ 0x7fc669002a00] keyframe filepositions = 7235927 times = 73000 +[flv @ 0x7fc669002a00] keyframe filepositions = 8175324 times = 83000 +[flv @ 0x7fc669002a00] keyframe filepositions = 9203399 times = 93000 +[flv @ 0x7fc669002a00] keyframe filepositions = 9936528 times = 103000 +[flv @ 0x7fc669002a00] keyframe filepositions = 11056393 times = 113000 +[flv @ 0x7fc669002a00] keyframe filepositions = 12183978 times = 123000 +[flv @ 0x7fc669002a00] keyframe filepositions = 13014068 times = 133000 +[flv @ 0x7fc669002a00] keyframe filepositions = 13610750 times = 143000 +[flv @ 0x7fc669002a00] keyframe filepositions = 14628601 times = 153000 +[flv @ 0x7fc669002a00] keyframe filepositions = 15873046 times = 163000 +[flv @ 0x7fc669002a00] keyframe filepositions = 17112198 times = 173000 +[flv @ 0x7fc669002a00] keyframe filepositions = 18301365 times = 182000 +[flv @ 0x7fc669002a00] keyframe filepositions = 18604436 times = 186000 +[flv @ 0x7fc669002a00] 0 17 0 +[flv @ 0x7fc669002a00] type:8, size:9, last:-1, dts:0 pos:1280 +[flv @ 0x7fc669002a00] 1 AF 0 +[flv @ 0x7fc669002a00] type:9, size:2117, last:-1, dts:0 pos:1304 +[flv @ 0x7fc669002a00] 0 17 0 +[NULL @ 0x7fc668809e00] nal_unit_type: 7, nal_ref_idc: 3 +[NULL @ 0x7fc668809e00] nal_unit_type: 8, nal_ref_idc: 3 +[NULL @ 0x7fc668809e00] user data:"x264 - core 142 r2 dd79a61 - H.264/MPEG-4 AVC codec - Copyleft 2003-2014 - http://www.videolan.org/x264.html - options: cabac=1 ref=8 deblock=1:-1:-1 analyse=0x1:0x131 me=umh subme=9 psy=1 psy_rd=1.00:0.15 mixed_ref=1 me_range=24 chroma_me=1 trellis=2 8x8dct=0 cqm=0 deadzone=21,11 fast_pskip=0 chroma_qp_offset=-3 threads=24 lookahead_threads=2 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 stitchable=1 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=2 b_bias=0 direct=3 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=25 scenecut=40 intra_refresh=0 rc_lookahead=60 rc=2pass mbtree=1 bitrate=680 ratetol=1.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 cplxblur=20.0 qblur=0.5 ip_ratio=1.40 aq=1:1.00" +[h264 @ 0x7fc668809e00] nal_unit_type: 7, nal_ref_idc: 3 +[h264 @ 0x7fc668809e00] nal_unit_type: 8, nal_ref_idc: 3 +[h264 @ 0x7fc668809e00] nal_unit_type: 6, nal_ref_idc: 0 +[h264 @ 0x7fc668809e00] nal_unit_type: 5, nal_ref_idc: 3 +[h264 @ 0x7fc668809e00] user data:"x264 - core 142 r2 dd79a61 - H.264/MPEG-4 AVC codec - Copyleft 2003-2014 - http://www.videolan.org/x264.html - options: cabac=1 ref=8 deblock=1:-1:-1 analyse=0x1:0x131 me=umh subme=9 psy=1 psy_rd=1.00:0.15 mixed_ref=1 me_range=24 chroma_me=1 trellis=2 8x8dct=0 cqm=0 deadzone=21,11 fast_pskip=0 chroma_qp_offset=-3 threads=24 lookahead_threads=2 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 stitchable=1 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=2 b_bias=0 direct=3 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=25 scenecut=40 intra_refresh=0 rc_lookahead=60 rc=2pass mbtree=1 bitrate=680 ratetol=1.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 cplxblur=20.0 qblur=0.5 ip_ratio=1.40 aq=1:1.00" +[h264 @ 0x7fc668809e00] Reinit context to 576x432, pix_fmt: yuv420p +[h264 @ 0x7fc668809e00] no picture +[flv @ 0x7fc669002a00] type:9, size:1653, last:-1, dts:40 pos:3436 +[flv @ 0x7fc669002a00] 0 27 0(省略......) +[flv @ 0x7fc669002a00] 1 AF 0 +[flv @ 0x7fc669002a00] type:9, size:88, last:-1, dts:1600 pos:31870 +[flv @ 0x7fc669002a00] 0 27 0 +[flv @ 0x7fc669002a00] All info found +[flv @ 0x7fc669002a00] stream 0: start_time: 0.080 duration: -9223372036854776.000 +[flv @ 0x7fc669002a00] stream 1: start_time: 0.080 duration: -9223372036854776.000 +[flv @ 0x7fc669002a00] format: start_time: 0.080 duration: 189.440 bitrate=787 kb/s +[flv @ 0x7fc669002a00] After avformat_find_stream_info() pos: 31965 bytes read:32768 seeks:0 frames:74 +Input #0, flv, from 'out.flv': + Metadata: + major_brand : isom + minor_version : 512 + compatible_brands: isomiso2avc1mp41 + artist : yinyuetai.com + album : Yinyuetai + date : 04/01/15 15:51:32 + comment : Yinyuetai-1TR1026 + encoder : Lavf57.83.100 + hasVideo : true + hasKeyframes : true + hasAudio : true + hasMetadata : true + canSeekToEnd : true + datasize : 18639072 + videosize : 16303552 + audiosize : 2335015 + lasttimestamp : 189 + lastkeyframetimestamp: 187 + lastkeyframelocation: 18603951 + Duration: 00:03:09.44, start: 0.080000, bitrate: 787 kb/s + Stream #0:0, 41, 1/1000: Video: h264 (Main), 1 reference frame, yuv420p(progressive, left), 576x432, 0/1, 684 kb/s, 25 fps, 25 tbr, 1k tbn, 50 tbc + Stream #0:1, 33, 1/1000: Audio: aac (HE-AAC), 44100 Hz, stereo, fltp, 95 kb/s +[h264 @ 0x7fc668824400] nal_unit_type: 7, nal_ref_idc: 3 +[h264 @ 0x7fc668824400] nal_unit_type: 8, nal_ref_idc: 3 +[AVIOContext @ 0x7fc667f005c0] Statistics: 32768 bytes read, 0 seeks +``` + + + +从以上内容可以看到,输出信息包含了keyframe关键帧存储在文件中的偏移位置及时间戳。 + +### 多媒体文件格式(三):M3U8 格式 + +#### M3U8 格式标准介绍 + +M3U8文件是指UTF-8编码格式的M3U文件。M3U文件是记录了一个索引纯文本文件,打开它时播放软件并不是播放它,而是根据它的索引找到对应的音视频文件的网络地址进行在线播放。 + +M3U8是一种常见的流媒体格式,主要以文件列表的形式存在,既支持直播又支持点播,尤其在Android、iOS等平台最为常用。 + +下面是CCTV6直播播放地址:http://ivi.bupt.edu.cn/hls/cctv6hd.m3u8的M3U8的文件列表: + + + +``` +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:35232 +#EXT-X-TARGETDURATION:10 +#EXTINF:10.000, +cctv6hd-1549272376000.ts +#EXTINF:10.000, +cctv6hd-1549272386000.ts +#EXTINF:10.000, +cctv6hd-1549272396000.ts +#EXTINF:10.000, +cctv6hd-1549272406000.ts +#EXTINF:10.000, +cctv6hd-1549272416000.ts +#EXTINF:10.000, +cctv6hd-1549272426000.ts +``` + + + +下面我们来分别说明一下相关的几个字段: + +- EXTM3U:这个是M3U8文件必须包含的标签,并且必须在文件的第一行,所有的M3U8文件中必须包含这个标签。 +- EXT-X-VERSION:M3U8文件的版本,常见的是3(目前最高版本应该是7)。 +- EXT-X-TARGETDURATION:该标签指定了媒体文件持续时间的最大值,播放文件列表中的媒体文件在EXTINF标签中定义的持续时间必须小于或者等于该标签指定的持续时间。该标签在播放列表文件中必须出现一次。 +- EXT-X-MEDIA-SEQUENCE:M3U8直播是的直播切换序列,当播放打开M3U8时,以这个标签的值作为参考,播放对应的序列号的切片。 +- EXTINF:EXTINF为M3U8列表中每一个分片的duration,如上面例子输出信息中的第一片的duration为10秒。在EXTINF标签中,除了duration值,还可以包含可选的描述信息,主要为标注切片信息,使用逗号分隔开。 + +关于客户端播放M3U8的标准还有更多的讲究,下面我们来介绍一些: + +1. 分片必须是动态改变的,序列不能相同,并且序列必须是增序的。 +2. 当M3U8没有出现EXT-X-ENDLIST标签时,无论这个M3U8列表中有多少分片,播放分片都是从倒数第三片开始播放,如果不满三片则不应该播放。当然如果有些播放器做了特别定制了,则可以不遵照这个原则。 +3. 以播放当前分片的duration时间刷新M3U8列表,然后做对应的加载动作。 +4. 前一片分片和后一片分片有不连续的时候,播放可能会出错,那么需要X-DISCONTINUTY标签来解决这个错误。 +5. 如果播放列表在刷新之后与之前的列表相同,那么在播放当前分片duration一半的时间内在刷新一次。 + +在上面,我们提到了,一些上面例子没有出现的一些标签字段,下面我们针对一些额外的标签做一些补充说明: + +- EXT-X-ENDLIST:若出现EXT-X-ENDLIST标签,则表明M3U8文件不会再产生更多的切片,可以理解为该M3U8已停止更新,并且播放分片到这个标签后结束。M3U8不仅仅是可以作为直播,也可以作为点播存在,在M3U8文件中保存所有切片信息最后使用EXT-X-ENDLIST结尾,这个M3U8即为点播M3U8。EXT-X-ENDLIST标签可能会出现在播放列表文件的任何地方,但是不能出现两次或以上。 +- EXT-X-STREAM-INF:EXT-X-STREAM-INF标签出现在M3U8时,主要是出现在多级M3U8文件中时,例如M3U8中包含子M3U8列表,或者主M3U8中包含多码率M3U8时;该标签后需要跟一些属性,下面就来逐一说明一下这些属性: + +1. 1. BANDWIDTH:BANDWIDTH的值为最高码率值,当播放EXT-X-STREAM-INF下对应的M3U8时占用的最大码率(必要参数)。 + 2. AVERAGE-BANDWIDTH:AVERAGE-BANDWIDTH的值为平均码率值,当播放EXT-X-STREAM-INF下对应的M3U8时占用的平均码率。(可选参数)。 + 3. CODECS:CODECS的值用于声明EXT-X-STREAM-INF下面对应M3U8里面的音视频编码、视频编码的信息(可选参数)。 + 4. RESOLUTION:M3U8中视频的宽高信息描述(可选参数)。 + 5. FRAME-RATE:子M3U8中的视频帧率(可选参数)。 + +#### HLS 与 M3U8 + +HLS(全称:Http Live Streaming)是由Apple公司定义的用于实时流传输的协议,HLS基于HTTP协议实现,传输内容包括两部分,一是M3U8描述文件,二是TS媒体文件。 + +HLS的优势为:自适应码率流播(adaptive streaming)。效果就是客户端会根据网络状况自动选择不同码率的视频流,条件允许的情况下使用高码率,网络繁忙的时候使用低码率,并且能够自动在二者之间随意切换。这对移动设备网络状况不稳定的情况下保障流畅播放非常有帮助。实现方法是服务器端提供多码率视频流,并且在列表文件中注明,播放器根据播放进度和下载速度进行自动调整。 + +为什么要用 TS 而不是 MP4?这是因为两个 TS 片段可以无缝拼接,播放器能连续播放,而 MP4 文件由于编码方式的原因,两段 MP4 不能无缝拼接,播放器连续播放两个 MP4 文件会出现破音和画面间断,影响用户体验。而且如果要在一段长达一小时的视频中跳转,如果使用单个 MP4 格式的视频文件,并且也是用 HTTP 协议,那么需要代理服务器支持 HTTP range request 获取大文件中的一部分。这样的话,对于代理服务器的性能来说要求较高。而 HTTP Live Streaming 则只需要根据列表文件中的时间轴找出对应的 TS 片段下载即可,不需要 range request,对代理服务器的要求小很多。所有代理服务器都支持小文件的高效缓存。 + +#### FFmpeg转HLS文件(M3U8)实战 + +##### FFmpeg转MP4为HLS(M3U8)文件 + +将MP4文件转换成HLS(M3U8)命令行: + +``` +ffmpeg -re -i 好汉歌.mp4 -c copy -f hls -bsf:v h264_mp4toannexb output.m3u8 +``` + +可以看到生成的M3U8及相应的ts文件: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190205171531524-1337912163.png) + +查看一下生成的M3U8文件: + + + +``` +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:10 +#EXT-X-MEDIA-SEQUENCE:19 +#EXTINF:10.000000, +output19.ts +#EXTINF:10.000000, +output20.ts +#EXTINF:9.280000, +output21.ts +#EXTINF:4.120000, +output22.ts +#EXTINF:2.440000, +output23.ts +#EXT-X-ENDLIST +``` + + + +细心的人可能发现一个问题,就是生成的m3u8文件里只有最后的五个片段的信息。这是因为ffmpeg 默认的list size 为5,所以只获得最后的5个片段。为了解决这个问题,需要指定参数-hls_list_size 0,这样就能包含所有的片段。 + +下面是优化后的命令行: + +``` +ffmpeg -re -i 好汉歌.mp4 -c copy -f hls -hls_list_size 0 -bsf:v h264_mp4toannexb output.m3u8 +``` + +这时,我们可以看到从output0.ts到output23.ts的文件列表了。 + +可能有人会发现,无论是优化之前的命令行,还是优化后的命令行都有一个参数-bsf:v h264_mp4toannexb,这个参数的作用是将MP4中的H.264数据转换成为H.264 AnnexB标准的编码,AnnexB标准的编码常见于实时传输流中。如果源文件为FLV、TS等可以作为直播传输流的视频,则不需要这个参数。 + +下面我们逐一介绍下使用FFmpeg生成HLS时还可以配置的其他参数。 + +#### FFmpeg 转 HLS (M3U8) 文件命令参数 + +##### start_number 参数 + +start_number 参数用于设置M3U8列表中的第一片的序列数。 + +下面的例子中,我们使用start_number参数设置M3U8中的第一片序列书为100,命令行如下: + +``` +ffmpeg -re -i huijia.mp4 -c copy -f hls -start_number 100 -hls_list_size 0 -bsf:v h264_mp4toannexb output.m3u8 +``` + +输出的M3U8内容如下: + + + +``` +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:100 +#EXTINF:3.000000, +output100.ts +#EXTINF:3.000000, +output101.ts +#EXTINF:3.000000, +output102.ts +#EXTINF:3.000000, +output103.ts +#EXTINF:3.000000, +output104.ts +#EXTINF:3.000000, +output105.ts +#EXTINF:3.000000, +output106.ts +#EXTINF:1.000000, +output107.ts +#EXT-X-ENDLIST +``` + + + +从输出可以看出,切片的第一片编号是100,上面的命令行参数的-start_number参数已生效。 + +##### hls_time 参数 + +hls_time参数用于设置M3U8列表中切片的duration。 + +下面的例子中,我们使用hls_time参数设置M3U8的TS文件的每一片时长为9秒左右。命令行如下: + +``` +ffmpeg -re -i huijia.mp4 -c copy -f hls -hls_time 9 -hls_list_size 0 -bsf:v h264_mp4toannexb output.m3u8 +``` + +然后查看输出的M3U8内容如下: + + + +``` +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:9 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:9.000000, +output0.ts +#EXTINF:9.000000, +output1.ts +#EXTINF:4.000000, +output2.ts +#EXT-X-ENDLIST +``` + + + +可以看到TS的文件每一片的时常都是9秒左右,hls_time参数生效。 + +( 注意:hls_time设置后效果不一定准确,会受到关键帧大小及其他因素影响。) + +如果需要相对非常准确的切片,可以添加hls_flags的子参数split_by_time来保证生成的切片能够与hls_time设置的切片时长差不多。 + +( 注意:split_by_time参数必须与hls_time配合使用,并且使用split_by_time参数有可能会影响首画面体验,例如花屏或者首画面显示慢的问题,因为视频的第一帧不一定是关键帧。) + +##### hls_list_size 参数 + +hls_list_size参数用于为M3U8列表中的TS切片的个数。其中设置为0的时候,将包含所有。 + +这个命令,我们在第3节优化MP4转HLS文件的命令行时使用到了。 + +下面的例子中,我们使用hls_list_size参数设置只保留2片TS切片。命令行如下: + +``` +ffmpeg -re -i huijia.mp4 -c copy -f hls -hls_list_size 2 -bsf:v h264_mp4toannexb output.m3u8 +``` + +查看输出的M3U8内容如下: + + + +``` +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:6 +#EXTINF:3.000000, +output6.ts +#EXTINF:1.000000, +output7.ts +#EXT-X-ENDLIST +``` + + + +从输出的M3U8内容可以看出,在M3U8文件中只保留了2片TS的文件信息,可以看出hls_list_size设置生效了。 + +##### hls_base_url参数 + +hls_base_url 参数用于为M3U8列表的文件路径设置前置基本路径参数,因为在FFmpeg中生成M3U8时写入的TS切片路径默认为M3U8生成的路径相同,但是实际上TS所存储的路径既可以为本地绝对路径,也可以为相对路径,还可以为网络路径,因此使用hls_base_url参数可以达到该效果,命令行如下: + +``` +ffmpeg -re -i huijia.mp4 -c copy -f hls -hls_base_url /Users/renhui/Desktop/test/ -bsf:v h264_mp4toannexb output.m3u8 +``` + +查看输出的M3U8内容如下: + + + +``` +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:3 +#EXTINF:3.000000, +/Users/renhui/Desktop/test/output3.ts +#EXTINF:3.000000, +/Users/renhui/Desktop/test/output4.ts +#EXTINF:3.000000, +/Users/renhui/Desktop/test/output5.ts +#EXTINF:3.000000, +/Users/renhui/Desktop/test/output6.ts +#EXTINF:1.000000, +/Users/renhui/Desktop/test/output7.ts +#EXT-X-ENDLIST +``` + + + +可以看到,TS的路径变为绝对路径了,使用ffplay output.m3u8播放,看到播放是能够正常播放的。这样就可以说明hls_base_url生效了。 + +### 多媒体文件格式(四):TS 格式 + +#### TS 格式标准介绍 + +TS是一种音视频封装格式,全称为MPEG2-TS。其中TS即"Transport Stream"的缩写。 + +先简要介绍一下什么是MPEG2-TS: + +DVD的音视频格式为MPEG2-PS,全称是Program Stream。而TS的全称则是Transport Stream。MPEG2-PS主要应用于存储的具有固定时长的节目,如DVD电影,而MPEG-TS则主要应用于实时传送的节目,比如实时广播的电视节目。这两种格式的主要区别是什么呢?简单地打个比喻说,你将DVD上的VOB文件的前面一截cut掉(或者干脆就是数据损坏),那么就会导致整个文件无法解码了,而电视节目是你任何时候打开电视机都能解码(收看)的。 + +**所以,MPEG2-TS格式的特点就是要求从视频流的任一片段开始都是可以独立解码的。** + +我们可以看出,TS格式是主要用于直播的码流结构,具有很好的容错能力。通常TS流的后缀是.ts、.mpg或者.mpeg,多数播放器直接支持这种格式的播放。TS流中不包含快速seek的机制,只能通过协议层实现seek。HLS协议基于TS流实现的。 + +TS格式分析工具:链接: https://pan.baidu.com/s/1mXPIyTt6dzuDUaTRqRMgCw 提取码: je5m + +#### TS 格式详解 + +TS文件(流)可以分为三层:TS层(Transport Stream)、PES层(Packet Elemental Stream)、ES层(Elementary Stream)。 + +ES层就是音视频数据,PES层是在音视频数据上加了时间戳等对数据帧的说明信息,TS层是在PES层上加入了数据流识别和传输的必要信息。TS文件(码流)由多个TS Packet组成的。 + +下图是TS文件(码流)的分层结构图: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190211204923332-1306467199.png) + +原图可以在:https://github.com/renhui/Thinking-in-AV/blob/master/多媒体格式/TS/1.TS分层结构.jpg 查看。 + +#### TS层 + +TS包大小固定为188字节,TS层分为三个部分:TS Header、Adaptation Field、Payload。 + +TS Header固定4个字节;Adaptation Field可能存在也可能不存在,主要作用是给不足188字节的数据做填充;Payload是PES数据。 + +##### TS Header + +TS包的包头提供关于传输方面的信息。 + +TS包的包头长度不固定,前4个字节是固定的,后面可能跟有自适应字段(适配域)。4个字节是最小包头。 + +包头的结构体字段如下: + +- sync_byte(同步字节):固定为0x47;该字节由解码器识别,使包头和有效负载可相互分离。 +- transport_error_indicator(传输错误标志):‘1’表示在相关的传输包中至少有一个不可纠正的错误位。当被置1后,在错误被纠正之前不能重置为0。 +- payload_unit_start_indicator(负载起始标志):为1时,表示当前TS包的有效载荷中包含PES或者PSI的起始位置;在前4个字节之后会有一个调整字节,其的数值为后面调整字段的长度length。因此有效载荷开始的位置应再偏移1+[length]个字节。 +- transport_priority(传输优先级标志):‘1’表明当前TS包的优先级比其他具有相同PID, 但此位没有被置‘1’的TS包高。 +- PID:指示存储与分组有效负载中数据的类型。 +- transport_scrambling_control(加扰控制标志):表示TS流分组有效负载的加密模式。空包为‘00’,如果传输包包头中包括调整字段,不应被加密。其他取值含义是用户自定义的。 +- adaptation_field_control(适配域控制标志):表示包头是否有调整字段或有效负载。‘00’为ISO/IEC未来使用保留;‘01’仅含有效载荷,无调整字段;‘10’ 无有效载荷,仅含调整字段;‘11’ 调整字段后为有效载荷,调整字段中的前一个字节表示调整字段的长度length,有效载荷开始的位置应再偏移[length]个字节。空包应为‘10’。 +- continuity_counter(连续性计数器):随着每一个具有相同PID的TS流分组而增加,当它达到最大值后又回复到0。范围为0~15。 + +##### TS Adaptation Field + +Adaptation Field的长度要包含传输错误指示符标识的一个字节。 + +PCR是节目时钟参考,PCR、DTS、PTS都是对同一个系统时钟的采样值,PCR是递增的,因此可以将其设置为DTS值,音频数据不需要PCR。 + +打包TS流时PAT和PMT表是没有Adaptation Field的,不够的长度直接补0xff即可。 + +视频流和音频流都需要加adaptation field,通常加在一个帧的第一个ts包和最后一个ts包里,中间的ts包不加。 + +##### TS Payload + +TS包中Payload所传输的信息包括两种类型:视频、音频的PES包以及辅助数据;节目专用信息PSI。 + +TS包也可以是空包。空包用来填充TS流,可能在重新进行多路复用时被插入或删除。 + +视频、音频的ES流需进行打包形成视频、音频的 PES流。辅助数据(如图文电视信息)不需要打成PES包。 + +#### PES层 & ES 层 + +##### PES层 + +PES结构如图: + +![img](https://img2018.cnblogs.com/blog/682616/201902/682616-20190211214448146-501104692.png) + +从上面的结构图可以看出,PES层是在每一个视频/音频帧上加入了时间戳等信息,PES包内容很多,下面我们说明一下最常用的字段: + +- pes start code:开始码,固定为0x000001。 +- stream id:音频取值(0xc0-0xdf),通常为0xc0;视频取值(0xe0-0xef),通常为0xe0。 +- pes packet length:后面pes数据的长度,0表示长度不限制,只有视频数据长度会超过0xffff。 +- pes data length:后面数据的长度,取值5或10。 +- pts:33bit值 +- dts:33bit值 + +关于时间戳PTS和DTS的说明: + +1. PTS是显示时间戳、DTS是解码时间戳。 +2. 视频数据两种时间戳都需要,音频数据的PTS和DTS相同,所以只需要PTS。 + +有PTS和DTS两种时间戳是B帧引起的,I帧和P帧的PTS等于DTS。如果一个视频没有B帧,则PTS永远和DTS相同。 + +从文件中顺序读取视频帧,取出的帧顺序和DTS顺序相同。DTS算法比较简单,初始值 + 增量即可,PTS计算比较复杂,需要在DTS的基础上加偏移量。 + +音频的PES中只有PTS(同DTS),视频的I、P帧两种时间戳都要有,视频B帧只要PTS(同DTS)。 + +##### ES 层 + +ES层指的就是音视频数据。 + +一般的,视频为H.264视频,音频为AAC音频。 + +#### TS流生成及解析流程 + +##### TS 流生成流程 + +- 将原始音视频数据压缩之后,压缩结果组成一个基本码流(ES)。 +- 对ES(基本码流)进行打包形成PES。 +- 在PES包中加入时间戳信息(PTS/DTS)。 +- 将PES包内容分配到一系列固定长度的传输包(TS Packet)中。 +- 在传输包中加入定时信息(PCR)。 +- 在传输包中加入节目专用信息(PSI) 。 +- 连续输出传输包形成具有恒定比特率的MPEG-TS流。 + +##### TS 流解析流程 + +- 复用的MPEG-TS流中解析出TS包; +- 从TS包中获取PAT及对应的PMT; +- 从而获取特定节目的音视频PID; +- 通过PID筛选出特定音视频相关的TS包,并解析出PES; +- 从PES中读取到PTS/DTS,并从PES中解析出基本码流ES; +- 将ES交给解码器,获得压缩前的原始音视频数据。 + +### 多媒体文件格式(五):PCM / WAV 格式 + +#### 名词解析 + +PCM(Pulse Code Modulation)也被称为脉码编码调制,PCM中的声音数据没有被压缩,它是由模拟信号经过采样、量化、编码转换成的标准的数字音频数据。采样转换方式参考下图进行了解: + +![img](https://img2018.cnblogs.com/blog/682616/202001/682616-20200104125147149-1090015063.png) + +音频采样包含以下几大要素: + + + +##### 采样率 + +采样率表示音频信号每秒的数字快照数。该速率决定了音频文件的频率范围。采样率越高,数字波形的形状越接近原始模拟波形。低采样率会限制可录制的频率范围,这可导致录音表现原始声音的效果不佳。根据奈奎斯特采样定理,为了重现给定频率,采样率必须至少是该频率的两倍。例如,一般CD唱片的采样率为每秒 44,100 个采样,因此可重现最高为 22,050 Hz 的频率,此频率刚好超过人类的听力极限 20,000 Hz。 + +![img](https://img2018.cnblogs.com/blog/682616/202001/682616-20200104124551841-1606892798.png) + +图中A是低采样率的音频信号,其效果已经将原始声波进行了扭曲,B则是完全重现原始声波的高采样率的音频信号。 + +数字音频常用的采样率如下: + +![img](https://img2018.cnblogs.com/blog/682616/202001/682616-20200104124756569-1015603325.png) + + + +##### 位深度 + +位深度决定动态范围。采样声波时,为每个采样指定最接近原始声波振幅的振幅值。较高的位深度可提供更多可能的振幅值,产生更大的动态范围、更低的噪声基准和更高的保真度。 + +![img](https://img2018.cnblogs.com/blog/682616/202001/682616-20200104124916045-1853047547.png) + +位深度越高,提供的动态范围越大。 + + + +#### PCM + +在上面的名词解析中我们应该对PCM有了一定的理解和认识,下面我们将对PCM做更多的讲解。 + + + +##### PCM音频数据存储方式 + +如果是单声道的文件,采样数据按时间的先后顺序依次存入。如果是单声道的音频文件,采样数据按时间的先后顺序依次存入(也可能采用 LRLRLR 方式存储,只是另一个声道的数据为 0)。 + +如果是双声道的话通常按照 LRLRLR 的方式存储,存储的时候还和机器的大小端有关。(关于字节序大小端的相关内容可参考《[字节序问题之大小端模式讲解](https://www.cnblogs.com/renhui/p/13600572.html)》进行了解) + +PCM的存储方式为小端模式,存储Data数据排列如下图所示: + +![img](https://img2018.cnblogs.com/blog/682616/202001/682616-20200104112313716-290297915.png) + + + +##### PCM 音频数据的参数 + +描述 PCM 音频数据的参数的时候有如下描述方式: + +``` +44100HZ 16bit stereo: 每秒钟有 44100 次采样, 采样数据用 16 位(2 字节)记录, 双声道(立体声) +22050HZ 8bit mono: 每秒钟有 22050 次采样, 采样数据用 8 位(1 字节)记录, 单声道 +48000HZ 32bit 51ch: 每秒钟有 48000 次采样, 采样数据用 32 位(4 字节浮点型)记录, 5.1 声道 +``` + +44100Hz 指的是采样率,它的意思是每秒取样 44100 次。采样率越大,存储数字音频所占的空间就越大。 + +16bit 指的是采样精度,意思是原始模拟信号被采样后,每一个采样点在计算机中用 16 位(两个字节)来表示。采样精度越高越能精细地表示模拟信号的差异。 + +Stereo 指的是声道数,也即采样时用到的麦克风的数量,麦克风越多就越能还原真实的采样环境(当然麦克风的放置位置也是有规定的)。 + + + +#### WAV + +WAV 是 Microsoft 和 IBM 为 PC 开发的一种声音文件格式,它符合 RIFF(Resource Interchange File Format)文件规范,用于保存 Windows 平台的音频信息资源,被 Windows 平台及其应用程序所广泛支持。WAVE 文件通常只是一个具有单个 “WAVE” 块的 RIFF 文件,该块由两个子块(”fmt” 子数据块和 ”data” 子数据块),它的格式如下图所示: + +![img](https://img2018.cnblogs.com/blog/682616/202001/682616-20200104131622902-1855086338.png) + +**WAV 格式定义** + +该格式的实质就是在 PCM 文件的前面加了一个文件头,每个字段的的含义如下: + + + +``` +typedef struct { + char ChunkID[4]; //内容为"RIFF" + unsigned long ChunkSize; //存储文件的字节数(不包含ChunkID和ChunkSize这8个字节) + char Format[4]; //内容为"WAVE“ +} WAVE_HEADER; + +typedef struct { + char Subchunk1ID[4]; //内容为"fmt" + unsigned long Subchunk1Size; //存储该子块的字节数(不含前面的Subchunk1ID和Subchunk1Size这8个字节) + unsigned short AudioFormat; //存储音频文件的编码格式,例如若为PCM则其存储值为1。 + unsigned short NumChannels; //声道数,单声道(Mono)值为1,双声道(Stereo)值为2,等等 + unsigned long SampleRate; //采样率,如8k,44.1k等 + unsigned long ByteRate; //每秒存储的bit数,其值 = SampleRate * NumChannels * BitsPerSample / 8 + unsigned short BlockAlign; //块对齐大小,其值 = NumChannels * BitsPerSample / 8 + unsigned short BitsPerSample; //每个采样点的bit数,一般为8,16,32等。 +} WAVE_FMT; + +typedef struct { + char Subchunk2ID[4]; //内容为“data” + unsigned long Subchunk2Size; //接下来的正式的数据部分的字节数,其值 = NumSamples * NumChannels * BitsPerSample / 8 +} WAVE_DATA; +``` + + + +**WAV 文件头解析** + +这里是一个 WAVE 文件的开头 72 字节,字节显示为十六进制数字: + +``` +52 49 46 46 | 24 08 00 00 | 57 41 56 45 +66 6d 74 20 | 10 00 00 00 | 01 00 02 00 +22 56 00 00 | 88 58 01 00 | 04 00 10 00 +64 61 74 61 | 00 08 00 00 | 00 00 00 00 +24 17 1E F3 | 3C 13 3C 14 | 16 F9 18 F9 +34 E7 23 A6 | 3C F2 24 F2 | 11 CE 1A 0D +``` + +字段解析如下图: + +![img](https://img2018.cnblogs.com/blog/682616/202001/682616-20200104131832991-1533022058.png) + + + +#### PCM & WAV 开发实践 + + + +##### PCM格式转为WAV格式(基于C语言) + + + +``` +int simplest_pcm16le_to_wave(const char *pcmpath,int channels,int sample_rate,const char *wavepath) +{ + typedef struct WAVE_HEADER{ + char fccID[4]; + unsigned long dwSize; + char fccType[4]; + }WAVE_HEADER; + typedef struct WAVE_FMT{ + char fccID[4]; + unsigned long dwSize; + unsigned short wFormatTag; + unsigned short wChannels; + unsigned long dwSamplesPerSec; + unsigned long dwAvgBytesPerSec; + unsigned short wBlockAlign; + unsigned short uiBitsPerSample; + }WAVE_FMT; + typedef struct WAVE_DATA{ + char fccID[4]; + unsigned long dwSize; + }WAVE_DATA; + if(channels==0||sample_rate==0){ + channels = 2; + sample_rate = 44100; + } + int bits = 16; + WAVE_HEADER pcmHEADER; + WAVE_FMT pcmFMT; + WAVE_DATA pcmDATA; + + unsigned short m_pcmData; + FILE *fp,*fpout; + fp=fopen(pcmpath, "rb"); + if(fp == NULL) { + printf("open pcm file error\n"); + return -1; + } + fpout=fopen(wavepath, "wb+"); + if(fpout == NULL) { + printf("create wav file error\n"); + return -1; + } + //WAVE_HEADER + memcpy(pcmHEADER.fccID,"RIFF",strlen("RIFF")); + memcpy(pcmHEADER.fccType,"WAVE",strlen("WAVE")); + fseek(fpout,sizeof(WAVE_HEADER),1); + //WAVE_FMT + pcmFMT.dwSamplesPerSec=sample_rate; + pcmFMT.dwAvgBytesPerSec=pcmFMT.dwSamplesPerSec*sizeof(m_pcmData); + pcmFMT.uiBitsPerSample=bits; + memcpy(pcmFMT.fccID,"fmt ",strlen("fmt ")); + pcmFMT.dwSize=16; + pcmFMT.wBlockAlign=2; + pcmFMT.wChannels=channels; + pcmFMT.wFormatTag=1; + + fwrite(&pcmFMT,sizeof(WAVE_FMT),1,fpout); + //WAVE_DATA; + memcpy(pcmDATA.fccID,"data",strlen("data")); + pcmDATA.dwSize=0; + fseek(fpout,sizeof(WAVE_DATA),SEEK_CUR); + fread(&m_pcmData,sizeof(unsigned short),1,fp); + while(!feof(fp)){ + pcmDATA.dwSize+=2; + fwrite(&m_pcmData,sizeof(unsigned short),1,fpout); + fread(&m_pcmData,sizeof(unsigned short),1,fp); + } + pcmHEADER.dwSize=44+pcmDATA.dwSize; + rewind(fpout); + fwrite(&pcmHEADER,sizeof(WAVE_HEADER),1,fpout); + fseek(fpout,sizeof(WAVE_FMT),SEEK_CUR); + fwrite(&pcmDATA,sizeof(WAVE_DATA),1,fpout); + + fclose(fp); + fclose(fpout); + return 0; +} +``` + + + + + +注意:函数里声明的数据类型unsigned long在有些C编译器上是64位的,这时候要改成unsigned int才可以,否则wav头有88bytes,标准的是44bytes,改完就正常了,对C还不熟悉的人小小的心得,另外,声道数和采样率也要注意,一般采样率有44100/16000/8000,要确认是哪个,声道是1还是2,这两个参数要设置好才会有正确的转换结果。 + + + +##### PCM降低某个声道的音量(基于C语言) + +一般来说 PCM 数据中的波形幅值越大,代表音量越大,对于 PCM 音频数据而言,它的幅值(即该采样点采样值的大小)代表音量的大小。 + +如果我们需要降低某个声道的音量,可以通过减小某个声道的数据的值来实现降低某个声道的音量。 + + + +``` +int pcm16le_half_volume_left( char *url ) { + FILE *fp_in = fopen( url, "rb+" ); + FILE *fp_out = fopen( "output_half_left.pcm", "wb+" ); + unsigned char *sample = ( unsigned char * )malloc(4); // 一次读取一个sample,因为是2声道,所以是4字节 + while ( !feof( fp_in ) ){ + fread( sample, 1, 4, fp_in ); + short* sample_num = ( short* )sample; // 转成左右声道两个short数据 + *sample_num = *sample_num / 2; // 左声道数据减半 + fwrite( sample, 1, 2, fp_out ); // L + fwrite( sample + 2, 1, 2, fp_out ); // R + } + free( sample ); + fclose( fp_in ); + fclose( fp_out ); + return 0; +} +``` + + + +上述代码做的事情是:在读出左声道的 2 Byte 的取样值之后,将其转成了 C 语言中的一个 short 类型的变量。将该数值除以 2 之后写回到了 PCM 文件中。 + + + +##### 分离PCM音频数据左右声道的数据 + +因为PCM音频数据是按照LRLRLR的方式来存储左右声道的音频数据的,所以我们可以通过将它们交叉的读出来的方式来分离左右声道的数据: + + + +``` +int simplest_pcm16le_split(char *url) { + FILE *fp=fopen(url,"rb+"); + FILE *fp1=fopen("output_l.pcm","wb+"); + FILE *fp2=fopen("output_r.pcm","wb+"); + unsigned char *sample=(unsigned char *)malloc(4); + while(!feof(fp)){ + fread(sample,1,4,fp); + //L + fwrite(sample,1,2,fp1); + //R + fwrite(sample+2,1,2,fp2); + } + free(sample); + fclose(fp); + fclose(fp1); + fclose(fp2); + return 0; +} +``` + + + +##### 从PCM16LE单声道音频采样数据中截取一部分数据 + +本程序中的函数可以从PCM16LE单声道数据中截取一段数据,并输出截取数据的样值。函数的代码如下所示: + + + +``` +/** + * Cut a 16LE PCM single channel file. + * @param url Location of PCM file. + * @param start_num start point + * @param dur_num how much point to cut + */ +int simplest_pcm16le_cut_singlechannel(char *url,int start_num,int dur_num){ + FILE *fp=fopen(url,"rb+"); + FILE *fp1=fopen("output_cut.pcm","wb+"); + FILE *fp_stat=fopen("output_cut.txt","wb+"); + + unsigned char *sample=(unsigned char *)malloc(2); + + int cnt=0; + while(!feof(fp)){ + fread(sample,1,2,fp); + if(cnt>start_num&&cnt<=(start_num+dur_num)){ + fwrite(sample,1,2,fp1); + + short samplenum=sample[1]; + samplenum=samplenum*256; + samplenum=samplenum+sample[0]; + + fprintf(fp_stat,"%6d,",samplenum); + if(cnt%10==0) + fprintf(fp_stat,"\n",samplenum); + } + cnt++; + } + + free(sample); + fclose(fp); + fclose(fp1); + fclose(fp_stat); + return 0; +} +``` + + + + + +##### 将PCM16LE双声道音频采样数据转换为PCM8音频采样数据 + +本程序中的函数可以通过计算的方式将PCM16LE双声道数据16bit的采样位数转换为8bit。函数的代码如下所示: + + + +``` +/** + * Convert PCM-16 data to PCM-8 data. + * @param url Location of PCM file. + */ +int simplest_pcm16le_to_pcm8(char *url){ + FILE *fp=fopen(url,"rb+"); + FILE *fp1=fopen("output_8.pcm","wb+"); + + int cnt=0; + + unsigned char *sample=(unsigned char *)malloc(4); + + while(!feof(fp)){ + + short *samplenum16=NULL; + char samplenum8=0; + unsigned char samplenum8_u=0; + fread(sample,1,4,fp); + //(-32768-32767) + samplenum16=(short *)sample; + samplenum8=(*samplenum16)>>8; + //(0-255) + samplenum8_u=samplenum8+128; + //L + fwrite(&samplenum8_u,1,1,fp1); + + samplenum16=(short *)(sample+2); + samplenum8=(*samplenum16)>>8; + samplenum8_u=samplenum8+128; + //R + fwrite(&samplenum8_u,1,1,fp1); + cnt++; + } + printf("Sample Cnt:%d\n",cnt); + + free(sample); + fclose(fp); + fclose(fp1); + return 0; +} +``` + + + +PCM16LE格式的采样数据的取值范围是-32768到32767,而PCM8格式的采样数据的取值范围是0到255。所以PCM16LE转换到PCM8需要经过两个步骤:第一步是将-32768到32767的16bit有符号数值转换为-128到127的8bit有符号数值,第二步是将-128到127的8bit有符号数值转换为0到255的8bit无符号数值。在本程序中,16bit采样数据是通过short类型变量存储的,而8bit采样数据是通过unsigned char类型存储的。 + + + +##### 将PCM16LE双声道音频采样数据的声音速度提高一倍 + +本程序中的函数可以通过抽象的方式将PCM16LE双声道数据的速度提高一倍,采用采样每个声道奇(偶)数点的样值的方式,函数的代码如下所示: + + + +``` +/** + * Re-sample to double the speed of 16LE PCM file + * @param url Location of PCM file. + */ +int simplest_pcm16le_doublespeed(char *url){ + FILE *fp=fopen(url,"rb+"); + FILE *fp1=fopen("output_doublespeed.pcm","wb+"); + + int cnt=0; + + unsigned char *sample=(unsigned char *)malloc(4); + + while(!feof(fp)){ + + fread(sample,1,4,fp); + + if(cnt%2!=0){ + //L + fwrite(sample,1,2,fp1); + //R + fwrite(sample+2,1,2,fp1); + } + cnt++; + } + printf("Sample Cnt:%d\n",cnt); + + free(sample); + fclose(fp); + fclose(fp1); + return 0; +} +``` + +## FFmpeg + +### FFmpeg命令行工具(一):查看媒体文件头信息工具ffprobe + +#### 简述 + +ffprobe是ffmpeg命令行工具中相对简单的,此命令是用来查看媒体文件格式的工具。 + +#### 命令格式 + +在命令行中输入如下格式的命令: + +``` +ffprobe [文件名] +``` + +#### 使用ffprobe查看mp3格式的文件 + +本文使用的是歌曲《社会摇》,执行的命令为: + +``` +ffprobe shy.mp3 +``` + +输出内容为: + + + +``` +Input #0, mp3, from 'shy.mp3': + Metadata: + genre : Blues + encoder : Lavf56.4.101 + comment : 163 key(Don't modify):L64FU3W4YxX3ZFTmbZ+8/UO6KmVXLfTij3uZN/wCXE4a00XHtvOwccwFlS+8ednRD4MnrdUH+aUYZFVY8bObsrabtBM2Ps/UAWPJtsmW/3RXnn6eJcNUHrPALM0003fIpQnn6MOWbdXqog6WFDLpaZJhoPMnFy9u41HxCalUwMEc+mkHNn+nSLlioJfpv4wPBwUhxfLNmOScmXPzOary2k37A/brRx7QUlMD9rkaZ + album : 社会摇 + title : 社会摇 + artist : 萧全 + track : 1 + Duration: 00:04:09.34, start: 0.025056, bitrate: 323 kb/s + Stream #0:0: Audio: mp3, 44100 Hz, stereo, s16p, 320 kb/s + Stream #0:1: Video: mjpeg, yuvj444p(pc, bt470bg/unknown/unknown), 500x500 [SAR 72:72 DAR 1:1], 90k tbr, 90k tbn, 90k tbc + Metadata: + comment : Media (e.g. label side of CD) +``` + + + +首先我们看以下这行信息: + +``` +Duration: 00:04:09.34, start: 0.025056, bitrate: 323 kb/s +``` + +这行信息表示,该视频文件的时长是4分9秒340毫秒,开始播放时间是0.025056,整个文件的比特率是256Kbit/s,然后我们看下一行信息: + +``` +Stream ####0:0: Audio: mp3, 44100 Hz, stereo, s16p, 320 kb/s +``` + +这行信息表示,第一个流是音频流,编码格式是MP3格式,采样率是44.1KHz,声道是立体声,采样表示格式是SInt16(short)的planner(平铺格式),这路流的比特率320Kbit/s。 + +#### 使用ffprobe查看mp4格式的文件 + +本文使用的是视频《泡沫》,执行的命令为: + +``` +ffprobe pm.mp4 +``` + +输出内容为: + + + +``` +Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'pm.mp4': + Metadata: + major_brand : isom + minor_version : 1 + compatible_brands: isomavc1 + creation_time : 2016-12-17T16:02:05.000000Z + album : Yinyuetai + artist : yinyuetai.com + comment : Yinyuetai-1TR1151 + date : 12/18/16 00:02:05 + Duration: 00:04:33.51, start: 0.000000, bitrate: 1104 kb/s + Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 960x540, 1008 kb/s, 25 fps, 25 tbr, 25k tbn, 50 tbc (default) + Metadata: + creation_time : 2016-12-17T16:02:05.000000Z + handler_name : 264@GPAC0.5.1-DEV-rev5472 + Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 92 kb/s (default) + Metadata: + creation_time : 2016-12-17T15:50:54.000000Z + handler_name : Sound Media Handler +``` + + + +首先我们看以下这行信息: + +``` +Duration: 00:04:33.51, start: 0.000000, bitrate: 1104 kb/s +``` + +这行信息表示,该视频文件的时长是4分33秒510毫秒,开始播放时间是0,整个文件的比特率是1104Kbit/s,然后我们看下一行信息: + +``` +Stream ####0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 960x540, 1008 kb/s, 25 fps, 25 tbr, 25k tbn, 50 tbc (default) +``` + +这行信息表示,第一个流是视频流,编码格式是H264格式(封装格式为AVC1),每一帧的数据表示为yuv420p,分辨率为960x540,这路流的比特率为1108Kbit/s,帧率为每秒钟25帧。 + +接下来我们看下一行: + +``` +Stream ####0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 92 kb/s (default) +``` + +这行信息表示第二个流是音频流,编码方式为ACC(封装格式为MP4A),并且采用的Profile是LC规格,采样率是44.1KHz,声道是立体声,这路流的比特率92Kbit/s。 + +**到此为止,我们就掌握了使用ffprobe提取媒体的头文件信息的方式,并了解了提取出来的信息的含义** + +#### ffprobe高级使用方式 + +1. 输出格式信息 + + + +``` +appledeMacBook-Pro:Desktop renhui$ ffprobe -show_format pm.mp4 +ffprobe version 3.4.2 Copyright (c) 2007-2018 the FFmpeg developers + built with Apple LLVM version 9.0.0 (clang-900.0.39.2) + configuration: --prefix=/usr/local/Cellar/ffmpeg/3.4.2 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --disable-jack --enable-gpl --enable-libmp3lame --enable-libx264 --enable-libxvid --enable-opencl --enable-videotoolbox --disable-lzma + libavutil 55. 78.100 / 55. 78.100 + libavcodec 57.107.100 / 57.107.100 + libavformat 57. 83.100 / 57. 83.100 + libavdevice 57. 10.100 / 57. 10.100 + libavfilter 6.107.100 / 6.107.100 + libavresample 3. 7. 0 / 3. 7. 0 + libswscale 4. 8.100 / 4. 8.100 + libswresample 2. 9.100 / 2. 9.100 + libpostproc 54. 7.100 / 54. 7.100 +Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'pm.mp4': + Metadata: + major_brand : isom + minor_version : 1 + compatible_brands: isomavc1 + creation_time : 2016-12-17T16:02:05.000000Z + album : Yinyuetai + artist : yinyuetai.com + comment : Yinyuetai-1TR1151 + date : 12/18/16 00:02:05 + Duration: 00:04:33.51, start: 0.000000, bitrate: 1104 kb/s + Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 960x540, 1008 kb/s, 25 fps, 25 tbr, 25k tbn, 50 tbc (default) + Metadata: + creation_time : 2016-12-17T16:02:05.000000Z + handler_name : 264@GPAC0.5.1-DEV-rev5472 + Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 92 kb/s (default) + Metadata: + creation_time : 2016-12-17T15:50:54.000000Z + handler_name : Sound Media Handler +[FORMAT] +filename=pm.mp4 +nb_streams=2 +nb_programs=0 +format_name=mov,mp4,m4a,3gp,3g2,mj2 +format_long_name=QuickTime / MOV +start_time=0.000000 +duration=273.506667 +size=37776599 +bit_rate=1104955 +probe_score=100 +TAG:major_brand=isom +TAG:minor_version=1 +TAG:compatible_brands=isomavc1 +TAG:creation_time=2016-12-17T16:02:05.000000Z +TAG:album=Yinyuetai +TAG:artist=yinyuetai.com +TAG:comment=Yinyuetai-1TR1151 +TAG:date=12/18/16 00:02:05 +[/FORMAT] +``` + + + +2. 输出每个流的具体信息(以JSON格式) + + + +``` +appledeMacBook-Pro:Desktop renhui$ ffprobe -print_format json -show_streams pm.mp4 +ffprobe version 3.4.2 Copyright (c) 2007-2018 the FFmpeg developers + built with Apple LLVM version 9.0.0 (clang-900.0.39.2) + configuration: --prefix=/usr/local/Cellar/ffmpeg/3.4.2 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --disable-jack --enable-gpl --enable-libmp3lame --enable-libx264 --enable-libxvid --enable-opencl --enable-videotoolbox --disable-lzma + libavutil 55. 78.100 / 55. 78.100 + libavcodec 57.107.100 / 57.107.100 + libavformat 57. 83.100 / 57. 83.100 + libavdevice 57. 10.100 / 57. 10.100 + libavfilter 6.107.100 / 6.107.100 + libavresample 3. 7. 0 / 3. 7. 0 + libswscale 4. 8.100 / 4. 8.100 + libswresample 2. 9.100 / 2. 9.100 + libpostproc 54. 7.100 / 54. 7.100 +{ +Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'pm.mp4': + Metadata: + major_brand : isom + minor_version : 1 + compatible_brands: isomavc1 + creation_time : 2016-12-17T16:02:05.000000Z + album : Yinyuetai + artist : yinyuetai.com + comment : Yinyuetai-1TR1151 + date : 12/18/16 00:02:05 + Duration: 00:04:33.51, start: 0.000000, bitrate: 1104 kb/s + Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 960x540, 1008 kb/s, 25 fps, 25 tbr, 25k tbn, 50 tbc (default) + Metadata: + creation_time : 2016-12-17T16:02:05.000000Z + handler_name : 264@GPAC0.5.1-DEV-rev5472 + Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 92 kb/s (default) + Metadata: + creation_time : 2016-12-17T15:50:54.000000Z + handler_name : Sound Media Handler + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "Main", + "codec_type": "video", + "codec_time_base": "1/50", + "codec_tag_string": "avc1", + "codec_tag": "0x31637661", + "width": 960, + "height": 540, + "coded_width": 960, + "coded_height": 540, + "has_b_frames": 2, + "sample_aspect_ratio": "0:1", + "display_aspect_ratio": "0:1", + "pix_fmt": "yuv420p", + "level": 31, + "chroma_location": "left", + "refs": 1, + "is_avc": "true", + "nal_length_size": "4", + "r_frame_rate": "25/1", + "avg_frame_rate": "25/1", + "time_base": "1/25000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 6835000, + "duration": "273.400000", + "bit_rate": "1008649", + "bits_per_raw_sample": "8", + "nb_frames": "6835", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "creation_time": "2016-12-17T16:02:05.000000Z", + "language": "und", + "handler_name": "264@GPAC0.5.1-DEV-rev5472" + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "profile": "LC", + "codec_type": "audio", + "codec_time_base": "1/44100", + "codec_tag_string": "mp4a", + "codec_tag": "0x6134706d", + "sample_fmt": "fltp", + "sample_rate": "44100", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/44100", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 12061696, + "duration": "273.507846", + "bit_rate": "92649", + "max_bit_rate": "136240", + "nb_frames": "11779", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "creation_time": "2016-12-17T15:50:54.000000Z", + "language": "und", + "handler_name": "Sound Media Handler" + } + } + ] +} +``` + +3. 显示帧信息 + +``` +ffprobe -show_frames pm.mp4 +``` + +4. 查看包信息 + +``` +ffprobe -show_packets pm.mp4 +``` + +### FFmpeg命令行工具(二):播放媒体文件的工具ffplay + +#### 简述 + +ffplay是以FFmpeg框架为基础,外加渲染音视频的库libSDL构建的媒体文件播放器。 + +在使用ffplay之前必须要安装到系统中,MAC的安装教程为:http://www.cnblogs.com/renhui/p/8458150.html + +#### 命令格式 + +在安装了在命令行中输入如下格式的命令: + +``` +ffplay [选项] ['输入文件'] +``` + +##### 主要选项 + +```xml +'-x width' 强制以 "width" 宽度显示 +'-y height' 强制以 "height" 高度显示 +'-an' 禁止音频 +'-vn' 禁止视频 +'-ss pos' 跳转到指定的位置(秒) +'-t duration' 播放 "duration" 秒音/视频 +'-bytes' 按字节跳转 +'-nodisp' 禁止图像显示(只输出音频) +'-f fmt' 强制使用 "fmt" 格式 +'-window_title title' 设置窗口标题(默认为输入文件名) +'-loop number' 循环播放 "number" 次(0将一直循环) +'-showmode mode' 设置显示模式 +可选的 mode : +'0, video' 显示视频 +'1, waves' 显示音频波形 +'2, rdft' 显示音频频带 +默认值为 'video',你可以在播放进行时,按 "w" 键在这几种模式间切换 +'-i input_file' 指定输入文件 +``` + +##### 一些高级选项 + +```xml +'-sync type' 设置主时钟为音频、视频、或者外部。默认为音频。主时钟用来进行音视频同步 +'-threads count' 设置线程个数 +'-autoexit' 播放完成后自动退出 +'-exitonkeydown' 任意键按下时退出 +'-exitonmousedown' 任意鼠标按键按下时退出 +'-acodec codec_name' 强制指定音频解码器为 "codec_name" +'-vcodec codec_name' 强制指定视频解码器为 "codec_name" +'-scodec codec_name' 强制指定字幕解码器为 "codec_name" +``` + +##### 一些快捷键 + +```xml +'q, ESC' 退出 +'f' 全屏 +'p, SPC' 暂停 +'w' 切换显示模式(视频/音频波形/音频频带) +'s' 步进到下一帧 +'left/right' 快退/快进 10 秒 +'down/up' 快退/快进 1 分钟 +'page down/page up' 跳转到前一章/下一章(如果没有章节,快退/快进 10 分钟) +'mouse click' 跳转到鼠标点击的位置(根据鼠标在显示窗口点击的位置计算百分比) +``` + +#### ffplay 播放音频 + +播放音频文件的命令: + +``` +ffplay shy.mp3 +``` + +这时候就会弹出来一个窗口,一边播放MP3文件,一边将播放音频的图画到该窗口上。针对该窗口的操作如下: + +1. 点击该窗口的任意一个位置,ffplay会按照点击的位置计算出时间的进度,然后seek到计算出来的时间点继续播放。 +2. 按下键盘的左键默认快退10s,右键默认快进10s,上键默认快进1min,下键默认快退1min。 +3. 按ESC就退出播放进程,按W会绘制音频的波形图。 + 相关效果图片如下: + ![img](https://images2018.cnblogs.com/blog/682616/201806/682616-20180622162548286-1645101855.png) + ![img](https://images2018.cnblogs.com/blog/682616/201806/682616-20180622162501722-1576928280.png) + +#### ffplay 播放视频 + +播放视频文件的命令: + +``` +ffplay pm.mp4 +``` + +这时候,就会在新弹出的窗口上播放该视频了。 + +1. 如果想要同时播放多个文件,只需在多个命令行下同时执行ffplay就可以了。 +2. 如果按s键就可以进入frame-step模式,即按s键一次就会播放下一帧图像。 + +#### ffplay 高级使用方式 + + + +##### 循环播放 + +``` +ffplay pm.mp4 -loop 10 +``` + +上述命令代表播放视频结束之后会从头再次播放,共循环播放10次。 + + + +##### 播放 pm.mp4 ,播放完成后自动退出 + +```xml +ffplay -autoexit pm.mp4 +``` + + + +##### 以 320 x 240 的大小播放 test.mp4 + +```xml +ffplay -x 320 -y 240 pm.mp4 +``` + + + +##### 将窗口标题设置为 "myplayer",循环播放 2 次 + +```xml +ffplay -window_title myplayer -loop 2 pm.mp4 +``` + + + +##### 播放 双通道 32K 的 PCM 音频数据 + +```xml +ffplay -f s16le -ar 32000 -ac 2 test.pcm +``` + +#### ffplay音画同步 + +ffplay也是一个视频播放器,所以不得不提出来的一个问题是:音画同步。ffplay的音画同步的实现方式其实有三种,分别是:以音频为主时间轴作为同步源,以视频为主时间轴作为同步源,以外部时钟为主时间轴作为同步源。 + +下面就以音频为主时间轴来作为同步源来作为案例进行讲解,而且ffplay默认也是以音频为基准进行对齐的,那么以音频作为对齐基准是如何实现的呢? + +首先需要说明的是,播放器接收到的视频帧或者音频帧,内部都是会有时间戳(PTS时钟)来标识它实际应该在什么时刻展示,实际的对齐策略如下:比较视频当前的播放时间和音频当前的播放时间,如果视频播放过快,则通过加大延迟或者重复播放来降低视频播放速度,如果视频播放满了,则通过减小延迟或者丢帧来追赶音频播放的时间点。关键就在于音视频时间的比较和延迟的计算,当前在比较的过程中会设置一个阈值,如果超过预设的阈值就应该作出调整(丢帧或者重复渲染),这就是整个对齐策略。 + +在使用ffplay的时候,我们可以明确的指定使用那种对齐方式,比如: + +``` +ffplay pm.mp4 -sync audio +``` + +上面这个命令显式的指定了使用以音频为基准进行音视频同步的方式播放视频文件,当然这也是ffplay的默认播放设置。 + +``` +ffplay pm.mp4 -sync video +``` + +上面这个命令显式的指定了使用以视频为基准进行音视频同步的方式播放视频文件。 + +``` +ffplay pm.mp4 -sync ext +``` + +上面这个命令显式的指定了使用外部时钟为基准进行音视频同步的方式播放视频文件。 + +大家可以分别使用这三种方式进行播放,尝试听一听,做一些快进或者seek的操作,看看不同的对齐策略对最终的播放会产生什么样的影响。 + +### FFmpeg命令行工具(三):媒体文件转换工具ffmpeg + +#### 简述 + +ffmpeg是一个非常强大的工具,它可以转换任何格式的媒体文件,并且还可以用自己的AudioFilter以及VideoFilter进行处理和编辑。有了它,我们就可以对媒体文件做很多我们想做的事情了。 + +#### 命令行参数 + +##### 通用参数 + +- -f fmt : 指定格式 +- -i filename:指定输入文件名 +- -y:覆盖已有文件 +- -t duration:指定时长 +- -fs limit_size:设置文件大小的上限 +- -ss time_off: 从指定的时间开始 +- -re:代表按照时间戳读取或发送数据,尤其在作为推流工具的时候一定要加上该参数,否则ffpmeg会按照最高速率向流媒体不停的发送数据。 +- -map:指定输出文件的流映射关系。例如:“-map 1:0 -map 1:1”要求按照第二个输入的文件的第一个流和第二个流写入输出文件。如果没有设置此项,则ffpmeg采用默认的映射关系。 + +##### 视频参数 + +- -b:指定比特率(bit/s),ffmpeg默认采用的是VBR的,若指定的该参数,则使用平均比特率。 +- -bitexact:使用标准比特率。 +- -vb:指定视频比特率(bit/s) +- -r rate:帧速率(fps) +- -s size:指定分辨率(320x240) +- -aspect aspect:设置视频长宽比(4:3、16:9或1.33333、1.77777) +- -croptop size:设置顶部切除尺寸(in pixels) +- -cropleft size:设置左切除尺寸(in pixels) +- -cropbottom size:设置地步切除尺寸(in pixels) +- -cropright size:设置右切除尺寸(in pixels) +- -padtop size:设置顶部补齐尺寸(in pixels) +- -padleft size:设置左补齐尺寸(in pixels) +- -padbottom size:设置地步补齐尺寸(in pixels) +- -padright size:设置右补齐尺寸(in pixels) +- -padcolor color:设置补齐颜色 +- -vn:取消视频的输出 +- -vcodec codec:强制使用codec编码方式 + +##### 音频参数 + +- -ab:设置比特率(bit/s),对于MP3的格式,想要听到较高品质的声音,建议设置160Kbit/s(单声道80Kbit/s)以上。 +- -aq quality:设置音频质量 +- -ar ratre:设置音频采样率(Hz) +- -ac channels:设置声道数,1就是单声道,2就是立体声 +- -an:取消音频输出 +- -acodec codec:强制使用codec编码方式 +- -vol volume:设置录制音量大小 + +以上就是在日常开发中经常用到的音视频参数及通用参数。下面会针对常见的开发场景进行实践和说明。 + +#### 实践学习 + +##### 列出ffmpeg支持的所有格式 + +相关命令: + +``` +ffmpeg -formats +``` + +输出结果: + + + +``` +File formats: + D. = Demuxing supported + .E = Muxing supported + -- + D 3dostr 3DO STR + E 3g2 3GP2 (3GPP2 file format) + E 3gp 3GP (3GPP file format) + D 4xm 4X Technologies + E a64 a64 - video for Commodore 64 + D aa Audible AA format files + D aac raw ADTS AAC (Advanced Audio Coding) + DE ac3 raw AC-3 省略...... + D xbin eXtended BINary text (XBIN) + D xmv Microsoft XMV + D xpm_pipe piped xpm sequence + D xvag Sony PS3 XVAG + D xwma Microsoft xWMA + D yop Psygnosis YOP + DE yuv4mpegpipe YUV4MPEG pipe +``` + + + +##### 剪切一段媒体文件,可以是音频或者视频文件 + +相关命令: + +``` +ffmpeg -i pm.mp4 -ss 00:00:50.0 -codec copy -t 20 output.mp4 +``` + +命令说明: + +表示将文件pm.mp4从第50s开始剪切20s的时间,输出到output.mp4中,其中-ss指定偏移时间(time Offset),-t指定的时长(duration)。 + +但是直接这样执行命令,固然我们能截取出来音视频的文件,但是当我们播放的时候,我们会发现虽然ffmepg剪切视频,很方便,但是也有很大缺陷: + +(1). 剪切时间点不精确 +(2). 有时剪切的视频开头有黑屏 + +造成这些问题的原因是ffmpeg无法seek到非关键帧上。 + +命令层面定位的话就是如果把-ss, -t参数放在-i参数之后,是对输出文件执行的seek操作 +输入文件会逐帧解码,直到-ss设置的时间点为止,这么操作会很慢,虽然时间点是准确的,但是很容易出现黑屏问题。 + +所以:我们优化了一下上面的那个命令,让视频的剪切更加精确: + +``` +ffmpeg -ss 10 -t 15 -accurate_seek -i pm.mp4 -codec copy output.mp4 +``` + +注意:accurate_seek必须放在-i参数之前。 + +但是,可能又会有人发现,还是存在剪切不准确的现象,那是因为,上述命令只是进行了数据的转封装,会受到关键帧的影响,所以如果需要特别准确的剪切,只能使用ffmpeg进行重新编解码的操作了,命令行如下: + +``` +ffmpeg -i input.mp4 -ss 00:00:03.123 -t 10 -c:v libx264 -c:a aac out.mp4 +``` + +此命令行相对上面的转封装的剪切来说,速度明显变慢,是因为对视频数据重新编解码了,但是精度相对转封装来说是大大提高了。 + +##### 提取视频文件中的音频数据,并保存为文件 + +相关命令: + +``` +ffmpeg -i pm.mp4 -vn -acodec copy output.m4a +``` + +命令说明: + +将文件pm.mp4的视频流禁用掉(参数为:-vn,如果禁用音频流参数为-an,禁用字母流参数为-sn )。 + +然后将pm.mp4中的音频流的数据封装到output.m4a文件中,音频流的编码格式不变。 + +##### 将视频中的音频静音,只保留视频 + +相关命令: + +``` +ffmpeg -i pm.mp4 -an -vcodec copy output.mp4 +``` + +命令说明: + +将文件pm.mp4的音频流禁用掉(参数为:-an )。 + +然后将pm.mp4中的视频流的数据封装到output.mp4文件中,视频流的编码格式不变。 + +##### 从mp4文件中抽取视频流导出为裸H264数据: + +相关命令: + +``` +ffmpeg -i pm.mp4 -an -vcodec copy -bsf:v h264_mp4toannexb output.h264 +``` + +命令说明: + +在指令中,我们舍弃了音频数据(-an),视频数据使用mp4toannexb这个bitstreasm filter来转换为原始的H264数据。(注:同一编码也会有不同的封装格式)。 + +验证播放: + +可以使用ffplay命令进行尝试播放,如果能播放成功,则说明生效。 + +##### 将视频推送到流媒体服务器上: + +``` +ffmpeg -re -i pm.mp4 -acodec copy -vcodec copy -f flv rtmp://127.0.0.1/rh/mylive +``` + +命令说明: + +将mp4文件的音视频数据的编码格式不变,按照rtmp的方式,将视频推送到流媒体服务器上。 + +##### 将流媒体服务器上的流dump到本地: + +``` +ffmpeg -i rtmp://127.0.0.1/rh/mylive -acodec copy -vcodec copy -f flv test.flv +``` + +命令说明: + +将流媒体服务器的数据,不进行转码,通过转封装的方式保存到本地。 + +##### 给视频添加水印 + +``` +ffmpeg -i pm.mp4 -i xxx.png -filter_complex "overlay=5:5" out.mp4 +``` + +命令说明: + +使用ffmpeg滤镜功能,将对mp4添加水印。 + +##### 倒放音视频 + + + +``` +// 1.视频倒放,无音频 +ffmpeg.exe -i inputfile.mp4 -filter_complex [0:v]reverse[v] -map [v] -preset superfast reversed.mp4 +// 2.视频倒放,音频不变 +ffmpeg.exe -i inputfile.mp4 -vf reverse reversed.mp4 +// 3.音频倒放,视频不变 +ffmpeg.exe -i inputfile.mp4 -map 0 -c:v copy -af "areverse" reversed_audio.mp4 +// 4.音视频同时倒放 +ffmpeg.exe -i inputfile.mp4 -vf reverse -af areverse -preset superfast reversed.mp4 +``` + + + +##### 将几个MP4视频文件合并为1个视频. + +实现思路: + +1.先将MP4文件转化为同样编码形式的ts流(ts流支持concate) + +2.第二步,连接(concate)ts流 + +3.最后,把连接好的ts流转化为MP4. + + + +``` +// 转换为ts流ffmpeg -i 0.mp4 -vcodec copy -acodec copy -vbsf h264_mp4toannexb 0.ts +ffmpeg -i 1.mp4 -vcodec copy -acodec copy -vbsf h264_mp4toannexb 1.ts +ffmpeg -i 2.mp4 -vcodec copy -acodec copy -vbsf h264_mp4toannexb 2.ts +ffmpeg -i 3.mp4 -vcodec copy -acodec copy -vbsf h264_mp4toannexb 3.ts +ffmpeg -i 4.mp4 -vcodec copy -acodec copy -vbsf h264_mp4toannexb 4.ts +ffmpeg -i 5.mp4 -vcodec copy -acodec copy -vbsf h264_mp4toannexb 5.ts +// 合并ts流为mp4 +ffmpeg -i "concat:0.ts|1.ts|2.ts|3.ts|4.ts|5.ts" -acodec copy -vcodec copy -absf aac_adtstoasc FileName.mp4 +``` + +### FFmpeg命令行工具(四):FFmpeg 调整音视频播放速度 + +FFmpeg对音频、视频播放速度的调整的原理不一样。下面简单的说一下各自的原理及实现方式: + + + +#### 调整视频速率 + +调整视频速率的原理为:**修改视频的pts,dts** + +实现: + +``` +ffmpeg -i input.mkv -an -filter:v "setpts=0.5*PTS" output.mkv +``` + +注意:视频调整的速度倍率范围为:[0.25, 4] + +如果只调整视频的话最好把音频禁掉。 + +对视频进行加速时,如果不想丢帧,可以用-r 参数指定输出视频FPS,方法如下: + +``` +ffmpeg -i input.mkv -an -r 60 -filter:v "setpts=2.0*PTS" output.mkv +``` + + + +#### 调整音频速率 + +调整视频速率的原理为:简单的方法是调整音频采样率,但是这种方法会改变音色, 一般采用通过对**原音进行重采样,差值**等方法。 + +``` +ffmpeg -i input.mkv -filter:a "atempo=2.0" -vn output.mkv +``` + +注意:倍率调整范围为[0.5, 2.0] + +如果需要调整4倍可采用以下方法: + +``` +ffmpeg -i input.mkv -filter:a "atempo=2.0,atempo=2.0" -vn output.mkv +``` + +如果需要同时调整,可以采用如下的方式来实现:* +* + +``` +ffmpeg -i input.mkv -filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]" -map "[v]" -map "[a]" output.mkv +``` + + + +#### 参考文献 + +[http://trac.ffmpeg.org/wiki/How%20to%20speed%20up%20/%20slow%20down%20a%20video](http://trac.ffmpeg.org/wiki/How to speed up / slow down a video) + +### FFmpeg(一):FFmpeg 简介 + +#### FFmpeg 介绍 + +FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库。 + +#### FFmpeg 组成 + +- libavformat:用于各种音视频[封装格式](https://baike.baidu.com/item/封装格式)的生成和解析,包括获取解码所需信息以生成解码上下文结构和读取音视频帧等功能; +- libavcodec:用于各种类型声音/图像编解码; +- libavutil:包含一些公共的工具函数; +- libswscale:用于视频场景比例缩放、色彩映射转换; +- libpostproc:用于后期效果处理; +- ffmpeg:该项目提供的一个工具,可用于格式转换、解码或[电视卡](https://baike.baidu.com/item/电视卡)即时编码等; +- ffsever:一个 HTTP 多媒体即时广播串流服务器; +- ffplay:是一个简单的播放器,使用ffmpeg 库解析和解码,通过SDL显示; + +#### FFmpeg包含类库说明 + +##### 类库说明 + +- libavformat - 用于各种音视频封装格式的生成和解析,包括获取解码所需信息、读取音视频数据等功能。各种流媒体协议代码(如rtmpproto.c等)以及音视频格式的(解)复用代码(如flvdec.c、flvenc.c等)都位于该目录下。 +- libavcodec - 音视频各种格式的编解码。各种格式的编解码代码(如aacenc.c、aacdec.c等)都位于该目录下。 +- libavutil - 包含一些公共的工具函数的使用库,包括算数运算,字符操作等。 +- libswscale - 提供原始视频的比例缩放、色彩映射转换、图像颜色空间或格式转换的功能。 +- libswresample - 提供音频重采样,采样格式转换和混合等功能。 +- libavfilter - 各种音视频滤波器。 +- libpostproc - 用于后期效果处理,如图像的去块效应等。 +- libavdevice - 用于硬件的音视频采集、加速和显示。 + +如果您之前没有阅读FFmpeg代码的经验,建议优先阅读libavformat、libavcodec以及libavutil下面的代码,它们提供了音视频开发的最基本功能,应用范围也是最广的。 + +##### 常用结构 + +FFmpeg里面最常用的数据结构,按功能可大致分为以下几类(以下代码行数,以branch: origin/release/3.4为准): + +###### 封装格式 + +- AVFormatContext - 描述了媒体文件的构成及基本信息,是统领全局的基本结构体,贯穿程序始终,很多函数都要用它作为参数; +- AVInputFormat - 解复用器对象,每种作为输入的封装格式(例如FLV、MP4、TS等)对应一个该结构体,如libavformat/flvdec.c的ff_flv_demuxer; +- AVOutputFormat - 复用器对象,每种作为输出的封装格式(例如FLV, MP4、TS等)对应一个该结构体,如libavformat/flvenc.c的ff_flv_muxer; +- AVStream - 用于描述一个视频/音频流的相关数据信息。 + +###### 编解码 + +- AVCodecContext - 描述编解码器上下文的数据结构,包含了众多编解码器需要的参数信息; +- AVCodec - 编解码器对象,每种编解码格式(例如H.264、AAC等)对应一个该结构体,如libavcodec/aacdec.c的ff_aac_decoder。每个AVCodecContext中含有一个AVCodec; +- AVCodecParameters - 编解码参数,每个AVStream中都含有一个AVCodecParameters,用来存放当前流的编解码参数。 + +###### 网络协议 + +- AVIOContext - 管理输入输出数据的结构体; +- URLProtocol - 描述了音视频数据传输所使用的协议,每种传输协议(例如HTTP、RTMP)等,都会对应一个URLProtocol结构,如libavformat/http.c中的ff_http_protocol; +- URLContext - 封装了协议对象及协议操作对象。 + +###### 数据存放 + +- AVPacket - 存放编码后、解码前的压缩数据,即ES数据; +- AVFrame - 存放编码前、解码后的原始数据,如YUV格式的视频数据或PCM格式的音频数据等; + +### FFmpeg(二):Mac下安装FFmpeg + +#### 安装ffmpeg + +分为两种安装方式: + +##### 命令行安装 + +``` +brew install ffmpeg +``` + +##### 下载压缩包安装 + +去 http://evermeet.cx/ffmpeg/ 下载7z压缩包,解压缩后,将ffmpeg文件拷贝到一个地方,然后在bash_profile里面配置好环境变量 + +#### 安装ffplay + +分为两种安装方式: + +##### 命令行安装 + +执行下面的命令就可以进行安装操作 + +``` +brew install ffmpeg --with-ffplay +``` + +> - 注:目前使用此安装方式安装后,执行ffplay会出现command not found的问题,可能是因为SDL的配置问题导致的。 + +##### 下载压缩包安装 + +去 http://evermeet.cx/ffmpeg/ 下载7z压缩包,解压缩后,将ffplay文件拷贝到一个地方,然后在bash_profile里面配置好环境变量 + +#### 附言 + +在上面我们接触到了命令行安装ffmpeg的方法,除了安装选项 --with-ffplay外还有更多的选项如下: + +```xml +–with-fdk-aac (Enable the Fraunhofer FDK AAC library) +–with-ffplay (Enable FFplay media player) +–with-freetype (Build with freetype support) +–with-frei0r (Build with frei0r support) +–with-libass (Enable ASS/SSA subtitle format) +–with-libcaca (Build with libcaca support) +–with-libvo-aacenc (Enable VisualOn AAC encoder) +–with-libvorbis (Build with libvorbis support) +–with-libvpx (Build with libvpx support) +–with-opencore-amr (Build with opencore-amr support) +–with-openjpeg (Enable JPEG 2000 image format) +–with-openssl (Enable SSL support) +–with-opus (Build with opus support) +–with-rtmpdump (Enable RTMP protocol) +–with-schroedinger (Enable Dirac video format) +–with-speex (Build with speex support) +–with-theora (Build with theora support) +–with-tools (Enable additional FFmpeg tools) +–without-faac (Build without faac support) +–without-lame (Disable MP3 encoder) +–without-x264 (Disable H.264 encoder) +–without-xvid (Disable Xvid MPEG-4 video encoder) +–devel (install development version 2.1.1) +–HEAD (install HEAD version) +``` + +### FFmpeg(三):将 FFmpeg 移植到 Android平台 + +首先需要去FFmpeg的官网http://www.ffmpeg.org/去下载FFmpeg的源码,目前的版本号为FFmpeg3.3(Hilbert)。 + +下载的文件为压缩包,解压后得到ffmpeg-3.3目录。 + +**修改ffmpeg-3.3的configure文件:** + + + +``` +# 原来的配置内容: +SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)' + +LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"' + +SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)' + +SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR)$(SLIBNAME)' + +#替换后的内容: + +SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)' + +LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"' + +SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)' + +SLIB_INSTALL_LINKS='$(SLIBNAME)' +``` + + + +原因:如果不修改配置,直接进行编译出来的so文件类似libavcodec.so.55.39.101,文件的版本号位于so之后,这样在Android上无法加载,所以需要修改! + +**编写build_android.sh脚本文件:** + +在编译FFmpeg之前需要进行配置,设置相应的环境变量等。所有的配置选项都在ffmpeg-3.3/configure这个脚本文件中,执行如下命令可查看所有的配置选项: + +$ ./configure –help + +下面将配置项和环境变量设置写成一个sh脚本文件来运行以便编译出Android平台需要的so文件出来。 + +build_android.sh的内容如下: + + + +``` +#!/bin/bash +NDK=/Users/renhui/framework/android-ndk-r14b +SYSROOT=$NDK/platforms/android-9/arch-arm/ +TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64 + +function build_one +{ +./configure \ + --prefix=$PREFIX \ + --enable-shared \ + --disable-static \ + --disable-doc \--enable-cross-compile \ + --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \ + --target-os=linux \ + --arch=arm \ + --sysroot=$SYSROOT \ + --extra-cflags="-Os -fpic $ADDI_CFLAGS" \ + --extra-ldflags="$ADDI_LDFLAGS" \ + $ADDITIONAL_CONFIGURE_FLAG +} +CPU=arm +PREFIX=$(pwd)/android/$CPU +ADDI_CFLAGS="-marm" +build_one +``` + + + +需要确定的是NDK,SYSROOT和TOOLCHAIN是否是本地的环境,并确定cross-prefix指向的路径存在。 + +保存脚本文件后,将脚本的权限提升: + +``` +chmod 777 build_android.sh +``` + +然后执行脚本,该脚本会完成对ffmpeg的配置,并生成config.h等配置文件,后面的编译会用到。如果未经过配置直接进行编译会提示无法找到config.h文件等错误。 + +然后执行下面两个命令: + +``` +$make +$make install +``` + +至此,会在ffmpeg-3.3目录下生成一个android目录,其/android/arm/lib目录下的so库文件就是能够在Android上运行的so库。 + + + +创建Demo工程,测试上面生成的so文件能否正常使用: + +1. 创建一个新的Android工程 + +2. 在工程根目录下创建jni文件夹 + +3. 在jni下创建prebuilt目录,然后:将上面编译成功的so文件放入到该目录下 + +4. 创建包含native方法的类,先在src下创建cn.renhui包,然后创建FFmpegNative.java类文件。主要包括加载so库文件和一个native测试方法两部分,其内容如下: + + + + ``` + package cn.renhui; + + public class FFmpegNative { + + static { + System.loadLibrary("avutil-55"); + System.loadLibrary("avcodec-57"); + System.loadLibrary("swresample-2"); + System.loadLibrary("avformat-57"); + System.loadLibrary("swscale-4"); + System.loadLibrary("avfilter-6"); + System.loadLibrary("avdevice-57"); + System.loadLibrary("ffmpeg_codec"); + } + + public native int avcodec_find_decoder(int codecID); + } + ``` + + + +1. 用javah创建.头文件: classes目录,执行:javah-jni cn.renhui.FFmpegNative,会在当前目录产生cn_renhui_FFmpegNative.h的C头文件; + +6. 根据头文件名,建立相同名字c文件cn_renhui_FFmpegNative.c,在这个源文件中实现头文件中定义的方法,代码如下: + + + + ``` + #include "cn_renhui_FFmpegNative.h" + + #ifdef __cplusplus + extern "C" { + #endif + + JNIEXPORT jint JNICALL Java_cn_renhui_FFmpegNative_avcodec_1find_1decoder + (JNIEnv *env, jobject obj, jint codecID) + { + AVCodec *codec = NULL; + + /* register all formats and codecs */ + av_register_all(); + + codec = avcodec_find_decoder(codecID); + + if (codec != NULL) + { + return 0; + } + else + { + return -1; + } + } + + #ifdef __cplusplus + } + #endif + ``` + + + +1. 编写Android.mk,内容如下: + + + + ``` + LOCAL_PATH := $(call my-dir) + + include $(CLEAR_VARS) + LOCAL_MODULE := avcodec-57-prebuilt + LOCAL_SRC_FILES := prebuilt/libavcodec-57.so + include $(PREBUILT_SHARED_LIBRARY) + + include $(CLEAR_VARS) + LOCAL_MODULE := avdevice-57-prebuilt + LOCAL_SRC_FILES := prebuilt/libavdevice-57.so + include $(PREBUILT_SHARED_LIBRARY) + + include $(CLEAR_VARS) + LOCAL_MODULE := avfilter-6-prebuilt + LOCAL_SRC_FILES := prebuilt/libavfilter-6.so + include $(PREBUILT_SHARED_LIBRARY) + + include $(CLEAR_VARS) + LOCAL_MODULE := avformat-57-prebuilt + LOCAL_SRC_FILES := prebuilt/libavformat-57.so + include $(PREBUILT_SHARED_LIBRARY) + + include $(CLEAR_VARS) + LOCAL_MODULE := avutil-55-prebuilt + LOCAL_SRC_FILES := prebuilt/libavutil-55.so + include $(PREBUILT_SHARED_LIBRARY) + + include $(CLEAR_VARS) + LOCAL_MODULE := avswresample-2-prebuilt + LOCAL_SRC_FILES := prebuilt/libswresample-2.so + include $(PREBUILT_SHARED_LIBRARY) + + include $(CLEAR_VARS) + LOCAL_MODULE := swscale-4-prebuilt + LOCAL_SRC_FILES := prebuilt/libswscale-4.so + include $(PREBUILT_SHARED_LIBRARY) + + include $(CLEAR_VARS) + + LOCAL_MODULE := ffmpeg_codec + LOCAL_SRC_FILES := cn_dennishucd_FFmpegNative.c + + LOCAL_LDLIBS := -llog -ljnigraphics -lz -landroid + LOCAL_SHARED_LIBRARIES := avcodec-57-prebuilt avdevice-57-prebuilt avfilter-6-prebuilt avformat-57-prebuilt avutil-55-prebuilt + + include $(BUILD_SHARED_LIBRARY) + ``` + + + +1. 编译so文件,执行ndk-build + +2. 新建一个Activity,进行测试,测试核心代码: + + + + ``` + FFmpegNative ffmpeg = new FFmpegNative(); + + int codecID = 28; + + int res = ffmpeg.avcodec_find_decoder(codecID); + + if (res == 0) { + tv.setText("Success!"); + } else { + tv.setText("Failed!"); + } + ``` + + + +​ 28是H264的编解码ID,可以在ffmpeg的源代码中找到,它是枚举类型定义的。在C语言中,可以换算为整型值。这里测试能否找到H264编解码,如果能找到,说明调用ffmpeg的库函数是成功的,这也表明我们编译的so文件是基本可用。 + +### FFmpeg(四):FFmpeg API 介绍与通用 API 分析 + +#### FFmpeg 编解码流程 + +FFmpeg编解码流程图如下,此图包含了整体的解封装、编解码的基本流程。 + +![img](https://img2020.cnblogs.com/blog/682616/202104/682616-20210402124122366-1492811886.png) + +下面我们要介绍的术语及相关API都是围绕这个流程图展开的。 + + + +#### FFmpeg 相关术语 + +**1. 容器/文件(Container/File)**:即特定格式的多媒体文件,比如MP4,flv,mov等。 + +**2. 媒体流(Stream)**:表示在时间轴上的一段连续的数据,比如一段声音数据、一段视频数据或者一段字母数据,可以是压缩的,也可以是非压缩的,压缩的数据需要关联特定的编解码器。 + +**3. 数据帧/数据包(Frame/Packet)**:通常一个媒体流是由大量的数据帧组成的,对于压缩数据,帧对应着编解码器的最小处理单元,分属于不同媒体流的数据帧交错存储与容器之中。 + +**4. 编解码器**:编解码器是以帧为单位实现压缩数据和原始数据之间的相互转换的。 + +前面介绍的术语,就是FFmpeg中抽象出来的概念。其中: + +**1. AVFormatContext**:就是对容器或者媒体文件层次的抽象。 + +**2. AVStream**:在文件中(容器里面)包含了多路流(音频流、视频流、字幕流),AVStream 就是对流的抽象。 + +**3. AVCodecContext 与 AVCodec**:在每一路流中都会描述这路流的编码格式,对编解码器格式以及编解码器的抽象就是AVCodecContext 与 AVCodec。 + +**4. AVPacket 与 AVFrame**:对于编码器或者解码器的输入输出部分,也就是压缩数据以及原始数据的抽象就是AVPacket与AVFrame。 + +**5. AVFilte**r:除了编解码之外,对音视频的处理肯定是针对于原始数据的处理,也就是针对AVFrame的处理,使用的就是AVFilter。 + + + +#### FFmpeg 通用 API 分析 + + + +##### av_register_all 分析 + +在最开始编译FFmpeg的时候,我们做了一个configure的配置,其中开启或者关闭了很多选项。configure的配置会生成两个文件:config.mk和config.h。 + +> config.mk:就是makefile文件需要包含进去的子模块,会作用在编译阶段,帮助开发者编译出正确的库。 +> +> config.h:作用在运行阶段,主要是确定需要注册那些容器及编解码格式到FFmpeg框架中。 + +调用 av_register_all 就可以注册config.h里面开发的编解码器,然后会注册所有的Muxer和Demuxer(封装格式),最后注册所有的Protocol(协议)。 + +这样在configure时开启或者关闭的选项就作用到了运行时,该函数的源码分析设计的源码文件包括:url.c、allformats.c、mux.c、format.c 等文件。已经将这几个源码文件单独提出来了,并放在百度网盘上了,地址:https://pan.baidu.com/s/1p8-ish6oeRTaUs84juQtHg。 + + + +##### av_find_codec 分析 + +这个方法包含了两部分的内容:一部分是寻找解码器,一部分是寻找编码器。其实在av_register_all的函数执行时,就已经把编码器和解码器都存放到一个链表中了。这里寻找编解码器就是从上一步构造的链表中遍历,通过Codec的ID或者name进行条件匹配,最终返回对于的Codec。 + + + +##### avcodec_open2 分析 + +该函数是打开编解码器(Codec)的函数,无论是编码过程还是解码过程,都会用到这个函数。该函数的输入参数有三个:第一个是AVCodecContext,解码过程由FFmpeg引擎填充,编码过程由开发者自己构造,如果想传入私有参数,则为它的priv_data设置参数;第二个参数是上一步通过av_find_codec寻找出来的编解码器(Codec);第三个参数一般传NULL。 + + + +##### avcodec_close 分析 + +如果理解了avcodec_open,那么对应的close就是一个逆过程,找到对应的实现文件中的close函数指针所只指向的函数,然后该函数会调用对应第三方库的API来关闭掉对应的编码库。 + + + +#### 总结 + +本文主要是讲述了FFmpeg的相关术语,并讲解了一下通用的API的分析,不难看出其实FFmpeg所做的事情就是透明化所有的编解码库,用自己的封装来为开发者提供统一的接口。开发者使用不同的编码库时,只需要指明要用哪一个即可,这也充分体现了面向对象编程中的封装特性 + +### FFmpeg(五):FFmpeg 编解码 API 分析 + +在上一篇文章 [FFmpeg(四):FFmpeg API 介绍与通用 API 分析](https://www.cnblogs.com/renhui/p/9293057.html) 中,我们简单的讲解了一下FFmpeg 的API基本概念,并分析了一下通用API,本文我们将分析 FFmpeg 在编解码时使用的API。 + + + +#### FFmpeg 解码 API 分析 + +##### avformat_open_input 分析 + +函数 avformat_open_input 会根据所提供的文件路径判断文件的格式,其实就是通过这一步来决定到底是使用哪个Demuxer。 + +举个例子:如果是flv,那么Demuxer就会使用对应的ff_flv_demuxer,所以对应的关键生命周期的方法read_header、read_packet、read_seek、read_close都会使用该flv的Demuxer中函数指针指定的函数。read_header会将AVStream结构体构造好,以方便后续的步骤继续使用AVStream作为输入参数。 + + + +##### avformat_find_stream_info 分析 + +该方法的作用就是把所有的Stream的MetaData信息填充好。方法内部会先查找对于的解码器,然后打开对应的解码器,紧接着会利用Demuxer中的read_packet函数读取一段数据进行解码,当然,解码的数据越多,分析出来的流信息就越准确,如果是本地资源,那么很快就可以得到准确的信息了。但是对于网络资源来说,则会比较慢,因此该函数有几个参数可以控制读取数据的长度,一个是probe size,一个是max_analyze_duration, 还有一个就是fps_probe_size,这三个参数共同控制解码数据的长度,如果配置的这几个参数的数值越小,那么这个函数执行的时间就会越快,但会导致AVStream结构体里面的信息(视频的宽、高、fps、编码类型)不准确。 + + + +##### av_read_frame 分析 + +该方法读取出来的数据是AVPacket,在FFmpeg的早期版本中开发给开发者的函数其实就是av_read_packet,但是需要开发者自己来处理AVPacket中的数据不能被解码器处理完的情况,即需要把未处理完的压缩数据缓存起来的问题。所以在新版本的FFmpeg中,提供了该函数,用于处理此状况。 该函数的实现首先会委托到Demuxer的read_packet方法中,当然read_packet通过解服用层和协议层的处理后,会将数据返回到这里,在该函数中进行数据缓冲处理。 + +对于音频流,一个AVPacket可能会包含多个AVFrame,但是对于一个视频流,一个AVPacket只包含一个AVFrame,该函数最终只会返回一个AVPacket结构体。 + + + +##### avcodec_decode分析 + +该方法包含了两部分内容:一部分是解码视频,一部分是解码音频。在上面的函数分析中,我们知道,解码是会委托给对应的解码器来实施的,在打开解码器的时候就找到了对应的解码器的实现,比如对于解码H264来讲,会找到ff_h264_decoder,其中会有对应的生命周期函数的实现,最重要的就是init,decode,close三个方法,分别对应于打开解码器、解码及关闭解码器的操作,而解码过程就是调用decode方法。 + + + +##### avformat_close_input 分析 + +该函数负责释放对应的资源,首先会调用对应的Demuxer中的生命周期read_close方法,然后释放掉,AVFormatContext,最后关闭文件或者远程网络链接。 + + + +#### FFmpeg 编码 API 分析 + + + +##### avformat_alloc_output_context2 分析 + +该函数内部需要调用方法avformat_alloc_context来分配一个AVFormatContext结构体,当然最关键的还是根据上一步注册的Muxer和Demuxer部分(也就是封装格式部分)去找对应的格式。有可能是flv格式、MP4格式、mov格式,甚至是MP3格式等,如果找不到对应的格式(应该是因为在configure选项中没有打开这个格式的开关),那么这里会返回找不到对于的格式的错误提示。在调用API的时候,可以使用av_err2str把返回的整数类型的错误代码转换为肉眼可读的字符串,这是个在调试中非常有用的工具函数。该函数最终会将找出来的格式赋值给AVFormatContext类型的oformat。 + + + +##### avio_open2 分析 + +首先会调用函数ffurl_open,构造出URLContext结构体,这个结构体中包含了URLProtocol(需要去第一步register_protocol中已经注册的协议链表)中去寻找;接着会调用avio_alloc_contex方法,分配出AVIOContext结构体,并将上一步构造出来的URLProtocol传递进来;然后把上一步分配出来的AVIOContext结构体赋值给AVFormatContext属性。 + +下面就是针对上面的描述总结的结构之间的构架图,各位可以参考此图进行进一步的理解: + +![img](https://images2018.cnblogs.com/blog/682616/201807/682616-20180720100812049-1285283157.png) + +avio_open2的过程也恰好是在上面我们分析avformat_open_input过程的一个逆过程。编码过程和解码过程从逻辑上来讲,也是一个逆过程,所以在FFmpeg实现的过程中,他们也互为逆过程。 + + + +##### 编码其他API(步骤)分析 + + 编码的其他步骤也是解码的一个逆过程,解码过程中的avformat_find_stream_info对应到编码就是avformat_new_stream和avformat_write_header。 + +- avformat_new_stream函数会将音频流或者视频流的信息填充好,分配出AVStream结构体,在音频流中分配声道、采样率、表示格式、编码器等信息,在视频中分配宽、高、帧率、表示格式、编码器等信息。 +- avformat_write_header函数与解码过程中的read_header恰好是一个逆过程,这里就不多赘述了。 + +接下来就是编码阶段了: + +1. 将手动封装好的AVFrame结构体,作为avcodec_encodec_video方法的输入,然后将其编码成为AVPacket,然后调用av_write_frame方法输出到媒体文件中。 + +2. av_write_frame 方法会将编码后的AVPacket结构体作为Muxer中的write_packet生命周期方法的输入,write_packet会加上自己封装格式的头信息,然后调用协议层,写到本地文件或者网络服务器上。 + +3. 最后一步就是av_write_trailer(该函数有一个非常大的坑,如果没执行write_header操作,就直接执行write_trailer操作,程序会直接Carsh掉,所以这两个函数必须成对出现),av_write_trailer会把没有输出的AVPacket全部丢给协议层去做输出,然后会调用Muxer的write_trailer生命周期方法(不同的格式,写出的尾部也不一样)。 + + + + + +#### FFmpeg 解码 API 超时设置 + +当视频流地址能打开,但是视频流中并没有流内容的时候,可能会导致整体执行流程阻塞在 avformat_open_input 或者 av_read_frame 方法上。 + +主要原因就是avformat_open_input 和av_read_frame 这两个方法是阻塞的。 + +av_read_frame() -> read_frame_internal() -> ff_read_packet() -> s->iformat->read_packet() -> read_from_url() -> ffurl_read() -> retry_transfer_wrapper() (此方法会堵塞) + +虽然我们可以通过设置 ic->flags |= AVFMT_FLAG_NONBLOCK; 将操作设置为非阻塞,但这样设置是不推荐的,会导致后续的其他操作出现问题。 + +一般情况下,我们推荐另外两种机制进行设置: + + + +##### 设置开流的超时时间 + + 在设置开流超时时间的时候,需要注意 不同的协议设置的方式是不一样的。 + +| ``1``
``2`` | 方法: timeout --> 单位:(http:ms udp:s)
方法:stimeout --> 单位:(rtsp us) | +| --------------- | ------------------------------------------------------------ | + +设置udp、http 超时的示例代码如下: + +``` +AVDictionary* opts = NULL; +av_dict_set(&opts, "timeout", "3000000", 0);//单位 如果是http:ms 如果是udp:s +int ret = avformat_open_input(&ctx, url, NULL, &opts); +``` + + + +设置rtsp超时的示例代码如下: + +``` +AVDictionary* opts = NULL; +av_dict_set(&opts, "rtsp_transport", m_bTcp ? "tcp" : "udp", 0); //设置tcp or udp,默认一般优先tcp再尝试udp +av_dict_set(&opts, "stimeout", "3000000", 0);//单位us 也就是这里设置的是3s +ret = avformat_open_input(&ctx, url, NULL, &opts); +``` + + + +##### 设置interrupt_callback定义返回机制 + +设置回调,监控read超时情况,回调方法为: + + + +``` +int64_t lastReadPacktTime; +static int interrupt_cb(void *ctx) +{ + int timeout = 3; + if (av_gettime() - lastReadPacktTime > timeout * 1000 * 1000) + { + return -1; + } + return 0; +} +``` + + + +回调函数中返回0则代表ffmpeg继续阻塞直到ffmpeg正常工作为止,否则就代表ffmpeg结束阻塞可以将操纵权交给用户线程并返回错误码。 + +对指定的 AVFormatContext 进行设置,并在需要调用的设置的时间之前,记录当前的时间,这样在回调的时候就能根据时间差,判断执行相应的逻辑: + +avformat_open_input 设置方式: + +``` +inputContext = avformat_alloc_context(); +lastReadPacktTime = av_gettime(); +inputContext->interrupt_callback.callback = interrupt_cb; +int ret = avformat_open_input(&inputContext, inputUrl.c_str(), nullptr, nullptr); +``` + + + +av_read_frame 设置方式: + +``` +lastReadPacktTime = av_gettime(); +ret = av_read_frame(inputContext, packet); +``` + +在实际开发中,只是设计这个机制,很容易出现超时,但如果超时时间设置过程,又容易阻塞线程。一般推荐的方案为:在超时的机制上增加连续读流的时长统计,当连续读流超时超过一定时间时就通知当前读流操作已失败。 + +### FFmpeg(六):FFmpeg 核心模块 libavformat 与 libavcodec 分析 + +#### libavformat介绍 + +libavformat的主要组成与层次调用关系如下图: + + ![img](https://images2018.cnblogs.com/blog/682616/201807/682616-20180720175819282-1689330427.png) + +AVFromatContext是API层直接接触到的结构体,它会进行格式的封装和解封装,它的数据部分由底层提供,底层使用了AVIOContext,这个AVIOContext实际上就是为普通的I/O增加了一层Buffer缓冲区,再往底层就是URLContext,也就是达到了协议层,协议层的实现由很多,如rtmp、http、hls、file等,这个就是libavformat的内部封装结构了。 + +#### libavcodec介绍 + +libavcodec模块的主要组成和数据结构图如下: + + ![img](https://images2018.cnblogs.com/blog/682616/201807/682616-20180720180926182-1853199081.png) + +对于开发者来说,这个模块我们能接触到的最顶层的数据结构就是AVCodecContext,该结构体包含的就是与实际的编解码有关的部分。 + +首先AVCodecContext是包含在一个AVStream里面的,即描述了这路流的编码格式是什么,然后利用该编码器或者解码器进行AVPacket与AVFrame之间的转换(实际上就是编码或者解码的过程),这是FFmpeg中最重要的一部分。 + +### FFmpeg 结构体(一): AVFormatContext 分析 + +在 [FFmpeg (六):FFmpeg 核心模块 libavformat 与 libavcodec 分析](https://www.cnblogs.com/renhui/p/9343098.html) 中,我们分析了FFmpeg中最重要的两个模块以及重要的结构体之间的关系。 + +后面的文章,我们先不去继续了解其他模块,先针对在之前的学习中接触到的结构体进行分析,然后在根据功能源码,继续了解FFmpeg。 + +**AVFormatContext是包含码流参数较多的结构体。本文将会详细分析一下该结构体里每个变量的含义和作用。** + +#### 源码整理 + +首先我们先看一下结构体AVFormatContext的定义的结构体源码(位于libavformat/avformat.h,本人已经将相关注释翻译成中文,方便大家理解): + +```java +/** + * I/O格式上下文 + * + * sizeof(AVFormatContext)方法不能在libav*外部调用,使用avformat_alloc_context()来创建一个AVFormatContext. + */ +typedef struct AVFormatContext { + /** + * 一个用来记录和指向avoptions的类。由avformat_all_context()设置。 + * 如果(de)muxer存在私有option也会输出。 + */ + const AVClass *av_class; + + /** + * 输入容器的格式结构体 + * + * 只在解码中生成,由avformat_open_input()生成 + */ + struct AVInputFormat *iformat; + + /** + * 输出容器的格式的结构体 + * + * 只在编码中生成后,必须在调用avformat_write_header()方法之前被生成好。 + */ + struct AVOutputFormat *oformat; + + /** + * 私有数据的格式。这是一个AVOptions-enabled的结构体。 + * 当且仅当iformat/oformat.priv_class不为空的时候才会用到。 + * + * - 编码时: 由avformat_write_header()设置 + * - 解码时: 由avformat_open_input()设置 + */ + void *priv_data; + + /** + * 输入/输出上下文. + * + * - 解码时: 可以由用户自己设置(在avformat_open_intput()之前,而且必须手动关闭),也可以由avformat_open_input()设置. + * - 编码时: 由用户设置(在avformat_write_header之前).调用者必须注意关闭和释放的问题。 + * + * 如果在iformat/oformat.flags里面设置了AVFMT_NOFILE的标志,就不要设置设个字段。 因为在这个情况下,编解码器将以其他的方式进行I/O操作,这个字段将为NULL. + */ + AVIOContext *pb; + + /***************************** 流信息相关字段 ***********************************/ + /** + * 流属性标志.是AVFMTCTX_*的集合 + * 由libavformat设置. + */ + int ctx_flags; + + /** + * AVFormatContext.streams -- 流的数量 + * + * 由avformat_new_stream()设置,而且不能被其他代码更改. + */ + unsigned int nb_streams; + /** + * 文件中所有流的列表.新的流主要由avformat_new_stream()创建. + * + * - 解码时: 流是在avformat_open_input()方法里,由libavformat创建的。如果在ctx_flags里面设置了AVFMTCTX_NOHEADER,那么新的流也可能由av_read_frame()创建. + * - 编码时: 流是由用户创建的(在调用avformat_write_header()之前). + * + * 在avformat_free_context()释放. + */ + AVStream **streams; + +#if FF_API_FORMAT_FILENAME + /** + * 输入或输出的文件名 + * + * - 解码时: 由avformat_open_input()设置 + * - 编码时: 应该在调用avformat_write_header之前由调用者设置 + * + * @deprecated 本字段目前已经启用,更改为使用url地址 + */ + attribute_deprecated + char filename[1024]; +#endif + + /** + * 输入或输出的URL. 和旧文件名字段不同的是,这个字段没有长度限制. + * + * - 解码时: 有avformat_open_input()设置, 如果在avformat_open_input()设置的参数为NULL,则初始化为空字符串 + * - 编码时: 应该在调用avformat_writer_header()之前由调用者设置(或者调用avformat_init_output_()进行设置),如果在avformat_open_output()设置的参数为NULL,则初始化为空字符串。 + * + * 调用avformat_free_context()后由libavformat释放. + */ + char *url; + + /** + * 第一帧的时间(AV_TIME_BASE:单位为微秒),不要直接设置这个值,这个值是由AVStream推算出来的。 + * + * 仅用于解码,由libavformat设置. + */ + int64_t start_time; + + /** + * 流的时长(单位AV_TIME_BASE:微秒) + * + * 仅用于解码时,由libavformat设置. + */ + int64_t duration; + + /** + * 所有流的比特率,如果不可用的时候为0。不要设置这个字段,这个字段的值是由FFmpeg自动计算出来的。 + */ + int64_t bit_rate; + + unsigned int packet_size; + int max_delay; + + /** + * 用于修改编(解)码器行为的标志,由AVFMT_FLAG_*集合构成,需要用户在调用avformat_open_input()或avformat_write_header()之前进行设置 + */ + int flags; +#define AVFMT_FLAG_* 0x**** //***** + + /** + * 在确定输入格式的之前的最大输入数据量. + * 仅用于解码, 在调用avformat_open_input()之前设置。 + */ + int64_t probesize; + + /** + * 从avformat_find_stream_info()的输入数据里面读取的最大时长(单位AV_TIME_BASE:微秒) + * 仅用于解码, 在avformat_find_stream_info()设置 + * 可以设置0让avformat使用启发式机制. + */ + int64_t max_analyze_duration; + + const uint8_t *key; + int keylen; + + unsigned int nb_programs; + AVProgram **programs; + + /** + * 强制使用指定codec_id视频解码器 + * 仅用于解码时: 由用户自己设置 + */ + enum AVCodecID video_codec_id; + + /** + * 强制使用指定codec_id音频解码器 + * 仅用于解码时: 由用户自己设置. + */ + enum AVCodecID audio_codec_id; + + /** + * 强制使用指定codec_id字母解码器 + * 仅用于解码时: 由用户自己设置. + */ + enum AVCodecID subtitle_codec_id; + + /** + * 每个流的最大内存索引使用量。 + * 如果超过了大小,就会丢弃一些,这可能会使得seek操作更慢且不精准。 + * 如果提供了全部内存使用索引,这个字段会被忽略掉. + * - 编码时: 未使用 + * - 解码时: 由用户设置 + */ + unsigned int max_index_size; + + /** + * 最大缓冲帧的内存使用量(从实时捕获设备中获得的帧数据) + */ + unsigned int max_picture_buffer; + + /** + * AVChapter数组的数量 + */ + unsigned int nb_chapters; + AVChapter **chapters; + + /** + * 整个文件的元数据 + * + * - 解码时: 在avformat_open_input()方法里由libavformat设置 + * - 编码时: 可以由用户设置(在avformat_write_header()之前) + * + * 在avformat_free_context()方法里面由libavformat释放 + */ + AVDictionary *metadata; + + /** + * 流开始的绝对时间(真实世界时间) + */ + int64_t start_time_realtime; + + /** + * 用于确定帧速率的帧数 + * 仅在解码时使用 + */ + int fps_probe_size; + + /** + * 错误识别级别. + */ + int error_recognition; + + /** + * I/O层的自定义中断回调. + */ + AVIOInterruptCB interrupt_callback; + + /** + * 启动调试的标志 + */ + int debug; +#define FF_FDEBUG_TS 0x0001 + + /** + * 最大缓冲持续时间 + */ + int64_t max_interleave_delta; + + /** + * 允许非标准扩展和实验 + */ + int strict_std_compliance; + + /** + * 检测文件上发生事件的标志 + */ + int event_flags; +#define AVFMT_EVENT_FLAG_METADATA_UPDATED 0x0001 + + /** + * 等待第一个事件戳要读取的最大包数 + * 仅解码 + */ + int max_ts_probe; + + /** + * 在编码期间避免负时间戳. + * 值的大小应该是AVFMT_AVOID_NEG_TS_*其中之一. + * 注意,这个设置只会在av_interleaved_write_frame生效 + * - 编码时: 由用户设置 + * - 解码时: 未使用 + */ + int avoid_negative_ts; +#define AVFMT_AVOID_NEG_TS_* + + /** + * 传输流id. + * 这个将被转移到解码器的私有属性. 所以没有API/ABI兼容性 + */ + int ts_id; + + /** + * 音频预加载时间(单位:毫秒) + * 注意:并非所有的格式都支持这个功能,如果在不支持的时候使用,可能会发生不可预测的事情. + * - 编码时: 由用户设置 + * - 解码时: 未使用 + */ + int audio_preload; + + /** + * 最大块时间(单位:微秒). + * 注意:并非所有格式都支持这个功能,如果在不支持的时候使用,可能会发生不可预测的事情. + * - 编码时: 由用户设置 + * - 解码时: 未使用 + */ + int max_chunk_duration; + + /** + * 最大块大小(单位:bytes) + * 注意:并非所有格式都支持这个功能,如果在不支持的时候使用,可能会发生不可预测的事情. + * - 编码时: 由用户设置 + * - 解码时: 未使用 + */ + int max_chunk_size; + + /** + * 强制使用wallclock时间戳作为数据包的pts/dts + */ + int use_wallclock_as_timestamps; + + /** + * avio标志 + */ + int avio_flags; + + /** + * 可以用各种方法估计事件的字段 + */ + enum AVDurationEstimationMethod duration_estimation_method; + + /** + * 打开流时跳过初始字节 + */ + int64_t skip_initial_bytes; + + /** + * 纠正单个时间戳溢出 + */ + unsigned int correct_ts_overflow; + + /** + * 强制寻找任何帧 + */ + int seek2any; + + /** + * 在每个包只会刷新I/O context + */ + int flush_packets; + + /** + * 格式探索得分 + */ + int probe_score; + + /** + * 最大读取字节数(用于识别格式) + */ + int format_probesize; + + /** + * 允许的编码器列表(通过','分割) + */ + char *codec_whitelist; + + /** + * 允许的解码器列表(通过','分割 ) + */ + char *format_whitelist; + + ......./** + * 强制视频解码器 + */ + AVCodec *video_codec; + + /** + * 强制音频解码器 + */ + AVCodec *audio_codec; + + /** + * 强制字母解码器 + */ + AVCodec *subtitle_codec; + + /** + * 强制数据解码器 + */ + AVCodec *data_codec; + + /** + * 在元数据头中写入填充的字节数 + */ + int metadata_header_padding; + + /** + * 用户数据(放置私人数据的地方) + */ + void *opaque; + + /** + * 用于设备和应用程序之间的回调 + */ + av_format_control_message control_message_cb; + + /** + * 输出时间戳偏移量(单位:微秒) + */ + int64_t output_ts_offset; + + /** + * 转储格式分隔符 + */ + uint8_t *dump_separator; + + /** + * 强制使用的数据解码器id + */ + enum AVCodecID data_codec_id; + +#if FF_API_OLD_OPEN_CALLBACKS + /** + * 需要为解码开启更多的IO contexts时调用 + * @deprecated 已弃用,建议使用io_open and io_close. + */ + attribute_deprecated + int (*open_cb)(struct AVFormatContext *s, AVIOContext **p, const char *url, int flags, const AVIOInterruptCB *int_cb, AVDictionary **options); +#endif + + /** + * ',' separated list of allowed protocols. + * - encoding: unused + * - decoding: set by user + */ + char *protocol_whitelist; + + /** + * 打开新IO流的回调 + */ + int (*io_open)(struct AVFormatContext *s, AVIOContext **pb, const char *url, + int flags, AVDictionary **options); + + /** + * 关闭流的回调(流是由AVFormatContext.io_open()打开的) + */ + void (*io_close)(struct AVFormatContext *s, AVIOContext *pb); + + /** + * ',' 单独的不允许的协议的列表 + * - 编码: 没使用到 + * - 解码: 由用户设置 + */ + char *protocol_blacklist; + + /** + * 最大流数 + * - 编码: 没使用到 + * - 解码: 由用户设置 + */ + int max_streams; +} AVFormatContext; +``` + +#### AVForamtContext 重点字段 + +在使用FFMPEG进行开发的时候,AVFormatContext是一个贯穿始终的数据结构,很多函数都要用到它作为参数。它是FFMPEG解封装(flv,mp4,rmvb,avi)功能的结构体。下面看几个主要变量的作用(在这里考虑解码的情况): + +``` +struct AVInputFormat *iformat:输入数据的封装格式 +AVIOContext *pb:输入数据的缓存 +unsigned int nb_streams:视音频流的个数 +AVStream **streams:视音频流 +char filename[1024]:文件名 +int64_t duration:时长(单位:微秒us,转换为秒需要除以1000000) +int bit_rate:比特率(单位bps,转换为kbps需要除以1000) +AVDictionary *metadata:元数据 +``` + +视频的时长可以转换成HH:MM:SS的形式,示例代码如下: + +``` +AVFormatContext *pFormatCtx; +CString timelong; +... +//duration是以微秒为单位 +//转换成hh:mm:ss形式 +int tns, thh, tmm, tss; +tns = (pFormatCtx->duration)/1000000; +thh = tns / 3600; +tmm = (tns % 3600) / 60; +tss = (tns % 60); +timelong.Format("%02d:%02d:%02d",thh,tmm,tss); +``` + +视频的原数据(metadata)信息可以通过AVDictionary获取。元数据存储在AVDictionaryEntry结构体中,如下所示: + +``` +typedef struct AVDictionaryEntry { + char *key; + char *value; +} AVDictionaryEntry; +``` + +每一条元数据分为key和value两个属性。 + +在ffmpeg中通过av_dict_get()函数获得视频的原数据。 + +下列代码显示了获取元数据并存入meta字符串变量的过程,注意每一条key和value之间有一个"\t:",value之后有一个"\r\n" + +``` +//MetaData------------------------------------------------------------ +//从AVDictionary获得 +//需要用到AVDictionaryEntry对象 +//CString author,copyright,description; +CString meta=NULL,key,value; +AVDictionaryEntry *m = NULL; +//不用一个一个找出来 +/* m=av_dict_get(pFormatCtx->metadata,"author",m,0); +author.Format("作者:%s",m->value); +m=av_dict_get(pFormatCtx->metadata,"copyright",m,0); +copyright.Format("版权:%s",m->value); +m=av_dict_get(pFormatCtx->metadata,"description",m,0); +description.Format("描述:%s",m->value); +*/ +//使用循环读出 +//(需要读取的数据,字段名称,前一条字段(循环时使用),参数) +while(m=av_dict_get(pFormatCtx->metadata,"",m,AV_DICT_IGNORE_SUFFIX)){ + key.Format(m->key); + value.Format(m->value); + meta+=key+"\t:"+value+"\r\n" ; +} +``` + +### FFmpeg 结构体(二): AVStream 分析 + +在上文[FFmpeg 结构体(一): AVFormatContext 分析](https://www.cnblogs.com/renhui/p/9361276.html)我们学习了AVFormatContext结构体的相关内容。本文,我们将讲述一下AVStream。 + +AVStream是存储每一个视频/音频流信息的结构体。下面我们来分析一下该结构体里重要变量的含义和作用。 + +#### 源码整理 + +首先我们先看一下结构体AVStream的定义的结构体源码(位于libavformat/avformat.h): + +```java +/** + * Stream structure. + * New fields can be added to the end with minor version bumps. + * Removal, reordering and changes to existing fields require a major + * version bump. + * sizeof(AVStream) must not be used outside libav*. + */ +typedef struct AVStream { + int index; /**< stream index in AVFormatContext */ + /** + * Format-specific stream ID. + * decoding: set by libavformat + * encoding: set by the user + */ + int id; + AVCodecContext *codec; /**< codec context */ + /** + * Real base framerate of the stream. + * This is the lowest framerate with which all timestamps can be + * represented accurately (it is the least common multiple of all + * framerates in the stream). Note, this value is just a guess! + * For example, if the time base is 1/90000 and all frames have either + * approximately 3600 or 1800 timer ticks, then r_frame_rate will be 50/1. + */ + AVRational r_frame_rate; + void *priv_data; + + /** + * encoding: pts generation when outputting stream + */ + struct AVFrac pts; + + /** + * This is the fundamental unit of time (in seconds) in terms + * of which frame timestamps are represented. For fixed-fps content, + * time base should be 1/framerate and timestamp increments should be 1. + * decoding: set by libavformat + * encoding: set by libavformat in av_write_header + */ + AVRational time_base; + + /** + * Decoding: pts of the first frame of the stream in presentation order, in stream time base. + * Only set this if you are absolutely 100% sure that the value you set + * it to really is the pts of the first frame. + * This may be undefined (AV_NOPTS_VALUE). + * @note The ASF header does NOT contain a correct start_time the ASF + * demuxer must NOT set this. + */ + int64_t start_time; + + /** + * Decoding: duration of the stream, in stream time base. + * If a source file does not specify a duration, but does specify + * a bitrate, this value will be estimated from bitrate and file size. + */ + int64_t duration; + + int64_t nb_frames; ///< number of frames in this stream if known or 0 + + int disposition; /**< AV_DISPOSITION_* bit field */ + + enum AVDiscard discard; ///< Selects which packets can be discarded at will and do not need to be demuxed. + + /** + * sample aspect ratio (0 if unknown) + * - encoding: Set by user. + * - decoding: Set by libavformat. + */ + AVRational sample_aspect_ratio; + + AVDictionary *metadata; + + /** + * Average framerate + */ + AVRational avg_frame_rate; + + /** + * For streams with AV_DISPOSITION_ATTACHED_PIC disposition, this packet + * will contain the attached picture. + * + * decoding: set by libavformat, must not be modified by the caller. + * encoding: unused + */ + AVPacket attached_pic; + + /***************************************************************** + * All fields below this line are not part of the public API. They + * may not be used outside of libavformat and can be changed and + * removed at will. + * New public fields should be added right above. + ***************************************************************** + */ + + /** + * Stream information used internally by av_find_stream_info() + */ +#define MAX_STD_TIMEBASES (60*12+5) + struct { + int64_t last_dts; + int64_t duration_gcd; + int duration_count; + double duration_error[2][2][MAX_STD_TIMEBASES]; + int64_t codec_info_duration; + int nb_decoded_frames; + int found_decoder; + } *info; + + int pts_wrap_bits; /**< number of bits in pts (used for wrapping control) */ + + // Timestamp generation support: + /** + * Timestamp corresponding to the last dts sync point. + * + * Initialized when AVCodecParserContext.dts_sync_point >= 0 and + * a DTS is received from the underlying container. Otherwise set to + * AV_NOPTS_VALUE by default. + */ + int64_t reference_dts; + int64_t first_dts; + int64_t cur_dts; + int64_t last_IP_pts; + int last_IP_duration; + + /** + * Number of packets to buffer for codec probing + */ +#define MAX_PROBE_PACKETS 2500 + int probe_packets; + + /** + * Number of frames that have been demuxed during av_find_stream_info() + */ + int codec_info_nb_frames; + + /** + * Stream Identifier + * This is the MPEG-TS stream identifier +1 + * 0 means unknown + */ + int stream_identifier; + + int64_t interleaver_chunk_size; + int64_t interleaver_chunk_duration; + + /* av_read_frame() support */ + enum AVStreamParseType need_parsing; + struct AVCodecParserContext *parser; + + /** + * last packet in packet_buffer for this stream when muxing. + */ + struct AVPacketList *last_in_packet_buffer; + AVProbeData probe_data; +#define MAX_REORDER_DELAY 16 + int64_t pts_buffer[MAX_REORDER_DELAY+1]; + + AVIndexEntry *index_entries; /**< Only used if the format does not + support seeking natively. */ + int nb_index_entries; + unsigned int index_entries_allocated_size; + + /** + * flag to indicate that probing is requested + * NOT PART OF PUBLIC API + */ + int request_probe; +} AVStream; +``` + +#### AVStream 重点字段 + +``` +int index:标识该视频/音频流 + +AVCodecContext *codec:指向该视频/音频流的AVCodecContext(它们是一一对应的关系) + +AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。FFMPEG其他结构体中也有这个字段,但是根据我的经验,只有AVStream中的time_base是可用的。PTS*time_base=真正的时间 + +int64_t duration:该视频/音频流长度 + +AVDictionary *metadata:元数据信息 + +AVRational avg_frame_rate:帧率(注:对视频来说,这个挺重要的) + +AVPacket attached_pic:附带的图片。比如说一些MP3,AAC音频文件附带的专辑封面。 +``` + +### FFmpeg 结构体(三): AVPacket 分析 + +在上文[FFmpeg 结构体(二): AVStream 分析](https://www.cnblogs.com/renhui/p/9469856.html)我们学习了AVStream结构体的相关内容。本文,我们将讲述一下AVPacket。 + +AVPacket是存储压缩编码数据相关信息的结构体。下面我们来分析一下该结构体里重要变量的含义和作用。 + +#### 源码整理 + +首先我们先看一下结构体AVPacket的定义的结构体源码(位于libavcodec/avcodec.h): + + + +``` +typedef struct AVPacket { + /** + * Presentation timestamp in AVStream->time_base units; the time at which + * the decompressed packet will be presented to the user. + * Can be AV_NOPTS_VALUE if it is not stored in the file. + * pts MUST be larger or equal to dts as presentation cannot happen before + * decompression, unless one wants to view hex dumps. Some formats misuse + * the terms dts and pts/cts to mean something different. Such timestamps + * must be converted to true pts/dts before they are stored in AVPacket. + */ + int64_t pts; + /** + * Decompression timestamp in AVStream->time_base units; the time at which + * the packet is decompressed. + * Can be AV_NOPTS_VALUE if it is not stored in the file. + */ + int64_t dts; + uint8_t *data; + int size; + int stream_index; + /** + * A combination of AV_PKT_FLAG values + */ + int flags; + /** + * Additional packet data that can be provided by the container. + * Packet can contain several types of side information. + */ + struct { + uint8_t *data; + int size; + enum AVPacketSideDataType type; + } *side_data; + int side_data_elems; + + /** + * Duration of this packet in AVStream->time_base units, 0 if unknown. + * Equals next_pts - this_pts in presentation order. + */ + int duration; + void (*destruct)(struct AVPacket *); + void *priv; + int64_t pos; ///< byte position in stream, -1 if unknown + + /** + * Time difference in AVStream->time_base units from the pts of this + * packet to the point at which the output from the decoder has converged + * independent from the availability of previous frames. That is, the + * frames are virtually identical no matter if decoding started from + * the very first frame or from this keyframe. + * Is AV_NOPTS_VALUE if unknown. + * This field is not the display duration of the current packet. + * This field has no meaning if the packet does not have AV_PKT_FLAG_KEY + * set. + * + * The purpose of this field is to allow seeking in streams that have no + * keyframes in the conventional sense. It corresponds to the + * recovery point SEI in H.264 and match_time_delta in NUT. It is also + * essential for some types of subtitle streams to ensure that all + * subtitles are correctly displayed after seeking. + */ + int64_t convergence_duration; +} AVPacket; +``` + + + +#### AVPacket 重点字段 + + + +``` +uint8_t *data:压缩编码的数据。 + +int size:data的大小 + +int64_t pts:显示时间戳 + +int64_t dts:解码时间戳 + +int stream_index:标识该AVPacket所属的视频/音频流。 +``` + + + +针对data做一下说明:对于H.264格式来说,在使用FFMPEG进行视音频处理的时候,我们常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。 + +### FFmpeg 结构体(四): AVFrame 分析 + +在上文[FFmpeg 结构体(三): AVPacket 分析](https://www.cnblogs.com/renhui/p/9488751.html)我们学习了AVPacket结构体的相关内容。本文,我们将讲述一下AVFrame。 + +AVFrame是包含码流参数较多的结构体。下面我们来分析一下该结构体里重要变量的含义和作用。 + +#### 源码整理 + +首先我们先看一下结构体AVFrame的定义的结构体源码(位于libavcodec/avcodec.h): + +```java +/* + *雷霄骅 + *leixiaohua1020@126.com + *中国传媒大学/数字电视技术 + */ +/** + * Audio Video Frame. + * New fields can be added to the end of AVFRAME with minor version + * bumps. Similarly fields that are marked as to be only accessed by + * av_opt_ptr() can be reordered. This allows 2 forks to add fields + * without breaking compatibility with each other. + * Removal, reordering and changes in the remaining cases require + * a major version bump. + * sizeof(AVFrame) must not be used outside libavcodec. + */ +typedef struct AVFrame { +#define AV_NUM_DATA_POINTERS 8 + /**图像数据 + * pointer to the picture/channel planes. + * This might be different from the first allocated byte + * - encoding: Set by user + * - decoding: set by AVCodecContext.get_buffer() + */ + uint8_t *data[AV_NUM_DATA_POINTERS]; + + /** + * Size, in bytes, of the data for each picture/channel plane. + * + * For audio, only linesize[0] may be set. For planar audio, each channel + * plane must be the same size. + * + * - encoding: Set by user + * - decoding: set by AVCodecContext.get_buffer() + */ + int linesize[AV_NUM_DATA_POINTERS]; + + /** + * pointers to the data planes/channels. + * + * For video, this should simply point to data[]. + * + * For planar audio, each channel has a separate data pointer, and + * linesize[0] contains the size of each channel buffer. + * For packed audio, there is just one data pointer, and linesize[0] + * contains the total size of the buffer for all channels. + * + * Note: Both data and extended_data will always be set by get_buffer(), + * but for planar audio with more channels that can fit in data, + * extended_data must be used by the decoder in order to access all + * channels. + * + * encoding: unused + * decoding: set by AVCodecContext.get_buffer() + */ + uint8_t **extended_data; + + /**宽高 + * width and height of the video frame + * - encoding: unused + * - decoding: Read by user. + */ + int width, height; + + /** + * number of audio samples (per channel) described by this frame + * - encoding: Set by user + * - decoding: Set by libavcodec + */ + int nb_samples; + + /** + * format of the frame, -1 if unknown or unset + * Values correspond to enum AVPixelFormat for video frames, + * enum AVSampleFormat for audio) + * - encoding: unused + * - decoding: Read by user. + */ + int format; + + /**是否是关键帧 + * 1 -> keyframe, 0-> not + * - encoding: Set by libavcodec. + * - decoding: Set by libavcodec. + */ + int key_frame; + + /**帧类型(I,B,P) + * Picture type of the frame, see ?_TYPE below. + * - encoding: Set by libavcodec. for coded_picture (and set by user for input). + * - decoding: Set by libavcodec. + */ + enum AVPictureType pict_type; + + /** + * pointer to the first allocated byte of the picture. Can be used in get_buffer/release_buffer. + * This isn't used by libavcodec unless the default get/release_buffer() is used. + * - encoding: + * - decoding: + */ + uint8_t *base[AV_NUM_DATA_POINTERS]; + + /** + * sample aspect ratio for the video frame, 0/1 if unknown/unspecified + * - encoding: unused + * - decoding: Read by user. + */ + AVRational sample_aspect_ratio; + + /** + * presentation timestamp in time_base units (time when frame should be shown to user) + * If AV_NOPTS_VALUE then frame_rate = 1/time_base will be assumed. + * - encoding: MUST be set by user. + * - decoding: Set by libavcodec. + */ + int64_t pts; + + /** + * reordered pts from the last AVPacket that has been input into the decoder + * - encoding: unused + * - decoding: Read by user. + */ + int64_t pkt_pts; + + /** + * dts from the last AVPacket that has been input into the decoder + * - encoding: unused + * - decoding: Read by user. + */ + int64_t pkt_dts; + + /** + * picture number in bitstream order + * - encoding: set by + * - decoding: Set by libavcodec. + */ + int coded_picture_number; + /** + * picture number in display order + * - encoding: set by + * - decoding: Set by libavcodec. + */ + int display_picture_number; + + /** + * quality (between 1 (good) and FF_LAMBDA_MAX (bad)) + * - encoding: Set by libavcodec. for coded_picture (and set by user for input). + * - decoding: Set by libavcodec. + */ + int quality; + + /** + * is this picture used as reference + * The values for this are the same as the MpegEncContext.picture_structure + * variable, that is 1->top field, 2->bottom field, 3->frame/both fields. + * Set to 4 for delayed, non-reference frames. + * - encoding: unused + * - decoding: Set by libavcodec. (before get_buffer() call)). + */ + int reference; + + /**QP表 + * QP table + * - encoding: unused + * - decoding: Set by libavcodec. + */ + int8_t *qscale_table; + /** + * QP store stride + * - encoding: unused + * - decoding: Set by libavcodec. + */ + int qstride; + + /** + * + */ + int qscale_type; + + /**跳过宏块表 + * mbskip_table[mb]>=1 if MB didn't change + * stride= mb_width = (width+15)>>4 + * - encoding: unused + * - decoding: Set by libavcodec. + */ + uint8_t *mbskip_table; + + /**运动矢量表 + * motion vector table + * @code + * example: + * int mv_sample_log2= 4 - motion_subsample_log2; + * int mb_width= (width+15)>>4; + * int mv_stride= (mb_width << mv_sample_log2) + 1; + * motion_val[direction][x + y*mv_stride][0->mv_x, 1->mv_y]; + * @endcode + * - encoding: Set by user. + * - decoding: Set by libavcodec. + */ + int16_t (*motion_val[2])[2]; + + /**宏块类型表 + * macroblock type table + * mb_type_base + mb_width + 2 + * - encoding: Set by user. + * - decoding: Set by libavcodec. + */ + uint32_t *mb_type; + + /**DCT系数 + * DCT coefficients + * - encoding: unused + * - decoding: Set by libavcodec. + */ + short *dct_coeff; + + /**参考帧列表 + * motion reference frame index + * the order in which these are stored can depend on the codec. + * - encoding: Set by user. + * - decoding: Set by libavcodec. + */ + int8_t *ref_index[2]; + + /** + * for some private data of the user + * - encoding: unused + * - decoding: Set by user. + */ + void *opaque; + + /** + * error + * - encoding: Set by libavcodec. if flags&CODEC_FLAG_PSNR. + * - decoding: unused + */ + uint64_t error[AV_NUM_DATA_POINTERS]; + + /** + * type of the buffer (to keep track of who has to deallocate data[*]) + * - encoding: Set by the one who allocates it. + * - decoding: Set by the one who allocates it. + * Note: User allocated (direct rendering) & internal buffers cannot coexist currently. + */ + int type; + + /** + * When decoding, this signals how much the picture must be delayed. + * extra_delay = repeat_pict / (2*fps) + * - encoding: unused + * - decoding: Set by libavcodec. + */ + int repeat_pict; + + /** + * The content of the picture is interlaced. + * - encoding: Set by user. + * - decoding: Set by libavcodec. (default 0) + */ + int interlaced_frame; + + /** + * If the content is interlaced, is top field displayed first. + * - encoding: Set by user. + * - decoding: Set by libavcodec. + */ + int top_field_first; + + /** + * Tell user application that palette has changed from previous frame. + * - encoding: ??? (no palette-enabled encoder yet) + * - decoding: Set by libavcodec. (default 0). + */ + int palette_has_changed; + + /** + * codec suggestion on buffer type if != 0 + * - encoding: unused + * - decoding: Set by libavcodec. (before get_buffer() call)). + */ + int buffer_hints; + + /** + * Pan scan. + * - encoding: Set by user. + * - decoding: Set by libavcodec. + */ + AVPanScan *pan_scan; + + /** + * reordered opaque 64bit (generally an integer or a double precision float + * PTS but can be anything). + * The user sets AVCodecContext.reordered_opaque to represent the input at + * that time, + * the decoder reorders values as needed and sets AVFrame.reordered_opaque + * to exactly one of the values provided by the user through AVCodecContext.reordered_opaque + * @deprecated in favor of pkt_pts + * - encoding: unused + * - decoding: Read by user. + */ + int64_t reordered_opaque; + + /** + * hardware accelerator private data (FFmpeg-allocated) + * - encoding: unused + * - decoding: Set by libavcodec + */ + void *hwaccel_picture_private; + + /** + * the AVCodecContext which ff_thread_get_buffer() was last called on + * - encoding: Set by libavcodec. + * - decoding: Set by libavcodec. + */ + struct AVCodecContext *owner; + + /** + * used by multithreading to store frame-specific info + * - encoding: Set by libavcodec. + * - decoding: Set by libavcodec. + */ + void *thread_opaque; + + /** + * log2 of the size of the block which a single vector in motion_val represents: + * (4->16x16, 3->8x8, 2-> 4x4, 1-> 2x2) + * - encoding: unused + * - decoding: Set by libavcodec. + */ + uint8_t motion_subsample_log2; + + /**(音频)采样率 + * Sample rate of the audio data. + * + * - encoding: unused + * - decoding: read by user + */ + int sample_rate; + + /** + * Channel layout of the audio data. + * + * - encoding: unused + * - decoding: read by user. + */ + uint64_t channel_layout; + + /** + * frame timestamp estimated using various heuristics, in stream time base + * Code outside libavcodec should access this field using: + * av_frame_get_best_effort_timestamp(frame) + * - encoding: unused + * - decoding: set by libavcodec, read by user. + */ + int64_t best_effort_timestamp; + + /** + * reordered pos from the last AVPacket that has been input into the decoder + * Code outside libavcodec should access this field using: + * av_frame_get_pkt_pos(frame) + * - encoding: unused + * - decoding: Read by user. + */ + int64_t pkt_pos; + + /** + * duration of the corresponding packet, expressed in + * AVStream->time_base units, 0 if unknown. + * Code outside libavcodec should access this field using: + * av_frame_get_pkt_duration(frame) + * - encoding: unused + * - decoding: Read by user. + */ + int64_t pkt_duration; + + /** + * metadata. + * Code outside libavcodec should access this field using: + * av_frame_get_metadata(frame) + * - encoding: Set by user. + * - decoding: Set by libavcodec. + */ + AVDictionary *metadata; + + /** + * decode error flags of the frame, set to a combination of + * FF_DECODE_ERROR_xxx flags if the decoder produced a frame, but there + * were errors during the decoding. + * Code outside libavcodec should access this field using: + * av_frame_get_decode_error_flags(frame) + * - encoding: unused + * - decoding: set by libavcodec, read by user. + */ + int decode_error_flags; +#define FF_DECODE_ERROR_INVALID_BITSTREAM 1 +#define FF_DECODE_ERROR_MISSING_REFERENCE 2 + + /** + * number of audio channels, only used for audio. + * Code outside libavcodec should access this field using: + * av_frame_get_channels(frame) + * - encoding: unused + * - decoding: Read by user. + */ + int64_t channels; +} AVFrame; +``` + + + +#### AVFrame 重点字段 + +AVFrame结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),此外还包含了一些相关的信息。比如说,解码的时候存储了宏块类型表,QP表,运动矢量表等数据。编码的时候也存储了相关的数据。因此在使用FFMPEG进行码流分析的时候,AVFrame是一个很重要的结构体。 + +下面看几个主要变量的作用(在这里考虑解码的情况): + + + +``` +uint8_t *data[AV_NUM_DATA_POINTERS]:解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM) + +int linesize[AV_NUM_DATA_POINTERS]:data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽。 + +int width, height:视频帧宽和高(1920x1080,1280x720...) + +int nb_samples:音频的一个AVFrame中可能包含多个音频帧,在此标记包含了几个 + +int format:解码后原始数据类型(YUV420,YUV422,RGB24...) + +int key_frame:是否是关键帧 + +enum AVPictureType pict_type:帧类型(I,B,P...) + +AVRational sample_aspect_ratio:宽高比(16:9,4:3...) + +int64_t pts:显示时间戳 + +int coded_picture_number:编码帧序号 + +int display_picture_number:显示帧序号 + +int8_t *qscale_table:QP表 + +uint8_t *mbskip_table:跳过宏块表 + +int16_t (*motion_val[2])[2]:运动矢量表 + +uint32_t *mb_type:宏块类型表 + +short *dct_coeff:DCT系数,这个没有提取过 + +int8_t *ref_index[2]:运动估计参考帧列表(貌似H.264这种比较新的标准才会涉及到多参考帧) + +int interlaced_frame:是否是隔行扫描 + +uint8_t motion_subsample_log2:一个宏块中的运动矢量采样个数,取log的 +``` + + + +其他的变量不再一一列举,源代码中都有详细的说明。在这里重点分析一下几个需要一定的理解的变量: + +##### data[] + +对于packed格式的数据(例如RGB24),会存到data[0]里面。 + +对于planar格式的数据(例如YUV420P),则会分开成data[0],data[1],data[2]...(YUV420P中data[0]存Y,data[1]存U,data[2]存V) + +##### pict_type + +包含以下类型: + + + +``` +enum AVPictureType { + AV_PICTURE_TYPE_NONE = 0, ///< Undefined + AV_PICTURE_TYPE_I, ///< Intra + AV_PICTURE_TYPE_P, ///< Predicted + AV_PICTURE_TYPE_B, ///< Bi-dir predicted + AV_PICTURE_TYPE_S, ///< S(GMC)-VOP MPEG4 + AV_PICTURE_TYPE_SI, ///< Switching Intra + AV_PICTURE_TYPE_SP, ///< Switching Predicted + AV_PICTURE_TYPE_BI, ///< BI type +}; +``` + + + +##### sample_aspect_ratio + +宽高比是一个分数,FFMPEG中用AVRational表达分数: + + + +``` +/** + * rational number numerator/denominator + */ +typedef struct AVRational{ + int num; ///< numerator + int den; ///< denominator +} AVRational; +``` + + + +##### qscale_table + +QP表指向一块内存,里面存储的是每个宏块的QP值。宏块的标号是从左往右,一行一行的来的。每个宏块对应1个QP。 + +qscale_table[0]就是第1行第1列宏块的QP值;qscale_table[1]就是第1行第2列宏块的QP值;qscale_table[2]就是第1行第3列宏块的QP值。以此类推... + +宏块的个数用下式计算: + +注:宏块大小是16x16的。 + +每行宏块数: + +``` +int mb_stride = pCodecCtx->width/16+1 +``` + +宏块的总数: + +``` +int mb_sum = ((pCodecCtx->height+15)>>4)*(pCodecCtx->width/16+1) +``` + +### FFmpeg 结构体(五): AVCodec 分析 + +在上文[FFmpeg 结构体(四): AVFrame 分析](https://www.cnblogs.com/renhui/p/9493393.html)我们学习了AVFrame结构体的相关内容。本文,我们将讲述一下AVCodec。 + +AVCodec是存储编解码器信息的结构体。下面我们来分析一下该结构体里重要变量的含义和作用。 + +#### 源码整理 + +首先我们先看一下结构体AVCodec的定义的结构体源码(位于libavcodec/avcodec.h): + +View Code + +#### AVCodec 重点字段 + +下面说一下最主要的几个变量: + +``` +const char *name:编解码器的名字,比较短 + +const char *long_name:编解码器的名字,全称,比较长 + +enum AVMediaType type:指明了类型,是视频,音频,还是字幕 + +enum AVCodecID id:ID,不重复 + +const AVRational *supported_framerates:支持的帧率(仅视频) + +const enum AVPixelFormat *pix_fmts:支持的像素格式(仅视频) + +const int *supported_samplerates:支持的采样率(仅音频) + +const enum AVSampleFormat *sample_fmts:支持的采样格式(仅音频) + +const uint64_t *channel_layouts:支持的声道数(仅音频) + +int priv_data_size:私有数据的大小 +``` + + + +详细介绍几个变量: + +##### enum AVMediaType type + +AVMediaType定义如下: + + + +``` +enum AVMediaType { + AVMEDIA_TYPE_UNKNOWN = -1, ///< Usually treated as AVMEDIA_TYPE_DATA + AVMEDIA_TYPE_VIDEO, + AVMEDIA_TYPE_AUDIO, + AVMEDIA_TYPE_DATA, ///< Opaque data information usually continuous + AVMEDIA_TYPE_SUBTITLE, + AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse + AVMEDIA_TYPE_NB +}; +``` + + + +##### enum AVCodecID id + +AVCodecID定义如下: + + + +``` +enum AVCodecID { + AV_CODEC_ID_NONE, + + /* video codecs */ + AV_CODEC_ID_MPEG1VIDEO, + AV_CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding + AV_CODEC_ID_MPEG2VIDEO_XVMC, + AV_CODEC_ID_H261, + AV_CODEC_ID_H263, + AV_CODEC_ID_RV10, + AV_CODEC_ID_RV20, + AV_CODEC_ID_MJPEG, + AV_CODEC_ID_MJPEGB, + AV_CODEC_ID_LJPEG, + AV_CODEC_ID_SP5X, + AV_CODEC_ID_JPEGLS, + AV_CODEC_ID_MPEG4, + AV_CODEC_ID_RAWVIDEO, + AV_CODEC_ID_MSMPEG4V1, + AV_CODEC_ID_MSMPEG4V2, + AV_CODEC_ID_MSMPEG4V3, + AV_CODEC_ID_WMV1, + AV_CODEC_ID_WMV2, + AV_CODEC_ID_H263P, + AV_CODEC_ID_H263I, + AV_CODEC_ID_FLV1, + AV_CODEC_ID_SVQ1, + AV_CODEC_ID_SVQ3, + AV_CODEC_ID_DVVIDEO, + AV_CODEC_ID_HUFFYUV, + AV_CODEC_ID_CYUV, + AV_CODEC_ID_H264, + ... +} +``` + + + +##### const enum AVPixelFormat *pix_fmts + +AVPixelFormat定义如下: + + + +``` +enum AVPixelFormat { + AV_PIX_FMT_NONE = -1, + AV_PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples) + AV_PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr + AV_PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB... + AV_PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR... + AV_PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples) + AV_PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples) + AV_PIX_FMT_YUV410P, ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y samples) + AV_PIX_FMT_YUV411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples) + AV_PIX_FMT_GRAY8, ///< Y , 8bpp + AV_PIX_FMT_MONOWHITE, ///< Y , 1bpp, 0 is white, 1 is black, in each byte pixels are ordered from the msb to the lsb + AV_PIX_FMT_MONOBLACK, ///< Y , 1bpp, 0 is black, 1 is white, in each byte pixels are ordered from the msb to the lsb + AV_PIX_FMT_PAL8, ///< 8 bit with PIX_FMT_RGB32 palette + AV_PIX_FMT_YUVJ420P, ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV420P and setting color_range + AV_PIX_FMT_YUVJ422P, ///< planar YUV 4:2:2, 16bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV422P and setting color_range + AV_PIX_FMT_YUVJ444P, ///< planar YUV 4:4:4, 24bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV444P and setting color_range + AV_PIX_FMT_XVMC_MPEG2_MC,///< XVideo Motion Acceleration via common packet passing + AV_PIX_FMT_XVMC_MPEG2_IDCT, + ...(代码太长,略) +} +``` + + + +##### const enum AVSampleFormat *sample_fmts + + + +``` +enum AVSampleFormat { + AV_SAMPLE_FMT_NONE = -1, + AV_SAMPLE_FMT_U8, ///< unsigned 8 bits + AV_SAMPLE_FMT_S16, ///< signed 16 bits + AV_SAMPLE_FMT_S32, ///< signed 32 bits + AV_SAMPLE_FMT_FLT, ///< float + AV_SAMPLE_FMT_DBL, ///< double + + AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar + AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar + AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar + AV_SAMPLE_FMT_FLTP, ///< float, planar + AV_SAMPLE_FMT_DBLP, ///< double, planar + + AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically +}; +``` + + + + 每一个编解码器对应一个该结构体,查看一下ffmpeg的源代码,我们可以看一下H.264解码器的结构体如下所示(h264.c): + + + +``` +AVCodec ff_h264_decoder = { + .name = "h264", + .type = AVMEDIA_TYPE_VIDEO, + .id = CODEC_ID_H264, + .priv_data_size = sizeof(H264Context), + .init = ff_h264_decode_init, + .close = ff_h264_decode_end, + .decode = decode_frame, + .capabilities = /*CODEC_CAP_DRAW_HORIZ_BAND |*/ CODEC_CAP_DR1 | CODEC_CAP_DELAY | + CODEC_CAP_SLICE_THREADS | CODEC_CAP_FRAME_THREADS, + .flush= flush_dpb, + .long_name = NULL_IF_CONFIG_SMALL("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"), + .init_thread_copy = ONLY_IF_THREADS_ENABLED(decode_init_thread_copy), + .update_thread_context = ONLY_IF_THREADS_ENABLED(decode_update_thread_context), + .profiles = NULL_IF_CONFIG_SMALL(profiles), + .priv_class = &h264_class, +}; +``` + + + +JPEG2000解码器结构体(j2kdec.c): + + + +``` +AVCodec ff_jpeg2000_decoder = { + .name = "j2k", + .type = AVMEDIA_TYPE_VIDEO, + .id = CODEC_ID_JPEG2000, + .priv_data_size = sizeof(J2kDecoderContext), + .init = j2kdec_init, + .close = decode_end, + .decode = decode_frame, + .capabilities = CODEC_CAP_EXPERIMENTAL, + .long_name = NULL_IF_CONFIG_SMALL("JPEG 2000"), + .pix_fmts = + (const enum PixelFormat[]) {PIX_FMT_GRAY8, PIX_FMT_RGB24, PIX_FMT_NONE} +}; +``` + + + +下面简单介绍一下遍历ffmpeg中的解码器信息的方法(这些解码器以一个链表的形式存储): + +1.注册所有编解码器:av_register_all(); + +2.声明一个AVCodec类型的指针,比如说AVCodec* first_c; + +3.调用av_codec_next()函数,即可获得指向链表下一个解码器的指针,循环往复可以获得所有解码器的信息。注意,如果想要获得指向第一个解码器的指针,则需要将该函数的参数设置为NULL。 + +### FFmpeg 结构体(六): AVCodecContext 分析 + +在上文[FFmpeg 结构体(五): AVCodec 分析](https://www.cnblogs.com/renhui/p/9493690.html)我们学习了AVCodec结构体的相关内容。本文,我们将讲述一下AVCodecContext。 + +AVCodecContext是包含变量较多的结构体(感觉差不多是变量最多的结构体)。下面我们来分析一下该结构体里重要变量的含义和作用。 + +#### 源码整理 + +首先我们先看一下结构体AVCodecContext的定义的结构体源码(位于libavcodec/avcodec.h): + +View Code + +#### AVCodecContext 重点字段 + +下面挑一些关键的变量来看看(这里只考虑解码): + + + +``` +enum AVMediaType codec_type:编解码器的类型(视频,音频...) + +struct AVCodec *codec:采用的解码器AVCodec(H.264,MPEG2...) + +int bit_rate:平均比特率 + +uint8_t *extradata; int extradata_size:针对特定编码器包含的附加信息(例如对于H.264解码器来说,存储SPS,PPS等) + +AVRational time_base:根据该参数,可以把PTS转化为实际的时间(单位为秒s) + +int width, height:如果是视频的话,代表宽和高 + +int refs:运动估计参考帧的个数(H.264的话会有多帧,MPEG2这类的一般就没有了) + +int sample_rate:采样率(音频) + +int channels:声道数(音频) + +enum AVSampleFormat sample_fmt:采样格式 + +int profile:型(H.264里面就有,其他编码标准应该也有) + +int level:级(和profile差不太多) +``` + + + +在这里需要注意:AVCodecContext中很多的参数是编码的时候使用的,而不是解码的时候使用的。 + +其实这些参数都比较容易理解。就不多费篇幅了。在这里看一下以下几个参数: + +##### codec_type + +编解码器类型有以下几种: + + + +``` +enum AVMediaType { + AVMEDIA_TYPE_UNKNOWN = -1, ///< Usually treated as AVMEDIA_TYPE_DATA + AVMEDIA_TYPE_VIDEO, + AVMEDIA_TYPE_AUDIO, + AVMEDIA_TYPE_DATA, ///< Opaque data information usually continuous + AVMEDIA_TYPE_SUBTITLE, + AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse + AVMEDIA_TYPE_NB +}; +``` + + + +##### sample_fmt + +在FFMPEG中音频采样格式有以下几种: + + + +``` +enum AVSampleFormat { + AV_SAMPLE_FMT_NONE = -1, + AV_SAMPLE_FMT_U8, ///< unsigned 8 bits + AV_SAMPLE_FMT_S16, ///< signed 16 bits + AV_SAMPLE_FMT_S32, ///< signed 32 bits + AV_SAMPLE_FMT_FLT, ///< float + AV_SAMPLE_FMT_DBL, ///< double + + AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar + AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar + AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar + AV_SAMPLE_FMT_FLTP, ///< float, planar + AV_SAMPLE_FMT_DBLP, ///< double, planar + + AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically +}; +``` + + + +##### profile + +在FFMPEG中型有以下几种,可以看出AAC,MPEG2,H.264,VC-1,MPEG4都有型的概念。 + + + +``` +####define FF_PROFILE_UNKNOWN -99 +####define FF_PROFILE_RESERVED -100 + +####define FF_PROFILE_AAC_MAIN 0 +####define FF_PROFILE_AAC_LOW 1 +####define FF_PROFILE_AAC_SSR 2 +####define FF_PROFILE_AAC_LTP 3 +####define FF_PROFILE_AAC_HE 4 +####define FF_PROFILE_AAC_HE_V2 28 +####define FF_PROFILE_AAC_LD 22 +####define FF_PROFILE_AAC_ELD 38 + +####define FF_PROFILE_DTS 20 +####define FF_PROFILE_DTS_ES 30 +####define FF_PROFILE_DTS_96_24 40 +####define FF_PROFILE_DTS_HD_HRA 50 +####define FF_PROFILE_DTS_HD_MA 60 + +####define FF_PROFILE_MPEG2_422 0 +####define FF_PROFILE_MPEG2_HIGH 1 +####define FF_PROFILE_MPEG2_SS 2 +####define FF_PROFILE_MPEG2_SNR_SCALABLE 3 +####define FF_PROFILE_MPEG2_MAIN 4 +####define FF_PROFILE_MPEG2_SIMPLE 5 + +####define FF_PROFILE_H264_CONSTRAINED (1<<9) // 8+1; constraint_set1_flag +####define FF_PROFILE_H264_INTRA (1<<11) // 8+3; constraint_set3_flag + +####define FF_PROFILE_H264_BASELINE 66 +####define FF_PROFILE_H264_CONSTRAINED_BASELINE (66|FF_PROFILE_H264_CONSTRAINED) +####define FF_PROFILE_H264_MAIN 77 +####define FF_PROFILE_H264_EXTENDED 88 +####define FF_PROFILE_H264_HIGH 100 +####define FF_PROFILE_H264_HIGH_10 110 +####define FF_PROFILE_H264_HIGH_10_INTRA (110|FF_PROFILE_H264_INTRA) +####define FF_PROFILE_H264_HIGH_422 122 +####define FF_PROFILE_H264_HIGH_422_INTRA (122|FF_PROFILE_H264_INTRA) +####define FF_PROFILE_H264_HIGH_444 144 +####define FF_PROFILE_H264_HIGH_444_PREDICTIVE 244 +####define FF_PROFILE_H264_HIGH_444_INTRA (244|FF_PROFILE_H264_INTRA) +####define FF_PROFILE_H264_CAVLC_444 44 + +####define FF_PROFILE_VC1_SIMPLE 0 +####define FF_PROFILE_VC1_MAIN 1 +####define FF_PROFILE_VC1_COMPLEX 2 +####define FF_PROFILE_VC1_ADVANCED 3 + +####define FF_PROFILE_MPEG4_SIMPLE 0 +####define FF_PROFILE_MPEG4_SIMPLE_SCALABLE 1 +####define FF_PROFILE_MPEG4_CORE 2 +####define FF_PROFILE_MPEG4_MAIN 3 +####define FF_PROFILE_MPEG4_N_BIT 4 +####define FF_PROFILE_MPEG4_SCALABLE_TEXTURE 5 +####define FF_PROFILE_MPEG4_SIMPLE_FACE_ANIMATION 6 +####define FF_PROFILE_MPEG4_BASIC_ANIMATED_TEXTURE 7 +####define FF_PROFILE_MPEG4_HYBRID 8 +####define FF_PROFILE_MPEG4_ADVANCED_REAL_TIME 9 +####define FF_PROFILE_MPEG4_CORE_SCALABLE 10 +####define FF_PROFILE_MPEG4_ADVANCED_CODING 11 +####define FF_PROFILE_MPEG4_ADVANCED_CORE 12 +####define FF_PROFILE_MPEG4_ADVANCED_SCALABLE_TEXTURE 13 +####define FF_PROFILE_MPEG4_SIMPLE_STUDIO 14 +####define FF_PROFILE_MPEG4_ADVANCED_SIMPLE 15 +``` + +### FFmpeg 结构体(七): AVIOContext 分析 + +在上文[FFmpeg 结构体(六): AVCodecContext 分析](https://www.cnblogs.com/renhui/p/9494286.html)我们学习了AVCodec结构体的相关内容。本文,我们将讲述一下AVIOContext。 + +AVIOContext是FFMPEG管理输入输出数据的结构体。下面我们来分析一下该结构体里重要变量的含义和作用。 + +#### 源码整理 + +首先我们先看一下结构体AVIOContext的定义的结构体源码(位于libavformat/avio.h): + + + +``` + /** + * Bytestream IO Context. + * New fields can be added to the end with minor version bumps. + * Removal, reordering and changes to existing fields require a major + * version bump. + * sizeof(AVIOContext) must not be used outside libav*. + * + * @note None of the function pointers in AVIOContext should be called + * directly, they should only be set by the client application + * when implementing custom I/O. Normally these are set to the + * function pointers specified in avio_alloc_context() + */ +typedef struct { + /** + * A class for private options. + * + * If this AVIOContext is created by avio_open2(), av_class is set and + * passes the options down to protocols. + * + * If this AVIOContext is manually allocated, then av_class may be set by + * the caller. + * + * warning -- this field can be NULL, be sure to not pass this AVIOContext + * to any av_opt_* functions in that case. + */ + AVClass *av_class; + unsigned char *buffer; /**< Start of the buffer. */ + int buffer_size; /**< Maximum buffer size */ + unsigned char *buf_ptr; /**< Current position in the buffer */ + unsigned char *buf_end; /**< End of the data, may be less than + buffer+buffer_size if the read function returned + less data than requested, e.g. for streams where + no more data has been received yet. */ + void *opaque; /**< A private pointer, passed to the read/write/seek/... + functions. */ + int (*read_packet)(void *opaque, uint8_t *buf, int buf_size); + int (*write_packet)(void *opaque, uint8_t *buf, int buf_size); + int64_t (*seek)(void *opaque, int64_t offset, int whence); + int64_t pos; /**< position in the file of the current buffer */ + int must_flush; /**< true if the next seek should flush */ + int eof_reached; /**< true if eof reached */ + int write_flag; /**< true if open for writing */ + int max_packet_size; + unsigned long checksum; + unsigned char *checksum_ptr; + unsigned long (*update_checksum)(unsigned long checksum, const uint8_t *buf, unsigned int size); + int error; /**< contains the error code or 0 if no error happened */ + /** + * Pause or resume playback for network streaming protocols - e.g. MMS. + */ + int (*read_pause)(void *opaque, int pause); + /** + * Seek to a given timestamp in stream with the specified stream_index. + * Needed for some network streaming protocols which don't support seeking + * to byte position. + */ + int64_t (*read_seek)(void *opaque, int stream_index, + int64_t timestamp, int flags); + /** + * A combination of AVIO_SEEKABLE_ flags or 0 when the stream is not seekable. + */ + int seekable; + + /** + * max filesize, used to limit allocations + * This field is internal to libavformat and access from outside is not allowed. + */ + int64_t maxsize; +} AVIOContext; +``` + + + +#### AVIOContext 重点字段 + +AVIOContext中有以下几个变量比较重要: + + + +``` +unsigned char *buffer:缓存开始位置 + +int buffer_size:缓存大小(默认32768) + +unsigned char *buf_ptr:当前指针读取到的位置 + +unsigned char *buf_end:缓存结束的位置 + +void *opaque:URLContext结构体 +``` + + + +在解码的情况下,buffer用于存储ffmpeg读入的数据。例如打开一个视频文件的时候,先把数据从硬盘读入buffer,然后在送给解码器用于解码。 + +其中opaque指向了URLContext。注意,这个结构体并不在FFMPEG提供的头文件中,而是在FFMPEG的源代码中。从FFMPEG源代码中翻出的定义如下所示: + + + +``` +typedef struct URLContext { + const AVClass *av_class; ///< information for av_log(). Set by url_open(). + struct URLProtocol *prot; + int flags; + int is_streamed; /**< true if streamed (no seek possible), default = false */ + int max_packet_size; /**< if non zero, the stream is packetized with this max packet size */ + void *priv_data; + char *filename; /**< specified URL */ + int is_connected; + AVIOInterruptCB interrupt_callback; +} URLContext; +``` + + + +URLContext结构体中还有一个结构体URLProtocol。注:每种协议(rtp,rtmp,file等)对应一个URLProtocol。这个结构体也不在FFMPEG提供的头文件中。从FFMPEG源代码中翻出其的定义: + + + +``` +typedef struct URLProtocol { + const char *name; + int (*url_open)(URLContext *h, const char *url, int flags); + int (*url_read)(URLContext *h, unsigned char *buf, int size); + int (*url_write)(URLContext *h, const unsigned char *buf, int size); + int64_t (*url_seek)(URLContext *h, int64_t pos, int whence); + int (*url_close)(URLContext *h); + struct URLProtocol *next; + int (*url_read_pause)(URLContext *h, int pause); + int64_t (*url_read_seek)(URLContext *h, int stream_index, + int64_t timestamp, int flags); + int (*url_get_file_handle)(URLContext *h); + int priv_data_size; + const AVClass *priv_data_class; + int flags; + int (*url_check)(URLContext *h, int mask); +} URLProtocol; +``` + + + +在这个结构体中,除了一些回调函数接口之外,有一个变量const char *name,该变量存储了协议的名称。每一种输入协议都对应这样一个结构体。 + +比如说,文件协议中代码如下(file.c): + + + +``` +URLProtocol ff_file_protocol = { + .name = "file", + .url_open = file_open, + .url_read = file_read, + .url_write = file_write, + .url_seek = file_seek, + .url_close = file_close, + .url_get_file_handle = file_get_handle, + .url_check = file_check, +}; +``` + + + +libRTMP中代码如下(libRTMP.c): + + + +``` +URLProtocol ff_rtmp_protocol = { + .name = "rtmp", + .url_open = rtmp_open, + .url_read = rtmp_read, + .url_write = rtmp_write, + .url_close = rtmp_close, + .url_read_pause = rtmp_read_pause, + .url_read_seek = rtmp_read_seek, + .url_get_file_handle = rtmp_get_file_handle, + .priv_data_size = sizeof(RTMP), + .flags = URL_PROTOCOL_FLAG_NETWORK, +}; +``` + + + +udp协议代码如下(udp.c): + + + +``` +URLProtocol ff_udp_protocol = { + .name = "udp", + .url_open = udp_open, + .url_read = udp_read, + .url_write = udp_write, + .url_close = udp_close, + .url_get_file_handle = udp_get_file_handle, + .priv_data_size = sizeof(UDPContext), + .flags = URL_PROTOCOL_FLAG_NETWORK, +}; +``` + + + +等号右边的函数是完成具体读写功能的函数。可以看一下file协议的几个函数(其实就是读文件,写文件这样的操作)(file.c): + + + +``` +/* standard file protocol */ + +static int file_read(URLContext *h, unsigned char *buf, int size) +{ + int fd = (intptr_t) h->priv_data; + int r = read(fd, buf, size); + return (-1 == r)?AVERROR(errno):r; +} + +static int file_write(URLContext *h, const unsigned char *buf, int size) +{ + int fd = (intptr_t) h->priv_data; + int r = write(fd, buf, size); + return (-1 == r)?AVERROR(errno):r; +} + +static int file_get_handle(URLContext *h) +{ + return (intptr_t) h->priv_data; +} + +static int file_check(URLContext *h, int mask) +{ + struct stat st; + int ret = stat(h->filename, &st); + if (ret < 0) + return AVERROR(errno); + + ret |= st.st_mode&S_IRUSR ? mask&AVIO_FLAG_READ : 0; + ret |= st.st_mode&S_IWUSR ? mask&AVIO_FLAG_WRITE : 0; + + return ret; +} + +####if CONFIG_FILE_PROTOCOL + +static int file_open(URLContext *h, const char *filename, int flags) +{ + int access; + int fd; + + av_strstart(filename, "file:", &filename); + + if (flags & AVIO_FLAG_WRITE && flags & AVIO_FLAG_READ) { + access = O_CREAT | O_TRUNC | O_RDWR; + } else if (flags & AVIO_FLAG_WRITE) { + access = O_CREAT | O_TRUNC | O_WRONLY; + } else { + access = O_RDONLY; + } +####ifdef O_BINARY + access |= O_BINARY; +####endif + fd = open(filename, access, 0666); + if (fd == -1) + return AVERROR(errno); + h->priv_data = (void *) (intptr_t) fd; + return 0; +} + +/* XXX: use llseek */ +static int64_t file_seek(URLContext *h, int64_t pos, int whence) +{ + int fd = (intptr_t) h->priv_data; + if (whence == AVSEEK_SIZE) { + struct stat st; + int ret = fstat(fd, &st); + return ret < 0 ? AVERROR(errno) : st.st_size; + } + return lseek(fd, pos, whence); +} + +static int file_close(URLContext *h) +{ + int fd = (intptr_t) h->priv_data; + return close(fd); +} +``` + +### FFmpeg 结构体(八):FFMPEG中重要结构体之间的关系 + +FFMPEG中结构体很多。最关键的结构体可以分成以下几类: + +#### 解协议(http,rtsp,rtmp,mms) + +AVIOContext,URLProtocol,URLContext主要存储视音频使用的协议的类型以及状态。URLProtocol存储输入视音频使用的封装格式。每种协议都对应一个URLProtocol结构。 + +#### 解封装(flv,avi,rmvb,mp4) + +AVFormatContext主要存储视音频封装格式中包含的信息;AVInputFormat存储输入视音频使用的封装格式。每种视音频封装格式都对应一个AVInputFormat 结构。 + +#### 解码(h264,mpeg2,aac,mp3) + +每个AVStream存储一个视频/音频流的相关数据;每个AVStream对应一个AVCodecContext,存储该视频/音频流使用解码方式的相关数据;每个AVCodecContext中对应一个AVCodec,包含该视频/音频对应的解码器。每种解码器都对应一个AVCodec结构。 + +#### 存数据 + +视频的话,每个结构一般是存一帧;音频可能有好几帧 + +解码前数据:AVPacket + +解码后数据:AVFrame + +他们之间的对应关系如下所示: + +![img](https://images2018.cnblogs.com/blog/682616/201808/682616-20180818164850060-1975380874.png) + +### FFmpeg 开发之 AVFilter 使用流程总结 + +在使用FFmpeg开发时,使用AVFilter的流程较为复杂,涉及到的数据结构和函数也比较多,那么使用FFmpeg AVFilter的整体流程是什么样,在其执行过程中都有哪些步骤,需要注意哪些细节?这些都是需要我们整理和总结的。 + +首先,我们需要引入三个概念结构体:AVFilterGraph 、AVFilterContext、AVFilter。 + + + +#### AVFilterGraph 、AVFilterContext、AVFilter + +在 FFmpeg 中有多种多样的滤镜,你可以把他们当成一个个小工具,专门用于处理视频和音频数据,以便实现一定的目的。如 overlay 这个滤镜,可以将一个图画覆盖到另一个图画上;transport 这个滤镜可以将图画做旋转等等。 + +一个 filter 的输出可以作为另一个 filter 的输入,因此多个 filter 可以组织成为一个网状的 filter graph,从而实现更加复杂或者综合的任务。 + +在 libavfilter 中,我们用类型 AVFilter 来表示一个 filter,每一个 filter 都是经过注册的,其特性是相对固定的。而 AVFilterContext 则表示一个真正的 filter 实例,这和 AVCodec 以及 AVCodecContext 的关系是类似的。 + +AVFilter 中最重要的特征就是其所需的输入和输出。 + +AVFilterContext 表示一个 AVFilter 的实例,我们在实际使用 filter 时,就是使用这个结构体。AVFilterContext 在被使用前,它必须是 被初始化的,就是需要对 filter 进行一些选项上的设置,通过初始化告诉 FFmpeg 我们已经做了相关的配置。 + +AVFilterGraph 表示一个 filter graph,当然它也包含了 filter chain的概念。graph 包含了诸多 filter context 实例,并负责它们之间的 link,graph 会负责创建,保存,释放 这些相关的 filter context 和 link,一般不需要用户进行管理。除此之外,它还有线程特性和最大线程数量的字段,和filter context类似。graph 的操作有:分配一个graph,往graph中添加一个filter context,添加一个 filter graph,对 filter 进行 link 操作,检查内部的link和format是否有效,释放graph等。 + + + +#### AVFilter 相关Api使用方法整理 + + + +##### AVFilterContext 初始化方法 + +AVFilterContext 的初始化方式有三种,avfilter_init_str() 和 avfilter_init_dict()、avfilter_graph_create_filter(). + +``` +/* + 使用提供的参数初始化 filter。 + 参数args:表示用于初始化 filter 的 options。该字符串必须使用 ":" 来分割各个键值对, 而键值对的形式为 'key=value'。如果不需要设置选项,args为空。 + 除了这种方式设置选项之外,还可以利用 AVOptions API 直接对 filter 设置选项。 + 返回值:成功返回0,失败返回一个负的错误值 +*/ +int avfilter_init_str(AVFilterContext *ctx, const char *args); +``` + +``` +/* + 使用提供的参数初始化filter。 + 参数 options:以 dict 形式提供的 options。 + 返回值:成功返回0,失败返回一个负的错误值 + 注意:这个函数和 avfilter_init_str 函数的功能是一样的,只不过传递的参数形式不同。 但是当传入的 options 中有不被 filter 所支持的参数时,这两个函数的行为是不同: avfilter_init_str 调用会失败,而这个函数则不会失败,它会将不能应用于指定 filter 的 option 通过参数 options 返回,然后继续执行任务。 +*/ +int avfilter_init_dict(AVFilterContext *ctx, AVDictionary **options); +``` + +``` +/** + * 创建一个Filter实例(根据args和opaque的参数),并添加到已存在的AVFilterGraph. + * 如果创建成功*filt_ctx会指向一个创建好的Filter实例,否则会指向NULL. + * @return 失败返回负数,否则返回大于等于0的数 + */ +int avfilter_graph_create_filter(AVFilterContext **filt_ctx, const AVFilter *filt, const char *name,                   const char *args, void *opaque, AVFilterGraph *graph_ctx); +``` + + + +##### AVFilterGraph 相关的Api + +AVFilterGraph 表示一个 filter graph,当然它也包含了 filter chain的概念。graph 包含了诸多 filter context 实例,并负责它们之间的 link,graph 会负责创建,保存,释放 这些相关的 filter context 和 link,一般不需要用户进行管理。 + +graph 的操作有:分配一个graph,往graph中添加一个filter context,添加一个 filter graph,对 filter 进行 link 操作,检查内部的link和format是否有效,释放graph等。 + +根据上述操作,可以列举的方法分别为: + +**分配空的filter graph:** + +``` +/* + 分配一个空的 filter graph. + 成功返回一个 filter graph,失败返回 NULL +*/ +AVFilterGraph *avfilter_graph_alloc(void); +``` + + **创建一个新的filter实例:** + +``` +/* + 在 filter graph 中创建一个新的 filter 实例。这个创建的实例尚未初始化。 + 详细描述:在 graph 中创建一个名称为 name 的 filter类型的实例。 + 创建失败,返回NULL。创建成功,返回 filter context实例。创建成功后的实例会加入到graph中, + 可以通过 AVFilterGraph.filters 或者 avfilter_graph_get_filter() 获取。 +*/ +AVFilterContext *avfilter_graph_alloc_filter(AVFilterGraph *graph, const AVFilter *filter, const char *name); +``` + +**返回名字为name的filter context:** + +``` +/* + 返回 graph 中的名为 name 的 filter context。 +*/ +AVFilterContext *avfilter_graph_get_filter(AVFilterGraph *graph, const char *name); + +``` + +**在 filter graph 中创建一个新的 filter context 实例,并使用args和opaque初始化这个filter context:** + +``` +/* + 在 filter graph 中创建一个新的 filter context 实例,并使用 args 和 opaque 初始化这个实例。 + + 参数 filt_ctx:返回成功创建的 filter context + + 返回值:成功返回正数,失败返回负的错误值。 +*/ +int avfilter_graph_create_filter(AVFilterContext **filt_ctx, const AVFilter *filt, const char *name,                        const char *args, void *opaque, AVFilterGraph *graph_ctx); +``` + + **配置 AVFilterGraph 的链接和格式:** + +``` +/* + 检查 graph 的有效性,并配置其中所有的连接和格式。 + 有效则返回 >= 0 的数,否则返回一个负值的 AVERROR. + */ +int avfilter_graph_config(AVFilterGraph *graphctx, void *log_ctx); +``` + +**释放AVFilterGraph:** + +``` +/* + 释放graph,摧毁内部的连接,并将其置为NULL。 +*/ +void avfilter_graph_free(AVFilterGraph **graph); +``` + +**在一个已经存在的link中插入一个FilterContext:** + +``` +/* + 在一个已经存在的 link 中间插入一个 filter context。 + 参数filt_srcpad_idx和filt_dstpad_idx:指定filt要连接的输入和输出pad的index。 + 成功返回0. +*/ +int avfilter_insert_filter(AVFilterLink *link, AVFilterContext *filt,                   unsigned filt_srcpad_idx, unsigned filt_dstpad_idx); +``` + +将字符串描述的filter graph 加入到一个已存在的graph中: + +``` +/* + 将一个字符串描述的 filter graph 加入到一个已经存在的 graph 中。 + + 注意:调用者必须提供 inputs 列表和 outputs 列表。它们在调用这个函数之前必须是已知的。 + + 注意:inputs 参数用于描述已经存在的 graph 的输入 pad 列表,也就是说,从新的被创建的 graph 来讲,它们是 output。 + outputs 参数用于已经存在的 graph 的输出 pad 列表,从新的被创建的 graph 来说,它们是 input。 + + 成功返回 >= 0,失败返回负的错误值。 +*/ +int avfilter_graph_parse(AVFilterGraph *graph, const char *filters, + AVFilterInOut *inputs, AVFilterInOut *outputs, + void *log_ctx); +``` + +``` +/* + 和 avfilter_graph_parse 类似。不同的是 inputs 和 outputs 参数,即做输入参数,也做输出参数。 + 在函数返回时,它们将会保存 graph 中所有的处于 open 状态的 pad。返回的 inout 应该使用 avfilter_inout_free() 释放掉。 + + 注意:在字符串描述的 graph 中,第一个 filter 的输入如果没有被一个字符串标识,默认其标识为"in",最后一个 filter 的输出如果没有被标识,默认为"output"。 + + intpus:作为输入参数是,用于保存已经存在的graph的open inputs,可以为NULL。 + 作为输出参数,用于保存这个parse函数之后,仍然处于open的inputs,当然如果传入为NULL,则并不输出。 + outputs:同上。 +*/ +int avfilter_graph_parse_ptr(AVFilterGraph *graph, const char *filters, + AVFilterInOut **inputs, AVFilterInOut **outputs, void *log_ctx); +``` + +``` +/* + 和 avfilter_graph_parse_ptr 函数类似,不同的是,inputs 和 outputs 函数不作为输入参数, + 仅作为输出参数,返回字符串描述的新的被解析的graph在这个parse函数后,仍然处于open状态的inputs和outputs。 + 返回的 inout 应该使用 avfilter_inout_free() 释放掉。 + + 成功返回0,失败返回负的错误值。 +*/ +int avfilter_graph_parse2(AVFilterGraph *graph, const char *filters, + AVFilterInOut **inputs, AVFilterInOut **outputs); +``` + +**将graph转换为可读取的字符串描述:** + +``` +/* + 将 graph 转化为可读的字符串描述。 + 参数options:未使用,忽略它。 +*/ +char *avfilter_graph_dump(AVFilterGraph *graph, const char *options); +``` + + + +#### FFmpeg Filter Buffer 和 BufferSink 相关APi的使用方法整理 + + Buffer 和 BufferSink 作为 graph 的输入点和输出点来和我们交互,我们仅需要和其进行数据交互即可。其API如下: + +``` +//buffersrc flag +enum { + //不去检测 format 的变化 + AV_BUFFERSRC_FLAG_NO_CHECK_FORMAT = 1, + + //立刻将 frame 推送到 output + AV_BUFFERSRC_FLAG_PUSH = 4, + + //对输入的frame新建一个引用,而非接管引用 + //如果 frame 是引用计数的,那么对它创建一个新的引用;否则拷贝frame中的数据 + AV_BUFFERSRC_FLAG_KEEP_REF = 8, +}; +``` + +**向 buffer_src 添加一个Frame:** + +``` +/* + 向 buffer_src 添加一个 frame。 + + 默认情况下,如果 frame 是引用计数的,那么这个函数将会接管其引用并重新设置 frame。 + 但这个行为可以由 flags 来控制。如果 frame 不是引用计数的,那么拷贝该 frame。 + + 如果函数返回一个 error,那么 frame 并未被使用。frame为NULL时,表示 EOF。 + 成功返回 >= 0,失败返回负的AVERROR。 +*/ +int av_buffersrc_add_frame_flags(AVFilterContext *buffer_src, AVFrame *frame, int flags); +``` + +**添加一个frame到 src filter:** + +``` +/* + 添加一个 frame 到 src filter。 + 这个函数等同于没有 AV_BUFFERSRC_FLAG_KEEP_REF 的 av_buffersrc_add_frame_flags() 函数。 + */ +int av_buffersrc_add_frame(AVFilterContext *ctx, AVFrame *frame); +``` +``` +/* + 添加一个 frame 到 src filter。 + 这个函数等同于设置了 AV_BUFFERSRC_FLAG_KEEP_REF 的av_buffersrc_add_frame_flags() 函数。 +*/ +int av_buffersrc_write_frame(AVFilterContext *ctx, const AVFrame *frame); +``` + +**从sink获取已filtered处理的帧,并放到参数frame中:** + +``` +/* + 从 sink 中获取已进行 filtered 处理的帧,并将其放到参数 frame 中。 + + 参数ctx:指向 buffersink 或 abuffersink 类型的 filter context + 参数frame:获取到的被处理后的frame,使用后必须使用av_frame_unref() / av_frame_free()释放掉它 + + 成功返回非负数,失败返回负的错误值,如 EAGAIN(表示需要新的输入数据来产生filter后的数据), + AVERROR_EOF(表示不会再有新的输入数据) + */ +int av_buffersink_get_frame_flags(AVFilterContext *ctx, AVFrame *frame, int flags); +``` + + + +``` +/* + 同 av_buffersink_get_frame_flags ,不过不能指定 flag。 + */ +int av_buffersink_get_frame(AVFilterContext *ctx, AVFrame *frame) +``` + + + +``` +/* + 和 av_buffersink_get_frame 相同,不过这个函数是针对音频的,而且可以指定读取的取样数。此时 ctx 只能指向 abuffersink 类型的 filter context。 +*/ +int av_buffersink_get_samples(AVFilterContext *ctx, AVFrame *frame, int nb_samples); +``` + + + +#### FFmpeg AVFilter 使用整体流程 + +下图就是FFmpeg AVFilter在使用过程中的流程图: + +![img](https://img2020.cnblogs.com/blog/682616/202104/682616-20210415141146493-1433896880.jpg) + +我们对上图先做下说明,理解下图中每个步骤的关系,然后,才从代码的角度来给出其使用的步骤。 + +1. 最顶端的AVFilterGraph,这个结构前面介绍过,主要管理加入的过滤器,其中加入的过滤器就是通过函数avfilter_graph_create_filter来创建并加入,这个函数返回是AVFilterContext(其封装了AVFilter的详细参数信息)。 + +2. buffer和buffersink这两个过滤器是FFMpeg为我们实现好的,buffer表示源,用来向后面的过滤器提供数据输入(其实就是原始的AVFrame);buffersink过滤器是最终输出的(经过过滤器链处理后的数据AVFrame),其它的诸如filter 1 等过滤器是由avfilter_graph_parse_ptr函数解析外部传入的过滤器描述字符串自动生成的,内部也是通过avfilter_graph_create_filter来创建过滤器的。 + +3. 上面的buffer、filter 1、filter 2、filter n、buffersink之间是通过avfilter_link函数来进行关联的(通过AVFilterLink结构),这样子过滤器和过滤器之间就通过AVFilterLink进行关联上了,前一个过滤器的输出就是下一个过滤器的输入,注意,除了源和接收过滤器之外,其它的过滤器至少有一个输入和输出,这很好理解,中间的过滤器处理完AVFrame后,得到新的处理后的AVFrame数据,然后把新的AVFrame数据作为下一个过滤器的输入。 + +4. 过滤器建立完成后,首先我们通过av_buffersrc_add_frame把最原始的AVFrame(没有经过任何过滤器处理的)加入到buffer过滤器的fifo队列。 + +5. 然后调用buffersink过滤器的av_buffersink_get_frame_flags来获取处理完后的数据帧(这个最终放入buffersink过滤器的AVFrame是通过之前创建的一系列过滤器处理后的数据)。 + + 使用流程图就介绍到这里,下面结合上面的使用流程图详细说下FFMpeg中使用过滤器的步骤,这个过程我们分为三个部分:过滤器构建、数据加工、资源释放。 + + + +##### 过滤器构建: + +1)分配AVFilterGraph + +``` +AVFilterGraph* graph = avfilter_graph_alloc(); +``` + + 2)创建过滤器源 + +``` +char srcArgs[256] = {0}; +AVFilterContext *srcFilterCtx; +AVFilter* srcFilter = avfilter_get_by_name("buffer"); +avfilter_graph_create_filter(&srcFilterCtx, srcFilter ,"out_buffer", srcArgs, NULL, graph); +``` + +3)创建接收过滤器 + +``` +AVFilterContext *sinkFilterCtx; +AVFilter* sinkFilter = avfilter_get_by_name("buffersink"); +avfilter_graph_create_filter(&sinkFilterCtx, sinkFilter,"in_buffersink", NULL, NULL, graph); +``` + +4)生成源和接收过滤器的输入输出 + +这里主要是把源和接收过滤器封装给AVFilterInOut结构,使用这个中间结构来把过滤器字符串解析并链接进graph,主要代码如下: + +``` +AVFilterInOut *inputs = avfilter_inout_alloc(); +AVFilterInOut *outputs = avfilter_inout_alloc(); +outputs->name = av_strdup("in"); +outputs->filter_ctx = srcFilterCtx; +outputs->pad_idx = 0; +outputs->next = NULL; +inputs->name = av_strdup("out"); +inputs->filter_ctx = sinkFilterCtx; +inputs->pad_idx = 0; +inputs->next = NULL; +``` + + 这里源对应的AVFilterInOut的name最好定义为in,接收对应的name为out,因为FFMpeg源码里默认会通过这样个name来对默认的输出和输入进行查找。 + +5)通过解析过滤器字符串添加过滤器 + +``` +const *char filtergraph = "[in1]过滤器名称=参数1:参数2[out1]"; +int ret = avfilter_graph_parse_ptr(graph, filtergraph, &inputs, &outputs, NULL); +``` + +这里过滤器是以字符串形式描述的,其格式为:[in]过滤器名称=参数[out],过滤器之间用,或;分割,如果过滤器有多个参数,则参数之间用:分割,其中[in]和[out]分别为过滤器的输入和输出,可以有多个。 + +6)检查过滤器的完整性 + +``` +avfilter_graph_config(graph, NULL); +``` + + + +##### 数据加工 + +1)向源过滤器加入AVFrame + +``` +AVFrame* frame; // 这是解码后获取的数据帧 +int ret = av_buffersrc_add_frame(srcFilterCtx, frame); +``` + +2)从buffersink接收处理后的AVFrame + +``` +int ret = av_buffersink_get_frame_flags(sinkFilterCtx, frame, 0); +``` + + 现在我们就可以使用处理后的AVFrame,比如显示或播放出来。 + + + +##### 资源释放 + +使用结束后,调用avfilter_graph_free(&graph);释放掉AVFilterGraph类型的graph。 + +### FFmpeg 过时 Api 汇总整理 + +在学习和使用FFmpeg的时候,我们经常会去查找很多资料并加以实践,但是目前存在一个问题困扰着不少刚接触音视频的同学,那就是FFmpeg的弃用API如何调整。 +我们知道FFmpeg中所谓的“被声明为已否决”就是因为函数或者结构体属性被标示为attribute_deprecated,很有可能在未来的版本中就删除了。所以我们最好的解决方案就是使用新的被推荐使用的函数、结构体等。 + +下面是相关的API的汇总: + +#### AVStream::codec: 被声明为已否决 + +旧版本: + +``` +if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){ +... +pCodecCtx = pFormatCtx->streams[videoIndex]->codec; +``` + +新版本: + +``` +if(pFormatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO){ +... +pCodecCtx = avcodec_alloc_context3(NULL); +if (pCodecCtx == NULL) +{ + printf("Could not allocate AVCodecContext \n"); + return -1; +} + if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + printf("Couldn't find audio stream information \n"); + return -1; +} +avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoIndex]->codecpar); +``` + +#### avcodec_encode_audio2:被声明为已否决 + +旧版本: + +``` +if (avcodec_encode_audio2(tmpAvCodecCtx, &pkt_out, frame, &got_picture) < 0) +``` + +新版本: + +``` +if(avcodec_send_frame(tmpAvCodecCtx, frame)<0 || (got_picture=avcodec_receive_packet(tmpAvCodecCtx, &pkt_out))<0) +``` + +#### 'avpicture_get_size': 被声明为已否决 + +旧版本: + +``` +avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height) +``` + +新版本: + +``` +#include "libavutil/imgutils.h" +av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1) +``` + +#### 'avpicture_fill': 被声明为已否决 + +``` +旧版本: +avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height); +``` + +新版本: + +``` +av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1); +``` + +#### 'avcodec_decode_video2': 被声明为已否决 + +旧版本: + +``` +ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet); +``` + +新版本: + +``` +if(avcodec_send_packet(pCodecCtx, packet)<0 || (got_picture =avcodec_receive_frame(pCodecCtx, pFrame))<0) {return -1} +``` + +#### ' avcodec_alloc_frame': 被声明为已否决 + +旧版本: + +``` +pFrame = avcodec_alloc_frame(); +``` + +新版本: + +``` +pFrame = av_frame_alloc(); +``` + +#### 'av_free_packet': 被声明为已否决 + +旧版本: + +``` +av_free_packet(packet); +``` + +新版本: + +``` +av_packet_unref(packet); +``` + +#### avcodec_decode_audio4:被声明为已否决 + +旧版本: + +``` +int avcodec_decode_audio4(AVCodecContext *avctx, AVFrame *frame, int *got_frame, const AVPacket *avpkt); +``` + +新版本: + +``` +if(avcodec_send_packet(pCodecCtxOut_Video, &pkt)<0 || (got_frame=avcodec_receive_frame(pCodecCtxOut_Video,picture))<0) {return -1} +``` + +#### avcodec_encode_video2:被声明为已否决 + +旧版本: + +``` +if(avcodec_encode_video2(tmpAvCodecCtx, &pkt, picture, &got_picture)<0) +``` + +新版本: + +``` +if(avcodec_send_frame(tmpAvCodecCtx, picture)<0 || (got_picture=avcodec_receive_packet(tmpAvCodecCtx, &pkt))<0) +``` \ No newline at end of file From a4006aaa8dd549a619c773a3671a960e20a088f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Mon, 13 Dec 2021 22:10:10 +0800 Subject: [PATCH 08/29] =?UTF-8?q?Rename=20Framework=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E7=82=B9=E6=B1=87=E6=80=BB.md=20to=20Docs/Framework=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E7=82=B9=E6=B1=87=E6=80=BB.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...47\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" => "Docs/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" (100%) diff --git "a/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" "b/Docs/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" similarity index 100% rename from "Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" rename to "Docs/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" From a40b6d3d51ad3333abe8f9888e19d0fa0423081c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Mon, 13 Dec 2021 22:10:42 +0800 Subject: [PATCH 09/29] =?UTF-8?q?Rename=20=E9=9F=B3=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E6=B1=87=E6=80=BB.md=20to=20Docs/?= =?UTF-8?q?=E9=9F=B3=E8=A7=86=E9=A2=91=E7=9F=A5=E8=AF=86=E7=82=B9=E6=B1=87?= =?UTF-8?q?=E6=80=BB.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename "\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" => "Docs/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" (97%) diff --git "a/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" "b/Docs/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" similarity index 97% rename from "\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" rename to "Docs/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" index f01e10b..58ccb7c 100644 --- "a/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" +++ "b/Docs/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" @@ -9095,4 +9095,4 @@ if(avcodec_encode_video2(tmpAvCodecCtx, &pkt, picture, &got_picture)<0) ``` if(avcodec_send_frame(tmpAvCodecCtx, picture)<0 || (got_picture=avcodec_receive_packet(tmpAvCodecCtx, &pkt))<0) -``` \ No newline at end of file +``` From 5f7bf34d155fc8adaf3207eeb52eae55c5832d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Mon, 13 Dec 2021 22:12:44 +0800 Subject: [PATCH 10/29] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9e3611d..7f8ce8b 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ 感谢大家的关注与支持,工作上比较忙,很难抽出时间来更新~ -接下来会有一个不算太大也不算太小的更新,应该是在11月左右,主要是针对Framework部分和音视频部分。(另外,之前做整合的时候记得有标注过原作者,之前有好友反馈说没保存下来,又得一个一个加上去~~~ +之前说11月更新的,一不小心拖更了这么久,这次主要是针对Framework部分和音视频部分。(另外,之前做整合的时候记得有标注过原作者,之前有好友反馈说没保存下来,又得一个一个加上去~~~ -总的结构方面还需要再优化一下,然后N久以前就打算再整理一下面试题相关的内容……emm,一直没有大家别怪我~后面计划会给大家制作一份知识体系图…… +目录以及总的结构方面还需要再优化一下,然后N久以前就打算再整理一下面试题相关的内容……emm,一直没有大家别怪我~后面计划会给大家制作一份知识体系图…… -时间跨度可能不会很短,但会尽自己能力去更新维护。感谢大家的关注与支持,同时也感谢向我提出修改建议的朋友们! +时间跨度可能不会很短,但会尽自己能力去更新维护。再次感谢大家的关注与支持,同时也感谢向我提出修改建议的朋友们! Emm……辛辛苦苦种树……确定不来个Star鼓励一下? From f328f25b7044776acfa9a774f10d6326b131cea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Wed, 15 Dec 2021 13:46:03 +0800 Subject: [PATCH 11/29] =?UTF-8?q?Update=20Framework=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E7=82=B9=E6=B1=87=E6=80=BB.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...06\347\202\271\346\261\207\346\200\273.md" | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git "a/Docs/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" "b/Docs/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" index 445651c..b20092e 100644 --- "a/Docs/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" +++ "b/Docs/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" @@ -1,3 +1,199 @@ +- [Handler](#handler) + - [Handler机制实现原理(一)宏观理论分析与Message源码分析](#handler机制实现原理一宏观理论分析与message源码分析) + - [Message:](#message) + - [看一下全局变量:有好多存数据的对象。](#看一下全局变量有好多存数据的对象) + - [Obtain方法:](#obtain方法) + - [recycle():回收当前message到全局池](#recycle回收当前message到全局池) + - [setData:](#setdata) + - [发送消息的一些方法:](#发送消息的一些方法) + - [构造方法:](#构造方法) + - [writeToParcel:](#writetoparcel) + - [Handler机制实现原理(二)MessageQueue的源码分析](#handler机制实现原理二messagequeue的源码分析) + - [消息队列存储原理](#消息队列存储原理) + - [使用JNI实现的native方法](#使用jni实现的native方法) + - [创建与销毁](#创建与销毁) + - [消息入队管理enqueueMessage()方法](#消息入队管理enqueuemessage方法) + - [同步消息拦截器](#同步消息拦截器) + - [队列空闲处理器IdleHandler](#队列空闲处理器idlehandler) + - [消息出队管理next()方法](#消息出队管理next方法) + - [总结](#总结) + - [Handler机制实现原理(三)Looper的源码分析](#handler机制实现原理三looper的源码分析) + - [创建与退出Looper](#创建与退出looper) + - [运行Looper处理消息](#运行looper处理消息) + - [总结](#总结-1) + - [Handler机制实现原理(四)handler的源码分析](#handler机制实现原理四handler的源码分析) + - [初始化](#初始化) + - [发送消息](#发送消息) + - [接收消息](#接收消息) + - [内存泄漏的可能](#内存泄漏的可能) + - [Handler机制实现原理(五)总结](#handler机制实现原理五总结) + - [Message缓存池](#message缓存池) + - [真正的阻塞发生在MessageQueue](#真正的阻塞发生在messagequeue) + - [为什么推荐使用Handler实现线程间通信](#为什么推荐使用handler实现线程间通信) +- [Binder](#binder) + - [Binder原理(一)学习Binder前必须要了解的知识点](#binder原理一学习binder前必须要了解的知识点) + - [Linux和Android的IPC机制种类](#linux和android的ipc机制种类) + - [Linux中的IPC机制种类](#linux中的ipc机制种类) + - [Android中的IPC机制](#android中的ipc机制) + - [Linux和Binder的IPC通信原理](#linux和binder的ipc通信原理) + - [Linux的IPC通信原理](#linux的ipc通信原理) + - [Binder的通信原理](#binder的通信原理) + - [为什么要使用Binder](#为什么要使用binder) + - [为什么要学习Binder?](#为什么要学习binder) + - [Binder原理(二)ServiceManager中的Binder机制](#binder原理二servicemanager中的binder机制) + - [基于Binder通信的C/S架构](#基于binder通信的cs架构) + - [MediaServer的main函数](#mediaserver的main函数) + - [每个进程唯一的ProcessState](#每个进程唯一的processstate) + - [ServiceManager中的Binder机制](#servicemanager中的binder机制) + - [BpBinder和BBinder](#bpbinder和bbinder) + - [解密IServiceManager](#解密iservicemanager) + - [IServiceManager家族](#iservicemanager家族) + - [小结](#小结) + - [Binder原理(三)系统服务的注册过程](#binder原理三系统服务的注册过程) + - [从调用链角度说明MediaPlayerService是如何注册的](#从调用链角度说明mediaplayerservice是如何注册的) + - [writeTransactionData函数分析](#writetransactiondata函数分析) + - [waitForResponse函数分析](#waitforresponse函数分析) + - [小结](#小结-1) + - [从进程角度说明MediaPlayerService是如何注册的](#从进程角度说明mediaplayerservice是如何注册的) + - [总结](#总结-2) + - [Binder原理(四)ServiceManager的启动过程](#binder原理四servicemanager的启动过程) + - [ServiceManager的入口函数](#servicemanager的入口函数) + - [打开binder设备](#打开binder设备) + - [注册成为Binder机制的上下文管理者](#注册成为binder机制的上下文管理者) + - [循环等待和处理client端发来的请求](#循环等待和处理client端发来的请求) + - [总结](#总结-3) + - [Binder原理(五)系统服务的获取过程](#binder原理五系统服务的获取过程) + - [客户端MediaPlayerService请求获取服务](#客户端mediaplayerservice请求获取服务) + - [服务端ServiceManager处理请求](#服务端servicemanager处理请求) + - [总结](#总结-4) + - [Binder原理(六)Java Binder的初始化](#binder原理六java-binder的初始化) + - [Java Binder的JNI注册](#java-binder的jni注册) + - [Binder类的注册](#binder类的注册) + - [BinderInternal类的注册](#binderinternal类的注册) + - [Binder原理(七)Java Binder中系统服务的注册过程](#binder原理七java-binder中系统服务的注册过程) + - [将AMS注册到ServiceManager](#将ams注册到servicemanager) + - [BinderInternal.getContextObject()](#binderinternalgetcontextobject) + - [ServiceManagerNative.asInterface()](#servicemanagernativeasinterface) + - [getIServiceManager().addService()](#getiservicemanageraddservice) + - [引出JavaBBinder](#引出javabbinder) + - [解析JavaBBinder](#解析javabbinder) + - [Java Binder架构](#java-binder架构) +- [Zygote](#zygote) + - [Zygote(一):Android系统的启动过程及Zygote的启动过程](#zygote一android系统的启动过程及zygote的启动过程) + - [init进程](#init进程) + - [属性服务初始化与启动](#属性服务初始化与启动) + - [设置进程信号处理](#设置进程信号处理) + - [解析init配置文件](#解析init配置文件) + - [Zygote进程启动](#zygote进程启动) + - [SystemServer启动过程](#systemserver启动过程) + - [Zygote(二):应用进程的启动过程](#zygote二应用进程的启动过程) + - [Zygote监听客户端请求](#zygote监听客户端请求) + - [AMS发送创建进程请求](#ams发送创建进程请求) + - [Zygote接收信息并创建进程](#zygote接收信息并创建进程) + - [启动Binder线程池](#启动binder线程池) +- [AMS](#ams) + - [AMS源码分析(一)Activity生命周期管理](#ams源码分析一activity生命周期管理) + - [Activity的生命周期](#activity的生命周期) + - [一个Activity从启动到销毁所经历的周期](#一个activity从启动到销毁所经历的周期) + - [从一个Activity启动另一个Activity的生命周期](#从一个activity启动另一个activity的生命周期) + - [源码分析](#源码分析) + - [Binder](#binder-1) + - [IdleHandler](#idlehandler) + - [Activity在AMS中的标识](#activity在ams中的标识) + - [启动过程时序图](#启动过程时序图) + - [从AActivity跳转BActivity的生命周期分析](#从aactivity跳转bactivity的生命周期分析) + - [AActivity#onPause](#aactivityonpause) + - [BActivity#onCreate](#bactivityoncreate) + - [BActivity#onStart](#bactivityonstart) + - [BActivity#onResume](#bactivityonresume) + - [AActivity#onStop](#aactivityonstop) + - [从BActivity返回到AActivity](#从bactivity返回到aactivity) + - [BActivity#onPause](#bactivityonpause) + - [AActivity#onStart、AActivity#onResume](#aactivityonstartaactivityonresume) + - [BActivity#onStop、BActivity#onDestroy](#bactivityonstopbactivityondestroy) + - [AMS源码分析(二)onActivityResult执行过程](#ams源码分析二onactivityresult执行过程) + - [onActivityResult](#onactivityresult) + - [AActivity跳转BAcitivty并从BActivity返回数据](#aactivity跳转bacitivty并从bactivity返回数据) + - [Intent.FLAG_ACTIVITY_FORWARD_RESULT](#intentflag_activity_forward_result) + - [示例:](#示例) + - [AActivity以startActivityForResult方式打开BActivity](#aactivity以startactivityforresult方式打开bactivity) + - [BActivity以普通方式打开CActivity,设置Intent 的Flag Intent.FLAG_ACTIVITY_FORWARD_RESULT](#bactivity以普通方式打开cactivity设置intent-的flag-intentflag_activity_forward_result) + - [源码解析](#源码解析) + - [ActivityResult数据的写入](#activityresult数据的写入) + - [ActivityResult数据的传递](#activityresult数据的传递) + - [Intent.FLAG_ACTIVITY_FORWARD_RESULT的实现](#intentflag_activity_forward_result的实现) + - [AMS源码分析(三)AMS中Activity栈管理详解](#ams源码分析三ams中activity栈管理详解) + - [Activity栈管理相关类](#activity栈管理相关类) + - [ActivityStackSupervisor](#activitystacksupervisor) + - [ActivityDisplay](#activitydisplay) + - [TaskRecord](#taskrecord) + - [ActivityStack](#activitystack) + - [关系图:](#关系图) + - [启动模式](#启动模式) + - [standard](#standard) + - [Intent.FLAG_ACTIVITY_CLEAR_TOP](#intentflag_activity_clear_top) + - [源码分析](#源码分析-1) + - [加入TaskRecord](#加入taskrecord) + - [standard + Intent.FLAG_ACTIVITY_CLEAR_TOP](#standard--intentflag_activity_clear_top) + - [singleTop](#singletop) + - [流程图:](#流程图) + - [原Activity不在栈顶](#原activity不在栈顶) + - [原Activity在栈顶](#原activity在栈顶) + - [singleTop + Intent.FLAG_ACTIVITY_CLEAR_TOP](#singletop--intentflag_activity_clear_top) + - [log](#log) + - [原Activity不在栈顶](#原activity不在栈顶-1) + - [原Activity在栈顶](#原activity在栈顶-1) + - [原Actiivty在栈顶且设置Intent.FLAG_ACTIVITY_CLEAR_TOP](#原actiivty在栈顶且设置intentflag_activity_clear_top) + - [源码分析](#源码分析-2) + - [singleTop + Intent.FLAG_ACTIVITY_CLEAR_TOP](#singletop--intentflag_activity_clear_top-1) + - [singleTask](#singletask) + - [启动流程](#启动流程) + - [场景一](#场景一) + - [场景二](#场景二) + - [场景三](#场景三) + - [singeTask源码分析](#singetask源码分析) + - [singleInstance](#singleinstance) + - [Intent.FLAG_ACTIVITY_NEW_TASK、taskAffinity、新Task的创建](#intentflag_activity_new_tasktaskaffinity新task的创建) + - [Intent.FLAG_ACTIVITY_NEW_TASK的自动设置](#intentflag_activity_new_task的自动设置) + - [taskAffinity的识别](#taskaffinity的识别) + - [是否创建新task的识别](#是否创建新task的识别) + - [总结](#总结-5) +- [PMS](#pms) + - [深入PMS源码(一)—— PMS的启动过程和执行流程](#深入pms源码一-pms的启动过程和执行流程) + - [PMS简介](#pms简介) + - [PMS的启动过程](#pms的启动过程) + - [PMS构造函数](#pms构造函数) + - [PMS的工作过程](#pms的工作过程) + - [解析配置文件package.xml](#解析配置文件packagexml) + - [扫描安装的应用程序](#扫描安装的应用程序) + - [将apk解析数据同步到PMS的属性中](#将apk解析数据同步到pms的属性中) + - [更新配置文件](#更新配置文件) + - [深入PMS源码(二)—— APK的安装和卸载源码分析](#深入pms源码二-apk的安装和卸载源码分析) + - [应用程序安装基础](#应用程序安装基础) + - [PMS中APK安装过程](#pms中apk安装过程) + - [应用程序的卸载过程](#应用程序的卸载过程) + - [深入PMS源码(三)—— PMS中intent-filter的匹配架构](#深入pms源码三-pms中intent-filter的匹配架构) + - [PMS保存IntentFilter](#pms保存intentfilter) + - [IntentFilter的查找匹配](#intentfilter的查找匹配) +- [WMS](#wms) + - [WMS(一):WMS的诞生](#wms一wms的诞生) + - [WMS概述](#wms概述) + - [窗口管理](#窗口管理) + - [窗口动画](#窗口动画) + - [输入系统的中转站](#输入系统的中转站) + - [Surface管理](#surface管理) + - [WMS的诞生](#wms的诞生) + - [WMS(二):WMS的重要成员和Window的添加过程](#wms二wms的重要成员和window的添加过程) + - [WMS的重要成员](#wms的重要成员) + - [Window的添加过程(WMS部分)](#window的添加过程wms部分) + - [addWindow方法part1](#addwindow方法part1) + - [addWindow方法part2](#addwindow方法part2) + - [addWindow方法part3](#addwindow方法part3) + - [addWindow方法总结](#addwindow方法总结) + - [结语](#结语) + - [WMS(三):Window的删除过程](#wms三window的删除过程) + - [Window的删除过程](#window的删除过程)# Handler + # Handler ## Handler机制实现原理(一)宏观理论分析与Message源码分析 From aa8456f202d194918daaad67eb50e821025ab7f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Wed, 15 Dec 2021 13:53:06 +0800 Subject: [PATCH 12/29] =?UTF-8?q?Update=20=E9=9F=B3=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E6=B1=87=E6=80=BB.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...06\347\202\271\346\261\207\346\200\273.md" | 437 ++++++++++++++++++ 1 file changed, 437 insertions(+) diff --git "a/Docs/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" "b/Docs/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" index 58ccb7c..16226b6 100644 --- "a/Docs/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" +++ "b/Docs/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" @@ -1,3 +1,440 @@ +- [Android音视频开发初级入门篇](#android音视频开发初级入门篇) + - [Android 音视频开发(一) : 通过三种方式绘制图片](#android-音视频开发一--通过三种方式绘制图片) + - [ImageView 绘制图片](#imageview-绘制图片) + - [SurfaceView 绘制图片](#surfaceview-绘制图片) + - [自定义 View 绘制图片](#自定义-view-绘制图片) + - [Android 音视频开发(二):使用 AudioRecord 采集音频PCM并保存到文件](#android-音视频开发二使用-audiorecord-采集音频pcm并保存到文件) + - [AudioRecord API详解](#audiorecord-api详解) + - [使用 AudioRecord 实现录音,并生成wav](#使用-audiorecord-实现录音并生成wav) + - [创建一个AudioRecord对象](#创建一个audiorecord对象) + - [初始化一个buffer](#初始化一个buffer) + - [开始录音](#开始录音) + - [创建一个数据流,一边从AudioRecord中读取声音数据到初始化的buffer,一边将buffer中数据导入数据流。](#创建一个数据流一边从audiorecord中读取声音数据到初始化的buffer一边将buffer中数据导入数据流) + - [关闭数据流](#关闭数据流) + - [停止录音](#停止录音) + - [附言](#附言) + - [源码](#源码) + - [Android 音视频开发(三):使用 AudioTrack 播放PCM音频](#android-音视频开发三使用-audiotrack-播放pcm音频) + - [AudioTrack 基本使用](#audiotrack-基本使用) + - [MODE_STATIC模式](#mode_static模式) + - [MODE_STREAM模式](#mode_stream模式) + - [AudioTrack 详解](#audiotrack-详解) + - [音频流的类型](#音频流的类型) + - [**Buffer分配和Frame的概念**](#buffer分配和frame的概念) + - [AudioTrack构造过程](#audiotrack构造过程) + - [AudioTrack 与 MediaPlayer 的对比](#audiotrack-与-mediaplayer-的对比) + - [区别](#区别) + - [联系](#联系) + - [SoundPool](#soundpool) + - [源码](#源码-1) + - [Android 音视频开发(四):使用 Camera API 采集视频数据](#android-音视频开发四使用-camera-api-采集视频数据) + - [预览 Camera 数据](#预览-camera-数据) + - [取到 NV21 的数据回调](#取到-nv21-的数据回调) + - [Android 音视频开发(五):使用 MediaExtractor 和 MediaMuxer API 解析和封装 mp4 文件](#android-音视频开发五使用-mediaextractor-和-mediamuxer-api-解析和封装-mp4-文件) + - [**MediaExtractor API介绍**](#mediaextractor-api介绍) + - [MediaMuxer API介绍](#mediamuxer-api介绍) + - [使用情境](#使用情境) + - [从MP4文件中提取视频并生成新的视频文件](#从mp4文件中提取视频并生成新的视频文件) + - [Android 音视频开发(六): MediaCodec API 详解](#android-音视频开发六-mediacodec-api-详解) + - [MediaCodec 介绍](#mediacodec-介绍) + - [MediaCodec API 说明](#mediacodec-api-说明) + - [MediaCodec 流控](#mediacodec-流控) + - [流控基本概念](#流控基本概念) + - [Android 硬编码流控](#android-硬编码流控) + - [Android 流控策略选择](#android-流控策略选择) + - [Android 音视频开发(七): 音视频录制流程总结](#android-音视频开发七-音视频录制流程总结) + - [流程分析](#流程分析) + - [需求说明](#需求说明) + - [实现方式](#实现方式) + - [数据处理思路](#数据处理思路) + - [实现过程](#实现过程) + - [收集Camera数据,并转码为H264存储到文件](#收集camera数据并转码为h264存储到文件) + - [音视频采集+混合,存储到文件](#音视频采集混合存储到文件) +- [Android音视频开发中级进阶篇](#android音视频开发中级进阶篇) + - [OpenGL ES](#opengl-es) + - [Android OpenGL ES 开发(一): OpenGL ES 介绍](#android-opengl-es-开发一-opengl-es-介绍) + - [简介OpenGL ES](#简介opengl-es) + - [基本介绍](#基本介绍) + - [GLSurfaceView](#glsurfaceview) + - [GLSurfaceView.Renderer](#glsurfaceviewrenderer) + - [Android OpenGL ES开发(二) : OpenGL ES 环境搭建](#android-opengl-es开发二--opengl-es-环境搭建) + - [环境搭建目的](#环境搭建目的) + - [在Manifest中声明OpenGL ES使用](#在manifest中声明opengl-es使用) + - [创建一个Activity 用于展示OpenGL ES 图形](#创建一个activity-用于展示opengl-es-图形) + - [创建GLSurfaceView对象](#创建glsurfaceview对象) + - [创建一个Renderer类](#创建一个renderer类) + - [总结](#总结) + - [Android OpenGL ES 开发(三):OpenGL ES定义形状](#android-opengl-es-开发三opengl-es定义形状) + - [定义三角形](#定义三角形) + - [定义正方形](#定义正方形) + - [Android OpenGL ES开发(四) : OpenGL ES绘制形状](#android-opengl-es开发四--opengl-es绘制形状) + - [初始化形状](#初始化形状) + - [绘制形状](#绘制形状) + - [Android OpenGL ES开发(五) : OpenGL ES使用投影和相机视图](#android-opengl-es开发五--opengl-es使用投影和相机视图) + - [定义投影](#定义投影) + - [定义相机视图](#定义相机视图) + - [应用投影和相机变换](#应用投影和相机变换) + - [Android openGL ES开发(六): OpenGL ES添加运动效果](#android-opengl-es开发六-opengl-es添加运动效果) + - [旋转一个图形](#旋转一个图形) + - [允许连续渲染](#允许连续渲染) + - [Android openGL ES开发(七) : OpenGL ES 响应触摸事件](#android-opengl-es开发七--opengl-es-响应触摸事件) + - [设置触摸事件](#设置触摸事件) + - [暴露旋转角度](#暴露旋转角度) + - [应用旋转](#应用旋转) + - [Android OpenGL ES开发(八) :OpenGL ES 着色器语言GLSL](#android-opengl-es开发八-opengl-es-着色器语言glsl) + - [GLSL 简介](#glsl-简介) + - [GLSL 基础](#glsl-基础) + - [基本数据类型](#基本数据类型) + - [标量:](#标量) + - [向量:](#向量) + - [矩阵:](#矩阵) + - [采样器:](#采样器) + - [结构体:](#结构体) + - [数组:](#数组) + - [空类型:](#空类型) + - [运算符](#运算符) + - [类型转换](#类型转换) + - [限定符](#限定符) + - [流程控制](#流程控制) + - [函数](#函数) + - [浮点精度](#浮点精度) + - [程序结构](#程序结构) + - [GLSL 内建变量](#glsl-内建变量) + - [顶点着色器的内建变量](#顶点着色器的内建变量) + - [片元着色器的内建变量](#片元着色器的内建变量) + - [常用内置函数](#常用内置函数) + - [常见函数](#常见函数) + - [几何函数](#几何函数) + - [矩阵函数](#矩阵函数) + - [纹理采样函数](#纹理采样函数) + - [Android OpenGL ES开发(九): OpenGL ES纹理贴图](#android-opengl-es开发九-opengl-es纹理贴图) + - [概念](#概念) + - [原理](#原理) + - [显示纹理图片](#显示纹理图片) + - [修改着色器](#修改着色器) + - [设置顶点坐标和纹理坐标](#设置顶点坐标和纹理坐标) + - [计算变换矩阵](#计算变换矩阵) + - [显示图片](#显示图片) + - [Android OpenGL ES 开发(十):通过GLES20与着色器交互](#android-opengl-es-开发十通过gles20与着色器交互) + - [获取着色器程序内成员变量的id(句柄、指针)](#获取着色器程序内成员变量的id句柄指针) + - [向着色器传递数据](#向着色器传递数据) + - [定义顶点属性数组](#定义顶点属性数组) + - [启用或者禁用顶点属性数组](#启用或者禁用顶点属性数组) + - [选择活动纹理单元。](#选择活动纹理单元) + - [OpenSL ES](#opensl-es) + - [Android OpenSL ES 开发:Android OpenSL 介绍和开发流程说明](#android-opensl-es-开发android-opensl-介绍和开发流程说明) + - [Android OpenSL ES 介绍](#android-opensl-es-介绍) + - [Android OpenSL ES 开发流程](#android-opensl-es-开发流程) + - [OpenSL ES 开发最重要的接口类 SLObjectItf](#opensl-es-开发最重要的接口类-slobjectitf) + - [SLObjectItf 创建的具体的接口对象实例](#slobjectitf-创建的具体的接口对象实例) + - [创建引擎并实现](#创建引擎并实现) + - [利用引擎对象创建其他接口对象](#利用引擎对象创建其他接口对象) + - [OpenSL ES 使用示例](#opensl-es-使用示例) + - [播放assets文件](#播放assets文件) + - [播放pcm文件](#播放pcm文件) + - [创建播放器和混音器](#创建播放器和混音器) + - [设置pcm格式的频率位数等信息并创建播放器](#设置pcm格式的频率位数等信息并创建播放器) + - [设置缓冲队列和回调函数](#设置缓冲队列和回调函数) + - [设置播放状态并手动开始调用回调函数](#设置播放状态并手动开始调用回调函数) + - [Android OpenSL ES 开发:使用 OpenSL 播放 PCM 数据](#android-opensl-es-开发使用-opensl-播放-pcm-数据) + - [创建声音引擎](#创建声音引擎) + - [创建声音播放器](#创建声音播放器) + - [设置播放缓冲](#设置播放缓冲) + - [获得其他接口用来控制播放](#获得其他接口用来控制播放) + - [提供播放数据](#提供播放数据) + - [播放音乐](#播放音乐) + - [调解音量](#调解音量) + - [释放资源](#释放资源) + - [参考源码](#参考源码) + - [Android OpenSL ES 开发:Android OpenSL 录制 PCM 音频数据](#android-opensl-es-开发android-opensl-录制-pcm-音频数据) + - [实现说明](#实现说明) + - [编码实现](#编码实现) + - [编写缓存buffer队列:RecordBuffer.h、RecordBuffer.cpp](#编写缓存buffer队列recordbufferhrecordbuffercpp) + - [使用OpenSL ES录制PCM数据](#使用opensl-es录制pcm数据) + - [验证录制成果](#验证录制成果) + - [参考源码](#参考源码-1) + - [Android OpenSL ES 开发:OpenSL ES利用SoundTouch实现PCM音频的变速和变调](#android-opensl-es-开发opensl-es利用soundtouch实现pcm音频的变速和变调) + - [缘由](#缘由) + - [实现](#实现) + - [移植SoundTouch(Android)](#移植soundtouchandroid) + - [用SoundTouch转码PCM源文件](#用soundtouch转码pcm源文件) + - [SoundTouch使用流程](#soundtouch使用流程) + - [添加命名空间,并创建SoundTouch指针变量](#添加命名空间并创建soundtouch指针变量) + - [设置SoundTouch参数](#设置soundtouch参数) + - [向SoundTouch中传入获取到的PCM数据,使用:putSamples函数](#向soundtouch中传入获取到的pcm数据使用putsamples函数) + - [获取SoundTouch输出的PCM数据:使用receiveSamples函数](#获取soundtouch输出的pcm数据使用receivesamples函数) + - [OpenSL ES播放SoundTouch处理后的PCM音频数据](#opensl-es播放soundtouch处理后的pcm音频数据) + - [思维发散](#思维发散) + - [FFmpeg解码得到的PCM数据(uint_8 *)利用SoundTouch转码](#ffmpeg解码得到的pcm数据uint_8-利用soundtouch转码) + - [总结](#总结-1) + - [参考源码](#参考源码-2) +- [Android音视频开发高级探究篇](#android音视频开发高级探究篇) + - [音视频编解码技术](#音视频编解码技术) + - [音视频编解码技术(一):MPEG-4/H.264 AVC 编解码标准](#音视频编解码技术一mpeg-4h264-avc-编解码标准) + - [H264 概述](#h264-概述) + - [H.264视频编解码的意义](#h264视频编解码的意义) + - [H.264编解码的理论依据](#h264编解码的理论依据) + - [H.264相关概念](#h264相关概念) + - [H.264 的基本单位](#h264-的基本单位) + - [帧类型](#帧类型) + - [GOP(画面组)](#gop画面组) + - [IDR 帧](#idr-帧) + - [H.264 压缩方式](#h264-压缩方式) + - [H.264 压缩算法](#h264-压缩算法) + - [H.264压缩方式说明](#h264压缩方式说明) + - [H.264 分层结构](#h264-分层结构) + - [VLC层(Video Coding Layer)](#vlc层video-coding-layer) + - [NAL层(Network Abstraction Layer)](#nal层network-abstraction-layer) + - [NALU (NAL Unit)](#nalu-nal-unit) + - [Start Code](#start-code) + - [NAL Header](#nal-header) + - [RBSP(Raw Byte Sequence Payload))](#rbspraw-byte-sequence-payload) + - [H.264 局限性](#h264-局限性) + - [音视频编解码技术(二):AAC 音频编码技术](#音视频编解码技术二aac-音频编码技术) + - [AAC编码概述](#aac编码概述) + - [AAC编码规格简述](#aac编码规格简述) + - [AAC编码的特点](#aac编码的特点) + - [AAC音频文件格式](#aac音频文件格式) + - [ACC 音频文件格式类型](#acc-音频文件格式类型) + - [ADIF 的 Header 结构](#adif-的-header-结构) + - [ADTS 的 Header 头结构](#adts-的-header-头结构) + - [AAC文件处理流程](#aac文件处理流程) + - [开源AAC解码器](#开源aac解码器) + - [流媒体协议](#流媒体协议) + - [流媒体协议(一):HLS 协议](#流媒体协议一hls-协议) + - [HLS 概述](#hls-概述) + - [原理介绍:](#原理介绍) + - [整体架构](#整体架构) + - [HLS 播放](#hls-播放) + - [播放未加密HLS](#播放未加密hls) + - [播放加密HLS](#播放加密hls) + - [HLS 协议总结](#hls-协议总结) + - [优点:](#优点) + - [缺点:](#缺点) + - [改进](#改进) + - [流媒体协议(二):RTMP协议](#流媒体协议二rtmp协议) + - [概念与摘要](#概念与摘要) + - [RTMP块流](#rtmp块流) + - [消息格式](#消息格式) + - [握手](#握手) + - [握手序列](#握手序列) + - [C0和S0格式](#c0和s0格式) + - [C1和S1格式](#c1和s1格式) + - [C2和S2格式](#c2和s2格式) + - [RMTP握手](#rmtp握手) + - [握手过程示意图](#握手过程示意图) + - [多媒体文件格式](#多媒体文件格式) + - [多媒体文件格式(一):MP4 格式](#多媒体文件格式一mp4-格式) + - [MP4 格式标准介绍](#mp4-格式标准介绍) + - [MP4分析工具](#mp4分析工具) + - [Elecard StreamEye](#elecard-streameye) + - [mp4box](#mp4box) + - [mp4info](#mp4info) + - [MP4格式重要Box](#mp4格式重要box) + - [ftyp(**File Type Box**)](#ftypfile-type-box) + - [moov**(Movie Box)**](#moovmovie-box) + - [trak(Track Box)](#traktrack-box) + - [mdat(Meida Data Box)](#mdatmeida-data-box) + - [free或skip(Free Space Box)](#free或skipfree-space-box) + - [stbl(Sample Table Box)](#stblsample-table-box) + - [MP4格式 与 FFmpeg实战](#mp4格式-与-ffmpeg实战) + - [在FFmpeg中的输出MP4的Demuxer信息](#在ffmpeg中的输出mp4的demuxer信息) + - [通过FFmepg faststart参数的使用,来理解mdat和moov的顺序的意义](#通过ffmepg-faststart参数的使用来理解mdat和moov的顺序的意义) + - [多媒体文件格式(二):FLV 格式](#多媒体文件格式二flv-格式) + - [FLV 格式标准介绍](#flv-格式标准介绍) + - [文件头 Header](#文件头-header) + - [文件体 FLV Body](#文件体-flv-body) + - [Tag](#tag) + - [FLV 分析工具](#flv-分析工具) + - [FlvAnalyzer](#flvanalyzer) + - [FLV Format Analysis 工具](#flv-format-analysis-工具) + - [FLV格式 与 FFmpeg 实战](#flv格式-与-ffmpeg-实战) + - [使用FFmpeg生成带关键索引信息的FLV](#使用ffmpeg生成带关键索引信息的flv) + - [使用ffprobe查看FLV关键帧索引相关信息](#使用ffprobe查看flv关键帧索引相关信息) + - [多媒体文件格式(三):M3U8 格式](#多媒体文件格式三m3u8-格式) + - [M3U8 格式标准介绍](#m3u8-格式标准介绍) + - [HLS 与 M3U8](#hls-与-m3u8) + - [FFmpeg转HLS文件(M3U8)实战](#ffmpeg转hls文件m3u8实战) + - [FFmpeg转MP4为HLS(M3U8)文件](#ffmpeg转mp4为hlsm3u8文件) + - [FFmpeg 转 HLS (M3U8) 文件命令参数](#ffmpeg-转-hls-m3u8-文件命令参数) + - [start_number 参数](#start_number-参数) + - [hls_time 参数](#hls_time-参数) + - [hls_list_size 参数](#hls_list_size-参数) + - [hls_base_url参数](#hls_base_url参数) + - [多媒体文件格式(四):TS 格式](#多媒体文件格式四ts-格式) + - [TS 格式标准介绍](#ts-格式标准介绍) + - [TS 格式详解](#ts-格式详解) + - [TS层](#ts层) + - [TS Header](#ts-header) + - [TS Adaptation Field](#ts-adaptation-field) + - [TS Payload](#ts-payload) + - [PES层 & ES 层](#pes层--es-层) + - [PES层](#pes层) + - [ES 层](#es-层) + - [TS流生成及解析流程](#ts流生成及解析流程) + - [TS 流生成流程](#ts-流生成流程) + - [TS 流解析流程](#ts-流解析流程) + - [多媒体文件格式(五):PCM / WAV 格式](#多媒体文件格式五pcm--wav-格式) + - [名词解析](#名词解析) + - [采样率](#采样率) + - [位深度](#位深度) + - [PCM](#pcm) + - [PCM音频数据存储方式](#pcm音频数据存储方式) + - [PCM 音频数据的参数](#pcm-音频数据的参数) + - [WAV](#wav) + - [PCM & WAV 开发实践](#pcm--wav-开发实践) + - [PCM格式转为WAV格式(基于C语言)](#pcm格式转为wav格式基于c语言) + - [PCM降低某个声道的音量(基于C语言)](#pcm降低某个声道的音量基于c语言) + - [分离PCM音频数据左右声道的数据](#分离pcm音频数据左右声道的数据) + - [从PCM16LE单声道音频采样数据中截取一部分数据](#从pcm16le单声道音频采样数据中截取一部分数据) + - [将PCM16LE双声道音频采样数据转换为PCM8音频采样数据](#将pcm16le双声道音频采样数据转换为pcm8音频采样数据) + - [将PCM16LE双声道音频采样数据的声音速度提高一倍](#将pcm16le双声道音频采样数据的声音速度提高一倍) + - [FFmpeg](#ffmpeg) + - [FFmpeg命令行工具(一):查看媒体文件头信息工具ffprobe](#ffmpeg命令行工具一查看媒体文件头信息工具ffprobe) + - [简述](#简述) + - [命令格式](#命令格式) + - [使用ffprobe查看mp3格式的文件](#使用ffprobe查看mp3格式的文件) + - [使用ffprobe查看mp4格式的文件](#使用ffprobe查看mp4格式的文件) + - [ffprobe高级使用方式](#ffprobe高级使用方式) + - [FFmpeg命令行工具(二):播放媒体文件的工具ffplay](#ffmpeg命令行工具二播放媒体文件的工具ffplay) + - [简述](#简述-1) + - [命令格式](#命令格式-1) + - [主要选项](#主要选项) + - [一些高级选项](#一些高级选项) + - [一些快捷键](#一些快捷键) + - [ffplay 播放音频](#ffplay-播放音频) + - [ffplay 播放视频](#ffplay-播放视频) + - [ffplay 高级使用方式](#ffplay-高级使用方式) + - [循环播放](#循环播放) + - [播放 pm.mp4 ,播放完成后自动退出](#播放-pmmp4-播放完成后自动退出) + - [以 320 x 240 的大小播放 test.mp4](#以-320-x-240-的大小播放-testmp4) + - [将窗口标题设置为 "myplayer",循环播放 2 次](#将窗口标题设置为-myplayer循环播放-2-次) + - [播放 双通道 32K 的 PCM 音频数据](#播放-双通道-32k-的-pcm-音频数据) + - [ffplay音画同步](#ffplay音画同步) + - [FFmpeg命令行工具(三):媒体文件转换工具ffmpeg](#ffmpeg命令行工具三媒体文件转换工具ffmpeg) + - [简述](#简述-2) + - [命令行参数](#命令行参数) + - [通用参数](#通用参数) + - [视频参数](#视频参数) + - [音频参数](#音频参数) + - [实践学习](#实践学习) + - [列出ffmpeg支持的所有格式](#列出ffmpeg支持的所有格式) + - [剪切一段媒体文件,可以是音频或者视频文件](#剪切一段媒体文件可以是音频或者视频文件) + - [提取视频文件中的音频数据,并保存为文件](#提取视频文件中的音频数据并保存为文件) + - [将视频中的音频静音,只保留视频](#将视频中的音频静音只保留视频) + - [从mp4文件中抽取视频流导出为裸H264数据:](#从mp4文件中抽取视频流导出为裸h264数据) + - [将视频推送到流媒体服务器上:](#将视频推送到流媒体服务器上) + - [将流媒体服务器上的流dump到本地:](#将流媒体服务器上的流dump到本地) + - [给视频添加水印](#给视频添加水印) + - [倒放音视频](#倒放音视频) + - [将几个MP4视频文件合并为1个视频.](#将几个mp4视频文件合并为1个视频) + - [FFmpeg命令行工具(四):FFmpeg 调整音视频播放速度](#ffmpeg命令行工具四ffmpeg-调整音视频播放速度) + - [调整视频速率](#调整视频速率) + - [调整音频速率](#调整音频速率) + - [参考文献](#参考文献) + - [FFmpeg(一):FFmpeg 简介](#ffmpeg一ffmpeg-简介) + - [FFmpeg 介绍](#ffmpeg-介绍) + - [FFmpeg 组成](#ffmpeg-组成) + - [FFmpeg包含类库说明](#ffmpeg包含类库说明) + - [类库说明](#类库说明) + - [常用结构](#常用结构) + - [封装格式](#封装格式) + - [编解码](#编解码) + - [网络协议](#网络协议) + - [数据存放](#数据存放) + - [FFmpeg(二):Mac下安装FFmpeg](#ffmpeg二mac下安装ffmpeg) + - [安装ffmpeg](#安装ffmpeg) + - [命令行安装](#命令行安装) + - [下载压缩包安装](#下载压缩包安装) + - [安装ffplay](#安装ffplay) + - [命令行安装](#命令行安装-1) + - [下载压缩包安装](#下载压缩包安装-1) + - [附言](#附言-1) + - [FFmpeg(三):将 FFmpeg 移植到 Android平台](#ffmpeg三将-ffmpeg-移植到-android平台) + - [FFmpeg(四):FFmpeg API 介绍与通用 API 分析](#ffmpeg四ffmpeg-api-介绍与通用-api-分析) + - [FFmpeg 编解码流程](#ffmpeg-编解码流程) + - [FFmpeg 相关术语](#ffmpeg-相关术语) + - [FFmpeg 通用 API 分析](#ffmpeg-通用-api-分析) + - [av_register_all 分析](#av_register_all-分析) + - [av_find_codec 分析](#av_find_codec-分析) + - [avcodec_open2 分析](#avcodec_open2-分析) + - [avcodec_close 分析](#avcodec_close-分析) + - [总结](#总结-2) + - [FFmpeg(五):FFmpeg 编解码 API 分析](#ffmpeg五ffmpeg-编解码-api-分析) + - [FFmpeg 解码 API 分析](#ffmpeg-解码-api-分析) + - [avformat_open_input 分析](#avformat_open_input-分析) + - [avformat_find_stream_info 分析](#avformat_find_stream_info-分析) + - [av_read_frame 分析](#av_read_frame-分析) + - [avcodec_decode分析](#avcodec_decode分析) + - [avformat_close_input 分析](#avformat_close_input-分析) + - [FFmpeg 编码 API 分析](#ffmpeg-编码-api-分析) + - [avformat_alloc_output_context2 分析](#avformat_alloc_output_context2-分析) + - [avio_open2 分析](#avio_open2-分析) + - [编码其他API(步骤)分析](#编码其他api步骤分析) + - [FFmpeg 解码 API 超时设置](#ffmpeg-解码-api-超时设置) + - [设置开流的超时时间](#设置开流的超时时间) + - [设置interrupt_callback定义返回机制](#设置interrupt_callback定义返回机制) + - [FFmpeg(六):FFmpeg 核心模块 libavformat 与 libavcodec 分析](#ffmpeg六ffmpeg-核心模块-libavformat-与-libavcodec-分析) + - [libavformat介绍](#libavformat介绍) + - [libavcodec介绍](#libavcodec介绍) + - [FFmpeg 结构体(一): AVFormatContext 分析](#ffmpeg-结构体一-avformatcontext-分析) + - [源码整理](#源码整理) + - [AVForamtContext 重点字段](#avforamtcontext-重点字段) + - [FFmpeg 结构体(二): AVStream 分析](#ffmpeg-结构体二-avstream-分析) + - [源码整理](#源码整理-1) + - [AVStream 重点字段](#avstream-重点字段) + - [FFmpeg 结构体(三): AVPacket 分析](#ffmpeg-结构体三-avpacket-分析) + - [源码整理](#源码整理-2) + - [AVPacket 重点字段](#avpacket-重点字段) + - [FFmpeg 结构体(四): AVFrame 分析](#ffmpeg-结构体四-avframe-分析) + - [源码整理](#源码整理-3) + - [AVFrame 重点字段](#avframe-重点字段) + - [data[]](#data) + - [pict_type](#pict_type) + - [sample_aspect_ratio](#sample_aspect_ratio) + - [qscale_table](#qscale_table) + - [FFmpeg 结构体(五): AVCodec 分析](#ffmpeg-结构体五-avcodec-分析) + - [源码整理](#源码整理-4) + - [AVCodec 重点字段](#avcodec-重点字段) + - [enum AVMediaType type](#enum-avmediatype-type) + - [enum AVCodecID id](#enum-avcodecid-id) + - [const enum AVPixelFormat *pix_fmts](#const-enum-avpixelformat-pix_fmts) + - [const enum AVSampleFormat *sample_fmts](#const-enum-avsampleformat-sample_fmts) + - [FFmpeg 结构体(六): AVCodecContext 分析](#ffmpeg-结构体六-avcodeccontext-分析) + - [源码整理](#源码整理-5) + - [AVCodecContext 重点字段](#avcodeccontext-重点字段) + - [codec_type](#codec_type) + - [sample_fmt](#sample_fmt) + - [profile](#profile) + - [FFmpeg 结构体(七): AVIOContext 分析](#ffmpeg-结构体七-aviocontext-分析) + - [源码整理](#源码整理-6) + - [AVIOContext 重点字段](#aviocontext-重点字段) + - [FFmpeg 结构体(八):FFMPEG中重要结构体之间的关系](#ffmpeg-结构体八ffmpeg中重要结构体之间的关系) + - [解协议(http,rtsp,rtmp,mms)](#解协议httprtsprtmpmms) + - [解封装(flv,avi,rmvb,mp4)](#解封装flvavirmvbmp4) + - [解码(h264,mpeg2,aac,mp3)](#解码h264mpeg2aacmp3) + - [存数据](#存数据) + - [FFmpeg 开发之 AVFilter 使用流程总结](#ffmpeg-开发之-avfilter-使用流程总结) + - [AVFilterGraph 、AVFilterContext、AVFilter](#avfiltergraph-avfiltercontextavfilter) + - [AVFilter 相关Api使用方法整理](#avfilter-相关api使用方法整理) + - [AVFilterContext 初始化方法](#avfiltercontext-初始化方法) + - [AVFilterGraph 相关的Api](#avfiltergraph-相关的api) + - [FFmpeg Filter Buffer 和 BufferSink 相关APi的使用方法整理](#ffmpeg-filter-buffer-和-buffersink-相关api的使用方法整理) + - [FFmpeg AVFilter 使用整体流程](#ffmpeg-avfilter-使用整体流程) + - [过滤器构建:](#过滤器构建) + - [数据加工](#数据加工) + - [资源释放](#资源释放) + - [FFmpeg 过时 Api 汇总整理](#ffmpeg-过时-api-汇总整理) + - [AVStream::codec: 被声明为已否决](#avstreamcodec-被声明为已否决) + - [avcodec_encode_audio2:被声明为已否决](#avcodec_encode_audio2被声明为已否决) + - ['avpicture_get_size': 被声明为已否决](#avpicture_get_size-被声明为已否决) + - ['avpicture_fill': 被声明为已否决](#avpicture_fill-被声明为已否决) + - ['avcodec_decode_video2': 被声明为已否决](#avcodec_decode_video2-被声明为已否决) + - [' avcodec_alloc_frame': 被声明为已否决](#-avcodec_alloc_frame-被声明为已否决) + - ['av_free_packet': 被声明为已否决](#av_free_packet-被声明为已否决) + - [avcodec_decode_audio4:被声明为已否决](#avcodec_decode_audio4被声明为已否决) + - [avcodec_encode_video2:被声明为已否决](#avcodec_encode_video2被声明为已否决) + # Android音视频开发初级入门篇 ## Android 音视频开发(一) : 通过三种方式绘制图片 From 7be36900561a47ac7a9ce1ededea8eca9eec3fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Thu, 16 Dec 2021 14:04:52 +0800 Subject: [PATCH 13/29] Update README.md --- README.md | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) diff --git a/README.md b/README.md index 7f8ce8b..662fbe6 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,204 @@ Android开发核心知识点笔记-目录: * [类加载器](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Android扩展知识点.md#类加载器) * [双亲委托模式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Android扩展知识点.md#双亲委托模式) * [DexPathList](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Android扩展知识点.md#dexpathlist) + +## Framework 知识点汇总 + + * [Handler](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#handler) + * [Handler机制实现原理(一)宏观理论分析与Message源码分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#handler机制实现原理一宏观理论分析与message源码分析) + * [Message:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#message) + * [看一下全局变量:有好多存数据的对象。](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#看一下全局变量有好多存数据的对象) + * [Obtain方法:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#obtain方法) + * [recycle():回收当前message到全局池](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#recycle回收当前message到全局池) + * [setData:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#setdata) + * [发送消息的一些方法:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#发送消息的一些方法) + * [构造方法:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#构造方法) + * [writeToParcel:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#writetoparcel) + * [Handler机制实现原理(二)MessageQueue的源码分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#handler机制实现原理二messagequeue的源码分析) + * [消息队列存储原理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#消息队列存储原理) + * [使用JNI实现的native方法](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#使用jni实现的native方法) + * [创建与销毁](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#创建与销毁) + * [消息入队管理enqueueMessage()方法](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#消息入队管理enqueuemessage方法) + * [同步消息拦截器](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#同步消息拦截器) + * [队列空闲处理器IdleHandler](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#队列空闲处理器idlehandler) + * [消息出队管理next()方法](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#消息出队管理next方法) + * [总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#总结) + * [Handler机制实现原理(三)Looper的源码分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#handler机制实现原理三looper的源码分析) + * [创建与退出Looper](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#创建与退出looper) + * [运行Looper处理消息](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#运行looper处理消息) + * [总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#总结-1) + * [Handler机制实现原理(四)handler的源码分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#handler机制实现原理四handler的源码分析) + * [初始化](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#初始化) + * [发送消息](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#发送消息) + * [接收消息](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#接收消息) + * [内存泄漏的可能](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#内存泄漏的可能) + * [Handler机制实现原理(五)总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#handler机制实现原理五总结) + * [Message缓存池](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#message缓存池) + * [真正的阻塞发生在MessageQueue](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#真正的阻塞发生在messagequeue) + * [为什么推荐使用Handler实现线程间通信](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#为什么推荐使用handler实现线程间通信) + * [Binder](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#binder) + * [Binder原理(一)学习Binder前必须要了解的知识点](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#binder原理一学习binder前必须要了解的知识点) + * [Linux和Android的IPC机制种类](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#linux和android的ipc机制种类) + * [Linux中的IPC机制种类](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#linux中的ipc机制种类) + * [Android中的IPC机制](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#android中的ipc机制) + * [Linux和Binder的IPC通信原理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#linux和binder的ipc通信原理) + * [Linux的IPC通信原理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#linux的ipc通信原理) + * [Binder的通信原理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#binder的通信原理) + * [为什么要使用Binder](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#为什么要使用binder) + * [为什么要学习Binder?](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#为什么要学习binder) + * [Binder原理(二)ServiceManager中的Binder机制](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#binder原理二servicemanager中的binder机制) + * [基于Binder通信的C/S架构](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#基于binder通信的cs架构) + * [MediaServer的main函数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#mediaserver的main函数) + * [每个进程唯一的ProcessState](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#每个进程唯一的processstate) + * [ServiceManager中的Binder机制](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#servicemanager中的binder机制) + * [BpBinder和BBinder](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bpbinder和bbinder) + * [解密IServiceManager](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#解密iservicemanager) + * [IServiceManager家族](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#iservicemanager家族) + * [小结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#小结) + * [Binder原理(三)系统服务的注册过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#binder原理三系统服务的注册过程) + * [从调用链角度说明MediaPlayerService是如何注册的](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#从调用链角度说明mediaplayerservice是如何注册的) + * [writeTransactionData函数分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#writetransactiondata函数分析) + * [waitForResponse函数分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#waitforresponse函数分析) + * [小结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#小结-1) + * [从进程角度说明MediaPlayerService是如何注册的](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#从进程角度说明mediaplayerservice是如何注册的) + * [总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#总结-2) + * [Binder原理(四)ServiceManager的启动过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#binder原理四servicemanager的启动过程) + * [ServiceManager的入口函数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#servicemanager的入口函数) + * [打开binder设备](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#打开binder设备) + * [注册成为Binder机制的上下文管理者](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#注册成为binder机制的上下文管理者) + * [循环等待和处理client端发来的请求](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#循环等待和处理client端发来的请求) + * [总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#总结-3) + * [Binder原理(五)系统服务的获取过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#binder原理五系统服务的获取过程) + * [客户端MediaPlayerService请求获取服务](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#客户端mediaplayerservice请求获取服务) + * [服务端ServiceManager处理请求](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#服务端servicemanager处理请求) + * [总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#总结-4) + * [Binder原理(六)Java Binder的初始化](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#binder原理六java-binder的初始化) + * [Java Binder的JNI注册](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#java-binder的jni注册) + * [Binder类的注册](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#binder类的注册) + * [BinderInternal类的注册](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#binderinternal类的注册) + * [Binder原理(七)Java Binder中系统服务的注册过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#binder原理七java-binder中系统服务的注册过程) + * [将AMS注册到ServiceManager](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#将ams注册到servicemanager) + * [BinderInternal.getContextObject()](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#binderinternalgetcontextobject) + * [ServiceManagerNative.asInterface()](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#servicemanagernativeasinterface) + * [getIServiceManager().addService()](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#getiservicemanageraddservice) + * [引出JavaBBinder](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#引出javabbinder) + * [解析JavaBBinder](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#解析javabbinder) + * [Java Binder架构](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#java-binder架构) + * [Zygote](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#zygote) + * [Zygote(一):Android系统的启动过程及Zygote的启动过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#zygote一android系统的启动过程及zygote的启动过程) + * [init进程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#init进程) + * [属性服务初始化与启动](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#属性服务初始化与启动) + * [设置进程信号处理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#设置进程信号处理) + * [解析init配置文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#解析init配置文件) + * [Zygote进程启动](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#zygote进程启动) + * [SystemServer启动过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#systemserver启动过程) + * [Zygote(二):应用进程的启动过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#zygote二应用进程的启动过程) + * [Zygote监听客户端请求](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#zygote监听客户端请求) + * [AMS发送创建进程请求](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#ams发送创建进程请求) + * [Zygote接收信息并创建进程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#zygote接收信息并创建进程) + * [启动Binder线程池](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#启动binder线程池) + * [AMS](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#ams) + * [AMS源码分析(一)Activity生命周期管理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#ams源码分析一activity生命周期管理) + * [Activity的生命周期](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#activity的生命周期) + * [一个Activity从启动到销毁所经历的周期](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#一个activity从启动到销毁所经历的周期) + * [从一个Activity启动另一个Activity的生命周期](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#从一个activity启动另一个activity的生命周期) + * [源码分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#源码分析) + * [Binder](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#binder-1) + * [IdleHandler](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#idlehandler) + * [Activity在AMS中的标识](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#activity在ams中的标识) + * [启动过程时序图](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#启动过程时序图) + * [从AActivity跳转BActivity的生命周期分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#从aactivity跳转bactivity的生命周期分析) + * [AActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onPause](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivityonpause) + * [BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onCreate](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityoncreate) + * [BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onStart](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonstart) + * [BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onResume](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonresume) + * [AActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onStop](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivityonstop) + * [从BActivity返回到AActivity](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#从bactivity返回到aactivity) + * [BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onPause](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonpause) + * [AActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onStart、AActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onResume](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivityonstartaactivityonresume) + * [BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onStop、BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onDestroy](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonstopbactivityondestroy) + * [AMS源码分析(二)onActivityResult执行过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#ams源码分析二onactivityresult执行过程) + * [onActivityResult](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onactivityresult) + * [AActivity跳转BAcitivty并从BActivity返回数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivity跳转bacitivty并从bactivity返回数据) + * [Intent.FLAG_ACTIVITY_FORWARD_RESULT](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#intentflag_activity_forward_result) + * [示例:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#示例) + * [AActivity以startActivityForResult方式打开BActivity](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivity以startactivityforresult方式打开bactivity) + * [BActivity以普通方式打开CActivity,设置Intent 的Flag Intent.FLAG_ACTIVITY_FORWARD_RESULT](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivity以普通方式打开cactivity设置intent-的flag-intentflag_activity_forward_result) + * [源码解析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#源码解析) + * [ActivityResult数据的写入](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#activityresult数据的写入) + * [ActivityResult数据的传递](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#activityresult数据的传递) + * [Intent.FLAG_ACTIVITY_FORWARD_RESULT的实现](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#intentflag_activity_forward_result的实现) + * [AMS源码分析(三)AMS中Activity栈管理详解](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#ams源码分析三ams中activity栈管理详解) + * [Activity栈管理相关类](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#activity栈管理相关类) + * [ActivityStackSupervisor](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#activitystacksupervisor) + * [ActivityDisplay](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#activitydisplay) + * [TaskRecord](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#taskrecord) + * [ActivityStack](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#activitystack) + * [关系图:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#关系图) + * [启动模式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#启动模式) + * [standard](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#standard) + * [Intent.FLAG_ACTIVITY_CLEAR_TOP](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#intentflag_activity_clear_top) + * [源码分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#源码分析-1) + * [加入TaskRecord](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#加入taskrecord) + * [standard + Intent.FLAG_ACTIVITY_CLEAR_TOP](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#standard--intentflag_activity_clear_top) + * [singleTop](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#singletop) + * [流程图:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#流程图) + * [原Activity不在栈顶](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#原activity不在栈顶) + * [原Activity在栈顶](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#原activity在栈顶) + * [singleTop + Intent.FLAG_ACTIVITY_CLEAR_TOP](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#singletop--intentflag_activity_clear_top) + * [log](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#log) + * [原Activity不在栈顶](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#原activity不在栈顶-1) + * [原Activity在栈顶](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#原activity在栈顶-1) + * [原Actiivty在栈顶且设置Intent.FLAG_ACTIVITY_CLEAR_TOP](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#原actiivty在栈顶且设置intentflag_activity_clear_top) + * [源码分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#源码分析-2) + * [singleTop + Intent.FLAG_ACTIVITY_CLEAR_TOP](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#singletop--intentflag_activity_clear_top-1) + * [singleTask](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#singletask) + * [启动流程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#启动流程) + * [场景一](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#场景一) + * [场景二](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#场景二) + * [场景三](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#场景三) + * [singeTask源码分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#singetask源码分析) + * [singleInstance](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#singleinstance) + * [Intent.FLAG_ACTIVITY_NEW_TASK、taskAffinity、新Task的创建](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#intentflag_activity_new_tasktaskaffinity新task的创建) + * [Intent.FLAG_ACTIVITY_NEW_TASK的自动设置](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#intentflag_activity_new_task的自动设置) + * [taskAffinity的识别](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#taskaffinity的识别) + * [是否创建新task的识别](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#是否创建新task的识别) + * [总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#总结-5) + * [PMS](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#pms) + * [深入PMS源码(一)—— PMS的启动过程和执行流程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#深入pms源码一-pms的启动过程和执行流程) + * [PMS简介](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#pms简介) + * [PMS的启动过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#pms的启动过程) + * [PMS构造函数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#pms构造函数) + * [PMS的工作过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#pms的工作过程) + * [解析配置文件package.xml](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#解析配置文件packagexml) + * [扫描安装的应用程序](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#扫描安装的应用程序) + * [将apk解析数据同步到PMS的属性中](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#将apk解析数据同步到pms的属性中) + * [更新配置文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#更新配置文件) + * [深入PMS源码(二)—— APK的安装和卸载源码分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#深入pms源码二-apk的安装和卸载源码分析) + * [应用程序安装基础](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#应用程序安装基础) + * [PMS中APK安装过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#pms中apk安装过程) + * [应用程序的卸载过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#应用程序的卸载过程) + * [深入PMS源码(三)—— PMS中intent-filter的匹配架构](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#深入pms源码三-pms中intent-filter的匹配架构) + * [PMS保存IntentFilter](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#pms保存intentfilter) + * [IntentFilter的查找匹配](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#intentfilter的查找匹配) + * [WMS](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#wms) + * [WMS(一):WMS的诞生](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#wms一wms的诞生) + * [WMS概述](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#wms概述) + * [窗口管理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#窗口管理) + * [窗口动画](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#窗口动画) + * [输入系统的中转站](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#输入系统的中转站) + * [Surface管理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#surface管理) + * [WMS的诞生](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#wms的诞生) + * [WMS(二):WMS的重要成员和Window的添加过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#wms二wms的重要成员和window的添加过程) + * [WMS的重要成员](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#wms的重要成员) + * [Window的添加过程(WMS部分)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#window的添加过程wms部分) + * [addWindow方法part1](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#addwindow方法part1) + * [addWindow方法part2](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#addwindow方法part2) + * [addWindow方法part3](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#addwindow方法part3) + * [addWindow方法总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#addwindow方法总结) + * [结语](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#结语) + * [WMS(三):Window的删除过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#wms三window的删除过程) + * [Window的删除过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#window的删除过程) ## 性能优化知识点汇总 From d7aa5dae177a5636f70970abbd09feb3e4d723df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Thu, 16 Dec 2021 16:56:46 +0800 Subject: [PATCH 14/29] Update README.md --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 662fbe6..e8c9398 100644 --- a/README.md +++ b/README.md @@ -336,15 +336,15 @@ Android开发核心知识点笔记-目录: * [Activity在AMS中的标识](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#activity在ams中的标识) * [启动过程时序图](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#启动过程时序图) * [从AActivity跳转BActivity的生命周期分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#从aactivity跳转bactivity的生命周期分析) - * [AActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onPause](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivityonpause) - * [BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onCreate](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityoncreate) - * [BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onStart](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonstart) - * [BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onResume](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonresume) - * [AActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onStop](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivityonstop) + * [AActivity#onPause](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivityonpause) + * [BActivity#onCreate](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityoncreate) + * [BActivity#onStart](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonstart) + * [BActivity#onResume](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonresume) + * [AActivity#onStop](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivityonstop) * [从BActivity返回到AActivity](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#从bactivity返回到aactivity) - * [BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onPause](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonpause) - * [AActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onStart、AActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onResume](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivityonstartaactivityonresume) - * [BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onStop、BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onDestroy](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonstopbactivityondestroy) + * [BActivity#onPause](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonpause) + * [AActivity#onStart、AActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onResume](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivityonstartaactivityonresume) + * [BActivity#onStop、BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onDestroy](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonstopbactivityondestroy) * [AMS源码分析(二)onActivityResult执行过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#ams源码分析二onactivityresult执行过程) * [onActivityResult](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onactivityresult) * [AActivity跳转BAcitivty并从BActivity返回数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivity跳转bacitivty并从bactivity返回数据) From 56e21e5d4a8789d46946735833601e1d408e1e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Thu, 16 Dec 2021 16:58:13 +0800 Subject: [PATCH 15/29] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e8c9398..7e02d9b 100644 --- a/README.md +++ b/README.md @@ -343,8 +343,8 @@ Android开发核心知识点笔记-目录: * [AActivity#onStop](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivityonstop) * [从BActivity返回到AActivity](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#从bactivity返回到aactivity) * [BActivity#onPause](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonpause) - * [AActivity#onStart、AActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onResume](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivityonstartaactivityonresume) - * [BActivity#onStop、BActivityhttps://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onDestroy](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonstopbactivityondestroy) + * [AActivity#onStart、AActivity#onResume](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivityonstartaactivityonresume) + * [BActivity#onStop、BActivity#onDestroy](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#bactivityonstopbactivityondestroy) * [AMS源码分析(二)onActivityResult执行过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#ams源码分析二onactivityresult执行过程) * [onActivityResult](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#onactivityresult) * [AActivity跳转BAcitivty并从BActivity返回数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Framework知识点汇总.md#aactivity跳转bacitivty并从bactivity返回数据) From 594bc2dd99bb877fa148315a23dceea3959fb24f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Thu, 16 Dec 2021 16:58:55 +0800 Subject: [PATCH 16/29] =?UTF-8?q?Update=20Framework=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E7=82=B9=E6=B1=87=E6=80=BB.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/Docs/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" "b/Docs/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" index b20092e..34d0549 100644 --- "a/Docs/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" +++ "b/Docs/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" @@ -192,7 +192,7 @@ - [addWindow方法总结](#addwindow方法总结) - [结语](#结语) - [WMS(三):Window的删除过程](#wms三window的删除过程) - - [Window的删除过程](#window的删除过程)# Handler + - [Window的删除过程](#window的删除过程) # Handler From 43e34f307688fc9f05d121ea440dac463b74d590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Thu, 16 Dec 2021 17:08:27 +0800 Subject: [PATCH 17/29] =?UTF-8?q?Update=20=E9=9F=B3=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E6=B1=87=E6=80=BB.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...06\347\202\271\346\261\207\346\200\273.md" | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git "a/Docs/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" "b/Docs/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" index 16226b6..76e3ae9 100644 --- "a/Docs/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" +++ "b/Docs/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" @@ -20,7 +20,7 @@ - [MODE_STREAM模式](#mode_stream模式) - [AudioTrack 详解](#audiotrack-详解) - [音频流的类型](#音频流的类型) - - [**Buffer分配和Frame的概念**](#buffer分配和frame的概念) + - [Buffer分配和Frame的概念](#buffer分配和frame的概念) - [AudioTrack构造过程](#audiotrack构造过程) - [AudioTrack 与 MediaPlayer 的对比](#audiotrack-与-mediaplayer-的对比) - [区别](#区别) @@ -31,7 +31,7 @@ - [预览 Camera 数据](#预览-camera-数据) - [取到 NV21 的数据回调](#取到-nv21-的数据回调) - [Android 音视频开发(五):使用 MediaExtractor 和 MediaMuxer API 解析和封装 mp4 文件](#android-音视频开发五使用-mediaextractor-和-mediamuxer-api-解析和封装-mp4-文件) - - [**MediaExtractor API介绍**](#mediaextractor-api介绍) + - [MediaExtractor API介绍](#mediaextractor-api介绍) - [MediaMuxer API介绍](#mediamuxer-api介绍) - [使用情境](#使用情境) - [从MP4文件中提取视频并生成新的视频文件](#从mp4文件中提取视频并生成新的视频文件) @@ -231,8 +231,8 @@ - [mp4box](#mp4box) - [mp4info](#mp4info) - [MP4格式重要Box](#mp4格式重要box) - - [ftyp(**File Type Box**)](#ftypfile-type-box) - - [moov**(Movie Box)**](#moovmovie-box) + - [ftyp(File Type Box)](#ftypfile-type-box) + - [moov(Movie Box)](#moovmovie-box) - [trak(Track Box)](#traktrack-box) - [mdat(Meida Data Box)](#mdatmeida-data-box) - [free或skip(Free Space Box)](#free或skipfree-space-box) @@ -936,7 +936,7 @@ Android将系统的声音分为好几种流类型,下面是几个常见的: -#### **Buffer分配和Frame的概念** +#### Buffer分配和Frame的概念 在计算Buffer分配的大小的时候,我们经常用到的一个方法就是:getMinBufferSize。这个函数决定了应用层分配多大的数据Buffer。 @@ -1132,7 +1132,7 @@ camera.setPreviewCallback(new Camera.PreviewCallback() { ## Android 音视频开发(五):使用 MediaExtractor 和 MediaMuxer API 解析和封装 mp4 文件 -### **MediaExtractor API介绍** +### MediaExtractor API介绍 **MediaExtractor的**作用是把音频和视频的数据进行分离。 @@ -4376,13 +4376,13 @@ mp4box 是GPAC项目中的一个组件,可以通过mp4box针对媒体文件进 #### MP4格式重要Box -##### ftyp(**File Type Box**) +##### ftyp(File Type Box) 该Box有且只有1个,并且只能被包含在文件层,而不能被其他Box包含。该Box应该被放在文件的最开始,指示该MP4文件应用的相关信息。 “ftyp” body依次包括1个32位的major brand(4个字符),1个32位的minor version(整数)和1个以32位(4个字符)为单位元素的数组Compatible Brands。 -##### moov**(Movie Box)** +##### moov(Movie Box) 该box包含了文件媒体的metadata信息,“moov”是一个container box,具体内容信息由子box诠释。同File Type Box一样,该box有且只有一个,且只被包含在文件层。一般情况下,“moov”会紧随“ftyp”出现。 @@ -6546,7 +6546,7 @@ $make install -1. 用javah创建.头文件: classes目录,执行:javah-jni cn.renhui.FFmpegNative,会在当前目录产生cn_renhui_FFmpegNative.h的C头文件; +5. 用javah创建.头文件: classes目录,执行:javah-jni cn.renhui.FFmpegNative,会在当前目录产生cn_renhui_FFmpegNative.h的C头文件; 6. 根据头文件名,建立相同名字c文件cn_renhui_FFmpegNative.c,在这个源文件中实现头文件中定义的方法,代码如下: @@ -6586,7 +6586,7 @@ $make install -1. 编写Android.mk,内容如下: +7. 编写Android.mk,内容如下: @@ -6641,9 +6641,9 @@ $make install -1. 编译so文件,执行ndk-build +8. 编译so文件,执行ndk-build -2. 新建一个Activity,进行测试,测试核心代码: +9. 新建一个Activity,进行测试,测试核心代码: From 39c40fe5c70c831b0990e95e74be9a33390d520a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Thu, 16 Dec 2021 17:14:10 +0800 Subject: [PATCH 18/29] Update README.md --- README.md | 440 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 440 insertions(+) diff --git a/README.md b/README.md index 7e02d9b..5ae3e91 100644 --- a/README.md +++ b/README.md @@ -611,8 +611,448 @@ Android开发核心知识点笔记-目录: * [EventBus](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Android开源库源码分析.md#EventBus) * [简单示例](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Android开源库源码分析.md#简单示例-3) * [源码分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Android开源库源码分析.md#源码分析-3) + +## 音视频知识点汇总 + + * [Android音视频开发初级入门篇](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android音视频开发初级入门篇) + * [Android 音视频开发(一) : 通过三种方式绘制图片](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-音视频开发一--通过三种方式绘制图片) + * [ImageView 绘制图片](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#imageview-绘制图片) + * [SurfaceView 绘制图片](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#surfaceview-绘制图片) + * [自定义 View 绘制图片](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#自定义-view-绘制图片) + * [Android 音视频开发(二):使用 AudioRecord 采集音频PCM并保存到文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-音视频开发二使用-audiorecord-采集音频pcm并保存到文件) + * [AudioRecord API详解](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#audiorecord-api详解) + * [使用 AudioRecord 实现录音,并生成wav](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#使用-audiorecord-实现录音并生成wav) + * [创建一个AudioRecord对象](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#创建一个audiorecord对象) + * [初始化一个buffer](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#初始化一个buffer) + * [开始录音](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#开始录音) + * [创建一个数据流,一边从AudioRecord中读取声音数据到初始化的buffer,一边将buffer中数据导入数据流。](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#创建一个数据流一边从audiorecord中读取声音数据到初始化的buffer一边将buffer中数据导入数据流) + * [关闭数据流](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#关闭数据流) + * [停止录音](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#停止录音) + * [附言](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#附言) + * [源码](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#源码) + * [Android 音视频开发(三):使用 AudioTrack 播放PCM音频](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-音视频开发三使用-audiotrack-播放pcm音频) + * [AudioTrack 基本使用](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#audiotrack-基本使用) + * [MODE_STATIC模式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mode_static模式) + * [MODE_STREAM模式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mode_stream模式) + * [AudioTrack 详解](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#audiotrack-详解) + * [音频流的类型](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#音频流的类型) + * [Buffer分配和Frame的概念](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#buffer分配和frame的概念) + * [AudioTrack构造过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#audiotrack构造过程) + * [AudioTrack 与 MediaPlayer 的对比](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#audiotrack-与-mediaplayer-的对比) + * [区别](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#区别) + * [联系](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#联系) + * [SoundPool](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#soundpool) + * [源码](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#源码-1) + * [Android 音视频开发(四):使用 Camera API 采集视频数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-音视频开发四使用-camera-api-采集视频数据) + * [预览 Camera 数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#预览-camera-数据) + * [取到 NV21 的数据回调](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#取到-nv21-的数据回调) + * [Android 音视频开发(五):使用 MediaExtractor 和 MediaMuxer API 解析和封装 mp4 文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-音视频开发五使用-mediaextractor-和-mediamuxer-api-解析和封装-mp4-文件) + * [MediaExtractor API介绍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mediaextractor-api介绍) + * [MediaMuxer API介绍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mediamuxer-api介绍) + * [使用情境](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#使用情境) + * [从MP4文件中提取视频并生成新的视频文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#从mp4文件中提取视频并生成新的视频文件) + * [Android 音视频开发(六): MediaCodec API 详解](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-音视频开发六-mediacodec-api-详解) + * [MediaCodec 介绍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mediacodec-介绍) + * [MediaCodec API 说明](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mediacodec-api-说明) + * [MediaCodec 流控](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mediacodec-流控) + * [流控基本概念](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#流控基本概念) + * [Android 硬编码流控](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-硬编码流控) + * [Android 流控策略选择](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-流控策略选择) + * [Android 音视频开发(七): 音视频录制流程总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-音视频开发七-音视频录制流程总结) + * [流程分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#流程分析) + * [需求说明](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#需求说明) + * [实现方式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#实现方式) + * [数据处理思路](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#数据处理思路) + * [实现过程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#实现过程) + * [收集Camera数据,并转码为H264存储到文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#收集camera数据并转码为h264存储到文件) + * [音视频采集+混合,存储到文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#音视频采集混合存储到文件) + * [Android音视频开发中级进阶篇](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android音视频开发中级进阶篇) + * [OpenGL ES](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#opengl-es) + * [Android OpenGL ES 开发(一): OpenGL ES 介绍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opengl-es-开发一-opengl-es-介绍) + * [简介OpenGL ES](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#简介opengl-es) + * [基本介绍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#基本介绍) + * [GLSurfaceView](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#glsurfaceview) + * [GLSurfaceView.Renderer](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#glsurfaceviewrenderer) + * [Android OpenGL ES开发(二) : OpenGL ES 环境搭建](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opengl-es开发二--opengl-es-环境搭建) + * [环境搭建目的](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#环境搭建目的) + * [在Manifest中声明OpenGL ES使用](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#在manifest中声明opengl-es使用) + * [创建一个Activity 用于展示OpenGL ES 图形](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#创建一个activity-用于展示opengl-es-图形) + * [创建GLSurfaceView对象](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#创建glsurfaceview对象) + * [创建一个Renderer类](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#创建一个renderer类) + * [总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#总结) + * [Android OpenGL ES 开发(三):OpenGL ES定义形状](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opengl-es-开发三opengl-es定义形状) + * [定义三角形](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#定义三角形) + * [定义正方形](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#定义正方形) + * [Android OpenGL ES开发(四) : OpenGL ES绘制形状](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opengl-es开发四--opengl-es绘制形状) + * [初始化形状](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#初始化形状) + * [绘制形状](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#绘制形状) + * [Android OpenGL ES开发(五) : OpenGL ES使用投影和相机视图](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opengl-es开发五--opengl-es使用投影和相机视图) + * [定义投影](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#定义投影) + * [定义相机视图](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#定义相机视图) + * [应用投影和相机变换](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#应用投影和相机变换) + * [Android openGL ES开发(六): OpenGL ES添加运动效果](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opengl-es开发六-opengl-es添加运动效果) + * [旋转一个图形](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#旋转一个图形) + * [允许连续渲染](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#允许连续渲染) + * [Android openGL ES开发(七) : OpenGL ES 响应触摸事件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opengl-es开发七--opengl-es-响应触摸事件) + * [设置触摸事件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#设置触摸事件) + * [暴露旋转角度](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#暴露旋转角度) + * [应用旋转](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#应用旋转) + * [Android OpenGL ES开发(八) :OpenGL ES 着色器语言GLSL](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opengl-es开发八-opengl-es-着色器语言glsl) + * [GLSL 简介](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#glsl-简介) + * [GLSL 基础](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#glsl-基础) + * [基本数据类型](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#基本数据类型) + * [标量:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#标量) + * [向量:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#向量) + * [矩阵:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#矩阵) + * [采样器:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#采样器) + * [结构体:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#结构体) + * [数组:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#数组) + * [空类型:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#空类型) + * [运算符](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#运算符) + * [类型转换](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#类型转换) + * [限定符](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#限定符) + * [流程控制](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#流程控制) + * [函数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#函数) + * [浮点精度](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#浮点精度) + * [程序结构](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#程序结构) + * [GLSL 内建变量](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#glsl-内建变量) + * [顶点着色器的内建变量](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#顶点着色器的内建变量) + * [片元着色器的内建变量](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#片元着色器的内建变量) + * [常用内置函数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#常用内置函数) + * [常见函数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#常见函数) + * [几何函数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#几何函数) + * [矩阵函数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#矩阵函数) + * [纹理采样函数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#纹理采样函数) + * [Android OpenGL ES开发(九): OpenGL ES纹理贴图](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opengl-es开发九-opengl-es纹理贴图) + * [概念](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#概念) + * [原理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#原理) + * [显示纹理图片](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#显示纹理图片) + * [修改着色器](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#修改着色器) + * [设置顶点坐标和纹理坐标](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#设置顶点坐标和纹理坐标) + * [计算变换矩阵](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#计算变换矩阵) + * [显示图片](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#显示图片) + * [Android OpenGL ES 开发(十):通过GLES20与着色器交互](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opengl-es-开发十通过gles20与着色器交互) + * [获取着色器程序内成员变量的id(句柄、指针)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#获取着色器程序内成员变量的id句柄指针) + * [向着色器传递数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#向着色器传递数据) + * [定义顶点属性数组](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#定义顶点属性数组) + * [启用或者禁用顶点属性数组](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#启用或者禁用顶点属性数组) + * [选择活动纹理单元。](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#选择活动纹理单元) + * [OpenSL ES](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#opensl-es) + * [Android OpenSL ES 开发:Android OpenSL 介绍和开发流程说明](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opensl-es-开发android-opensl-介绍和开发流程说明) + * [Android OpenSL ES 介绍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opensl-es-介绍) + * [Android OpenSL ES 开发流程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opensl-es-开发流程) + * [OpenSL ES 开发最重要的接口类 SLObjectItf](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#opensl-es-开发最重要的接口类-slobjectitf) + * [SLObjectItf 创建的具体的接口对象实例](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#slobjectitf-创建的具体的接口对象实例) + * [创建引擎并实现](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#创建引擎并实现) + * [利用引擎对象创建其他接口对象](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#利用引擎对象创建其他接口对象) + * [OpenSL ES 使用示例](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#opensl-es-使用示例) + * [播放assets文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#播放assets文件) + * [播放pcm文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#播放pcm文件) + * [创建播放器和混音器](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#创建播放器和混音器) + * [设置pcm格式的频率位数等信息并创建播放器](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#设置pcm格式的频率位数等信息并创建播放器) + * [设置缓冲队列和回调函数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#设置缓冲队列和回调函数) + * [设置播放状态并手动开始调用回调函数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#设置播放状态并手动开始调用回调函数) + * [Android OpenSL ES 开发:使用 OpenSL 播放 PCM 数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opensl-es-开发使用-opensl-播放-pcm-数据) + * [创建声音引擎](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#创建声音引擎) + * [创建声音播放器](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#创建声音播放器) + * [设置播放缓冲](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#设置播放缓冲) + * [获得其他接口用来控制播放](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#获得其他接口用来控制播放) + * [提供播放数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#提供播放数据) + * [播放音乐](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#播放音乐) + * [调解音量](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#调解音量) + * [释放资源](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#释放资源) + * [参考源码](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#参考源码) + * [Android OpenSL ES 开发:Android OpenSL 录制 PCM 音频数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opensl-es-开发android-opensl-录制-pcm-音频数据) + * [实现说明](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#实现说明) + * [编码实现](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#编码实现) + * [编写缓存buffer队列:RecordBuffer.h、RecordBuffer.cpp](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#编写缓存buffer队列recordbufferhrecordbuffercpp) + * [使用OpenSL ES录制PCM数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#使用opensl-es录制pcm数据) + * [验证录制成果](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#验证录制成果) + * [参考源码](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#参考源码-1) + * [Android OpenSL ES 开发:OpenSL ES利用SoundTouch实现PCM音频的变速和变调](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android-opensl-es-开发opensl-es利用soundtouch实现pcm音频的变速和变调) + * [缘由](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#缘由) + * [实现](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#实现) + * [移植SoundTouch(Android)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#移植soundtouchandroid) + * [用SoundTouch转码PCM源文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#用soundtouch转码pcm源文件) + * [SoundTouch使用流程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#soundtouch使用流程) + * [添加命名空间,并创建SoundTouch指针变量](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#添加命名空间并创建soundtouch指针变量) + * [设置SoundTouch参数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#设置soundtouch参数) + * [向SoundTouch中传入获取到的PCM数据,使用:putSamples函数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#向soundtouch中传入获取到的pcm数据使用putsamples函数) + * [获取SoundTouch输出的PCM数据:使用receiveSamples函数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#获取soundtouch输出的pcm数据使用receivesamples函数) + * [OpenSL ES播放SoundTouch处理后的PCM音频数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#opensl-es播放soundtouch处理后的pcm音频数据) + * [思维发散](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#思维发散) + * [FFmpeg解码得到的PCM数据(uint_8 *)利用SoundTouch转码](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg解码得到的pcm数据uint_8-利用soundtouch转码) + * [总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#总结-1) + * [参考源码](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#参考源码-2) + * [Android音视频开发高级探究篇](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#android音视频开发高级探究篇) + * [音视频编解码技术](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#音视频编解码技术) + * [音视频编解码技术(一):MPEG-4/H.264 AVC 编解码标准](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#音视频编解码技术一mpeg-4h264-avc-编解码标准) + * [H264 概述](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#h264-概述) + * [H.264视频编解码的意义](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#h264视频编解码的意义) + * [H.264编解码的理论依据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#h264编解码的理论依据) + * [H.264相关概念](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#h264相关概念) + * [H.264 的基本单位](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#h264-的基本单位) + * [帧类型](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#帧类型) + * [GOP(画面组)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#gop画面组) + * [IDR 帧](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#idr-帧) + * [H.264 压缩方式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#h264-压缩方式) + * [H.264 压缩算法](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#h264-压缩算法) + * [H.264压缩方式说明](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#h264压缩方式说明) + * [H.264 分层结构](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#h264-分层结构) + * [VLC层(Video Coding Layer)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#vlc层video-coding-layer) + * [NAL层(Network Abstraction Layer)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#nal层network-abstraction-layer) + * [NALU (NAL Unit)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#nalu-nal-unit) + * [Start Code](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#start-code) + * [NAL Header](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#nal-header) + * [RBSP(Raw Byte Sequence Payload))](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#rbspraw-byte-sequence-payload) + * [H.264 局限性](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#h264-局限性) + * [音视频编解码技术(二):AAC 音频编码技术](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#音视频编解码技术二aac-音频编码技术) + * [AAC编码概述](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#aac编码概述) + * [AAC编码规格简述](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#aac编码规格简述) + * [AAC编码的特点](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#aac编码的特点) + * [AAC音频文件格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#aac音频文件格式) + * [ACC 音频文件格式类型](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#acc-音频文件格式类型) + * [ADIF 的 Header 结构](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#adif-的-header-结构) + * [ADTS 的 Header 头结构](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#adts-的-header-头结构) + * [AAC文件处理流程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#aac文件处理流程) + * [开源AAC解码器](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#开源aac解码器) + * [流媒体协议](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#流媒体协议) + * [流媒体协议(一):HLS 协议](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#流媒体协议一hls-协议) + * [HLS 概述](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#hls-概述) + * [原理介绍:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#原理介绍) + * [整体架构](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#整体架构) + * [HLS 播放](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#hls-播放) + * [播放未加密HLS](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#播放未加密hls) + * [播放加密HLS](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#播放加密hls) + * [HLS 协议总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#hls-协议总结) + * [优点:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#优点) + * [缺点:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#缺点) + * [改进](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#改进) + * [流媒体协议(二):RTMP协议](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#流媒体协议二rtmp协议) + * [概念与摘要](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#概念与摘要) + * [RTMP块流](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#rtmp块流) + * [消息格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#消息格式) + * [握手](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#握手) + * [握手序列](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#握手序列) + * [C0和S0格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#c0和s0格式) + * [C1和S1格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#c1和s1格式) + * [C2和S2格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#c2和s2格式) + * [RMTP握手](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#rmtp握手) + * [握手过程示意图](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#握手过程示意图) + * [多媒体文件格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#多媒体文件格式) + * [多媒体文件格式(一):MP4 格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#多媒体文件格式一mp4-格式) + * [MP4 格式标准介绍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mp4-格式标准介绍) + * [MP4分析工具](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mp4分析工具) + * [Elecard StreamEye](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#elecard-streameye) + * [mp4box](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mp4box) + * [mp4info](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mp4info) + * [MP4格式重要Box](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mp4格式重要box) + * [ftyp(File Type Box)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ftypfile-type-box) + * [moov(Movie Box)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#moovmovie-box) + * [trak(Track Box)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#traktrack-box) + * [mdat(Meida Data Box)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mdatmeida-data-box) + * [free或skip(Free Space Box)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#free或skipfree-space-box) + * [stbl(Sample Table Box)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#stblsample-table-box) + * [MP4格式 与 FFmpeg实战](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#mp4格式-与-ffmpeg实战) + * [在FFmpeg中的输出MP4的Demuxer信息](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#在ffmpeg中的输出mp4的demuxer信息) + * [通过FFmepg faststart参数的使用,来理解mdat和moov的顺序的意义](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#通过ffmepg-faststart参数的使用来理解mdat和moov的顺序的意义) + * [多媒体文件格式(二):FLV 格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#多媒体文件格式二flv-格式) + * [FLV 格式标准介绍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#flv-格式标准介绍) + * [文件头 Header](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#文件头-header) + * [文件体 FLV Body](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#文件体-flv-body) + * [Tag](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#tag) + * [FLV 分析工具](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#flv-分析工具) + * [FlvAnalyzer](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#flvanalyzer) + * [FLV Format Analysis 工具](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#flv-format-analysis-工具) + * [FLV格式 与 FFmpeg 实战](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#flv格式-与-ffmpeg-实战) + * [使用FFmpeg生成带关键索引信息的FLV](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#使用ffmpeg生成带关键索引信息的flv) + * [使用ffprobe查看FLV关键帧索引相关信息](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#使用ffprobe查看flv关键帧索引相关信息) + * [多媒体文件格式(三):M3U8 格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#多媒体文件格式三m3u8-格式) + * [M3U8 格式标准介绍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#m3u8-格式标准介绍) + * [HLS 与 M3U8](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#hls-与-m3u8) + * [FFmpeg转HLS文件(M3U8)实战](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg转hls文件m3u8实战) + * [FFmpeg转MP4为HLS(M3U8)文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg转mp4为hlsm3u8文件) + * [FFmpeg 转 HLS (M3U8) 文件命令参数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-转-hls-m3u8-文件命令参数) + * [start_number 参数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#start_number-参数) + * [hls_time 参数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#hls_time-参数) + * [hls_list_size 参数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#hls_list_size-参数) + * [hls_base_url参数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#hls_base_url参数) + * [多媒体文件格式(四):TS 格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#多媒体文件格式四ts-格式) + * [TS 格式标准介绍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ts-格式标准介绍) + * [TS 格式详解](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ts-格式详解) + * [TS层](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ts层) + * [TS Header](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ts-header) + * [TS Adaptation Field](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ts-adaptation-field) + * [TS Payload](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ts-payload) + * [PES层 & ES 层](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#pes层--es-层) + * [PES层](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#pes层) + * [ES 层](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#es-层) + * [TS流生成及解析流程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ts流生成及解析流程) + * [TS 流生成流程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ts-流生成流程) + * [TS 流解析流程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ts-流解析流程) + * [多媒体文件格式(五):PCM / WAV 格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#多媒体文件格式五pcm--wav-格式) + * [名词解析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#名词解析) + * [采样率](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#采样率) + * [位深度](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#位深度) + * [PCM](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#pcm) + * [PCM音频数据存储方式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#pcm音频数据存储方式) + * [PCM 音频数据的参数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#pcm-音频数据的参数) + * [WAV](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#wav) + * [PCM & WAV 开发实践](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#pcm--wav-开发实践) + * [PCM格式转为WAV格式(基于C语言)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#pcm格式转为wav格式基于c语言) + * [PCM降低某个声道的音量(基于C语言)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#pcm降低某个声道的音量基于c语言) + * [分离PCM音频数据左右声道的数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#分离pcm音频数据左右声道的数据) + * [从PCM16LE单声道音频采样数据中截取一部分数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#从pcm16le单声道音频采样数据中截取一部分数据) + * [将PCM16LE双声道音频采样数据转换为PCM8音频采样数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#将pcm16le双声道音频采样数据转换为pcm8音频采样数据) + * [将PCM16LE双声道音频采样数据的声音速度提高一倍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#将pcm16le双声道音频采样数据的声音速度提高一倍) + * [FFmpeg](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg) + * [FFmpeg命令行工具(一):查看媒体文件头信息工具ffprobe](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg命令行工具一查看媒体文件头信息工具ffprobe) + * [简述](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#简述) + * [命令格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#命令格式) + * [使用ffprobe查看mp3格式的文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#使用ffprobe查看mp3格式的文件) + * [使用ffprobe查看mp4格式的文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#使用ffprobe查看mp4格式的文件) + * [ffprobe高级使用方式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffprobe高级使用方式) + * [FFmpeg命令行工具(二):播放媒体文件的工具ffplay](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg命令行工具二播放媒体文件的工具ffplay) + * [简述](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#简述-1) + * [命令格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#命令格式-1) + * [主要选项](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#主要选项) + * [一些高级选项](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#一些高级选项) + * [一些快捷键](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#一些快捷键) + * [ffplay 播放音频](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffplay-播放音频) + * [ffplay 播放视频](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffplay-播放视频) + * [ffplay 高级使用方式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffplay-高级使用方式) + * [循环播放](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#循环播放) + * [播放 pm.mp4 ,播放完成后自动退出](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#播放-pmmp4-播放完成后自动退出) + * [以 320 x 240 的大小播放 test.mp4](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#以-320-x-240-的大小播放-testmp4) + * [将窗口标题设置为 "myplayer",循环播放 2 次](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#将窗口标题设置为-myplayer循环播放-2-次) + * [播放 双通道 32K 的 PCM 音频数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#播放-双通道-32k-的-pcm-音频数据) + * [ffplay音画同步](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffplay音画同步) + * [FFmpeg命令行工具(三):媒体文件转换工具ffmpeg](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg命令行工具三媒体文件转换工具ffmpeg) + * [简述](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#简述-2) + * [命令行参数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#命令行参数) + * [通用参数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#通用参数) + * [视频参数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#视频参数) + * [音频参数](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#音频参数) + * [实践学习](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#实践学习) + * [列出ffmpeg支持的所有格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#列出ffmpeg支持的所有格式) + * [剪切一段媒体文件,可以是音频或者视频文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#剪切一段媒体文件可以是音频或者视频文件) + * [提取视频文件中的音频数据,并保存为文件](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#提取视频文件中的音频数据并保存为文件) + * [将视频中的音频静音,只保留视频](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#将视频中的音频静音只保留视频) + * [从mp4文件中抽取视频流导出为裸H264数据:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#从mp4文件中抽取视频流导出为裸h264数据) + * [将视频推送到流媒体服务器上:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#将视频推送到流媒体服务器上) + * [将流媒体服务器上的流dump到本地:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#将流媒体服务器上的流dump到本地) + * [给视频添加水印](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#给视频添加水印) + * [倒放音视频](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#倒放音视频) + * [将几个MP4视频文件合并为1个视频.](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#将几个mp4视频文件合并为1个视频) + * [FFmpeg命令行工具(四):FFmpeg 调整音视频播放速度](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg命令行工具四ffmpeg-调整音视频播放速度) + * [调整视频速率](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#调整视频速率) + * [调整音频速率](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#调整音频速率) + * [参考文献](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#参考文献) + * [FFmpeg(一):FFmpeg 简介](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg一ffmpeg-简介) + * [FFmpeg 介绍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-介绍) + * [FFmpeg 组成](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-组成) + * [FFmpeg包含类库说明](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg包含类库说明) + * [类库说明](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#类库说明) + * [常用结构](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#常用结构) + * [封装格式](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#封装格式) + * [编解码](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#编解码) + * [网络协议](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#网络协议) + * [数据存放](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#数据存放) + * [FFmpeg(二):Mac下安装FFmpeg](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg二mac下安装ffmpeg) + * [安装ffmpeg](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#安装ffmpeg) + * [命令行安装](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#命令行安装) + * [下载压缩包安装](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#下载压缩包安装) + * [安装ffplay](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#安装ffplay) + * [命令行安装](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#命令行安装-1) + * [下载压缩包安装](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#下载压缩包安装-1) + * [附言](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#附言-1) + * [FFmpeg(三):将 FFmpeg 移植到 Android平台](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg三将-ffmpeg-移植到-android平台) + * [FFmpeg(四):FFmpeg API 介绍与通用 API 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg四ffmpeg-api-介绍与通用-api-分析) + * [FFmpeg 编解码流程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-编解码流程) + * [FFmpeg 相关术语](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-相关术语) + * [FFmpeg 通用 API 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-通用-api-分析) + * [av_register_all 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#av_register_all-分析) + * [av_find_codec 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#av_find_codec-分析) + * [avcodec_open2 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avcodec_open2-分析) + * [avcodec_close 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avcodec_close-分析) + * [总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#总结-2) + * [FFmpeg(五):FFmpeg 编解码 API 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg五ffmpeg-编解码-api-分析) + * [FFmpeg 解码 API 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-解码-api-分析) + * [avformat_open_input 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avformat_open_input-分析) + * [avformat_find_stream_info 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avformat_find_stream_info-分析) + * [av_read_frame 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#av_read_frame-分析) + * [avcodec_decode分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avcodec_decode分析) + * [avformat_close_input 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avformat_close_input-分析) + * [FFmpeg 编码 API 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-编码-api-分析) + * [avformat_alloc_output_context2 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avformat_alloc_output_context2-分析) + * [avio_open2 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avio_open2-分析) + * [编码其他API(步骤)分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#编码其他api步骤分析) + * [FFmpeg 解码 API 超时设置](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-解码-api-超时设置) + * [设置开流的超时时间](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#设置开流的超时时间) + * [设置interrupt_callback定义返回机制](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#设置interrupt_callback定义返回机制) + * [FFmpeg(六):FFmpeg 核心模块 libavformat 与 libavcodec 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg六ffmpeg-核心模块-libavformat-与-libavcodec-分析) + * [libavformat介绍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#libavformat介绍) + * [libavcodec介绍](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#libavcodec介绍) + * [FFmpeg 结构体(一): AVFormatContext 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-结构体一-avformatcontext-分析) + * [源码整理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#源码整理) + * [AVForamtContext 重点字段](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avforamtcontext-重点字段) + * [FFmpeg 结构体(二): AVStream 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-结构体二-avstream-分析) + * [源码整理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#源码整理-1) + * [AVStream 重点字段](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avstream-重点字段) + * [FFmpeg 结构体(三): AVPacket 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-结构体三-avpacket-分析) + * [源码整理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#源码整理-2) + * [AVPacket 重点字段](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avpacket-重点字段) + * [FFmpeg 结构体(四): AVFrame 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-结构体四-avframe-分析) + * [源码整理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#源码整理-3) + * [AVFrame 重点字段](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avframe-重点字段) + * [data[]](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#data) + * [pict_type](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#pict_type) + * [sample_aspect_ratio](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#sample_aspect_ratio) + * [qscale_table](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#qscale_table) + * [FFmpeg 结构体(五): AVCodec 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-结构体五-avcodec-分析) + * [源码整理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#源码整理-4) + * [AVCodec 重点字段](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avcodec-重点字段) + * [enum AVMediaType type](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#enum-avmediatype-type) + * [enum AVCodecID id](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#enum-avcodecid-id) + * [const enum AVPixelFormat *pix_fmts](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#const-enum-avpixelformat-pix_fmts) + * [const enum AVSampleFormat *sample_fmts](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#const-enum-avsampleformat-sample_fmts) + * [FFmpeg 结构体(六): AVCodecContext 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-结构体六-avcodeccontext-分析) + * [源码整理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#源码整理-5) + * [AVCodecContext 重点字段](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avcodeccontext-重点字段) + * [codec_type](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#codec_type) + * [sample_fmt](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#sample_fmt) + * [profile](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#profile) + * [FFmpeg 结构体(七): AVIOContext 分析](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-结构体七-aviocontext-分析) + * [源码整理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#源码整理-6) + * [AVIOContext 重点字段](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#aviocontext-重点字段) + * [FFmpeg 结构体(八):FFMPEG中重要结构体之间的关系](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-结构体八ffmpeg中重要结构体之间的关系) + * [解协议(http,rtsp,rtmp,mms)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#解协议httprtsprtmpmms) + * [解封装(flv,avi,rmvb,mp4)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#解封装flvavirmvbmp4) + * [解码(h264,mpeg2,aac,mp3)](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#解码h264mpeg2aacmp3) + * [存数据](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#存数据) + * [FFmpeg 开发之 AVFilter 使用流程总结](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-开发之-avfilter-使用流程总结) + * [AVFilterGraph 、AVFilterContext、AVFilter](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avfiltergraph-avfiltercontextavfilter) + * [AVFilter 相关Api使用方法整理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avfilter-相关api使用方法整理) + * [AVFilterContext 初始化方法](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avfiltercontext-初始化方法) + * [AVFilterGraph 相关的Api](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avfiltergraph-相关的api) + * [FFmpeg Filter Buffer 和 BufferSink 相关APi的使用方法整理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-filter-buffer-和-buffersink-相关api的使用方法整理) + * [FFmpeg AVFilter 使用整体流程](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-avfilter-使用整体流程) + * [过滤器构建:](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#过滤器构建) + * [数据加工](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#数据加工) + * [资源释放](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#资源释放) + * [FFmpeg 过时 Api 汇总整理](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#ffmpeg-过时-api-汇总整理) + * [AVStream::codec: 被声明为已否决](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avstreamcodec-被声明为已否决) + * [avcodec_encode_audio2:被声明为已否决](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avcodec_encode_audio2被声明为已否决) + * ['avpicture_get_size': 被声明为已否决](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avpicture_get_size-被声明为已否决) + * ['avpicture_fill': 被声明为已否决](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avpicture_fill-被声明为已否决) + * ['avcodec_decode_video2': 被声明为已否决](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avcodec_decode_video2-被声明为已否决) + * [' avcodec_alloc_frame': 被声明为已否决](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#-avcodec_alloc_frame-被声明为已否决) + * ['av_free_packet': 被声明为已否决](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#av_free_packet-被声明为已否决) + * [avcodec_decode_audio4:被声明为已否决](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avcodec_decode_audio4被声明为已否决) + * [avcodec_encode_video2:被声明为已否决](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/音视频知识点汇总.md#avcodec_encode_video2被声明为已否决) ## Kotlin知识点汇总 + * [对象](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Kotlin知识点汇总.md#对象) * [类](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Kotlin知识点汇总.md#类) * [继承](https://github.com/BlackZhangJX/Android-Notes/blob/master/Docs/Kotlin知识点汇总.md#继承) From fd11113853804f3f21d4a1fd9b03d4e21afd7e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0?= <56581788+BlackZhangJX@users.noreply.github.com> Date: Sun, 19 Dec 2021 22:27:35 +0800 Subject: [PATCH 19/29] Delete Docs directory --- ...20\347\240\201\345\210\206\346\236\220.md" | 7369 ------------- ...25\347\237\245\350\257\206\347\202\271.md" | 994 -- ...06\347\202\271\346\261\207\346\200\273.md" | 2726 ----- ...06\347\202\271\346\261\207\346\200\273.md" | 436 - ...06\347\202\271\346\261\207\346\200\273.md" | 9704 ----------------- ...06\347\202\271\346\261\207\346\200\273.md" | 8 - ...06\347\202\271\346\261\207\346\200\273.md" | 1082 -- ...06\347\202\271\346\261\207\346\200\273.md" | 1492 --- ...25\351\242\230\346\261\207\346\200\273.md" | 1843 ---- ...06\347\202\271\346\261\207\346\200\273.md" | 2959 ----- ...21\347\273\234\345\237\272\347\241\200.md" | 334 - ...41\345\274\217\346\261\207\346\200\273.md" | 545 - ...06\347\202\271\346\261\207\346\200\273.md" | 9535 ---------------- 13 files changed, 39027 deletions(-) delete mode 100644 "Docs/Android\345\274\200\346\272\220\345\272\223\346\272\220\347\240\201\345\210\206\346\236\220.md" delete mode 100644 "Docs/Android\346\211\251\345\261\225\347\237\245\350\257\206\347\202\271.md" delete mode 100644 "Docs/Android\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" delete mode 100644 "Docs/C++\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" delete mode 100644 "Docs/Framework\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" delete mode 100644 "Docs/Gradle\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" delete mode 100644 "Docs/Java\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" delete mode 100644 "Docs/Kotlin\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" delete mode 100644 "Docs/\345\270\270\350\247\201\351\235\242\350\257\225\347\256\227\346\263\225\351\242\230\346\261\207\346\200\273.md" delete mode 100644 "Docs/\346\200\247\350\203\275\344\274\230\345\214\226\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" delete mode 100644 "Docs/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\237\272\347\241\200.md" delete mode 100644 "Docs/\350\256\276\350\256\241\346\250\241\345\274\217\346\261\207\346\200\273.md" delete mode 100644 "Docs/\351\237\263\350\247\206\351\242\221\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" diff --git "a/Docs/Android\345\274\200\346\272\220\345\272\223\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/Docs/Android\345\274\200\346\272\220\345\272\223\346\272\220\347\240\201\345\210\206\346\236\220.md" deleted file mode 100644 index 1dd18ce..0000000 --- "a/Docs/Android\345\274\200\346\272\220\345\272\223\346\272\220\347\240\201\345\210\206\346\236\220.md" +++ /dev/null @@ -1,7369 +0,0 @@ -- [OKHttp](#OKHttp) - - [OKHttp请求流程](#OKHttp请求流程) - - [新建OKHttpClient客户端](#新建OKHttpClient客户端) - - [同步请求流程](#同步请求流程) - - [异步请求流程](#异步请求流程) - - [网络请求缓存处理](#网络请求缓存处理之CacheInterceptor) - - [连接池](#ConnectInterceptor之连接池) -- [Retrofit](#Retrofit) - - [基本使用流程](#基本使用流程) - - [Retrofit构建过程](#Retrofit构建过程) - - [Retrofit核心对象解析](#Retrofit核心对象解析) - - [Builder内部构造](#Builder内部构造) - - [添加baseUrl](#添加baseUrl) - - [添加GsonConverterFactory](#添加GsonConverterFactory) - - [build过程](#build过程) - - [创建网络请求接口实例过程](#创建网络请求接口实例过程) - - [创建网络请求接口类实例并执行请求过程](#创建网络请求接口类实例并执行请求过程) - - [Retrofit源码流程图](#Retrofit源码流程图) -- [Glide](#Glide) - - [基本使用流程](#基本使用流程-1) - - [GlideApp.with(context)源码详解](#GlideAppwithcontext源码详解) - - [load(url)源码详解](#loadurl源码详解) - - [into(iv)源码详解](#intoiv源码详解) - - [完整Glide加载流程图](#完整Glide加载流程图) -- [GreenDao](#GreenDao) - - [基本使用流程](#基本使用流程-2) - - [GreenDao使用流程分析](#GreenDao使用流程分析) - - [创建数据库帮助类对象DaoMaster.DevOpenHelper](#创建数据库帮助类对象DaoMasterDevOpenHelper) - - [创建DaoMaster对象](#创建DaoMaster对象) - - [创建DaoSession对象](#创建DaoSession对象) - - [插入源码分析](#插入源码分析) - - [查询源码分析](#查询源码分析) - - [GreenDao是如何与ReactiveX结合?](#GreenDao是如何与ReactiveX结合) -- [RxJava](#RxJava) - - [RxJava是什么?](#RxJava到底是什么) - - [RxJava的订阅流程](#RxJava的订阅流程) - - [创建被观察者过程](#创建被观察者过程) - - [订阅过程](#订阅过程) - - [RxJava的线程切换](#RxJava的线程切换) -- [LeakCanary](#LeakCanary) - - [原理概述](#原理概述) - - [简单示例](#简单示例) - - [源码分析](#源码分析) - - [LeakCanary运作流程](#LeakCanary运作流程) -- [ButterKnife](#ButterKnife) - - [简单示例](#简单示例-1) - - [源码分析](#源码分析-1) - - [模板代码解析](#模板代码解析) - - [ButterKnife 是怎样实现代码注入的](#ButterKnife-是怎样实现代码注入的) - - [ButterKnife是如何在编译时生成代码的?](#ButterKnife是如何在编译时生成代码的) -- [Dagger 2](#Dagger-2) - - [预备知识](#预备知识) - - [@Inject](#@Inject) - - [@Module](#@Module) - - [@Singleton](#@Singleton) - - [@Providers](#@Providers) - - [@Component](#@Component) - - [@Scope](#@Scope) - - [@Qualifier](#@Qualifier) - - [dependencies](#dependencies) - - [@SubComponent](#@SubComponent) - - [简单示例](#简单示例-2) - - [源码分析](#源码分析-2) -- [EventBus](#EventBus) - - [简单示例](#简单示例-3) - - [源码分析](#源码分析-3) - -# OKHttp - -## OKHttp请求流程 - -OKHttp内部的大致请求流程图如下所示: - -![image](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/976154d56c224d96b33b1ca424e935a2~tplv-k3u1fbpfcp-zoom-1.image) - -如下为使用OKHttp进行Get请求的步骤: - -``` -//1.新建OKHttpClient客户端 -OkHttpClient client = new OkHttpClient(); -//新建一个Request对象 -Request request = new Request.Builder() - .url(url) - .build(); -//2.Response为OKHttp中的响应 -Response response = client.newCall(request).execute(); -``` - -### 新建OKHttpClient客户端 - -``` -OkHttpClient client = new OkHttpClient(); - -public OkHttpClient() { - this(new Builder()); -} - -OkHttpClient(Builder builder) { - .... -} -``` - -可以看到,OkHttpClient使用了建造者模式,Builder里面的可配置参数如下: - -``` -public static final class Builder { - Dispatcher dispatcher;// 分发器 - @Nullable Proxy proxy; - List protocols; - List connectionSpecs;// 传输层版本和连接协议 - final List interceptors = new ArrayList<>();// 拦截器 - final List networkInterceptors = new ArrayList<>(); - EventListener.Factory eventListenerFactory; - ProxySelector proxySelector; - CookieJar cookieJar; - @Nullable Cache cache; - @Nullable InternalCache internalCache;// 内部缓存 - SocketFactory socketFactory; - @Nullable SSLSocketFactory sslSocketFactory;// 安全套接层socket 工厂,用于HTTPS - @Nullable CertificateChainCleaner certificateChainCleaner;// 验证确认响应证书 适用 HTTPS 请求连接的主机名。 - HostnameVerifier hostnameVerifier;// 验证确认响应证书 适用 HTTPS 请求连接的主机名。 - CertificatePinner certificatePinner;// 证书锁定,使用CertificatePinner来约束哪些认证机构被信任。 - Authenticator proxyAuthenticator;// 代理身份验证 - Authenticator authenticator;// 身份验证 - ConnectionPool connectionPool;// 连接池 - Dns dns; - boolean followSslRedirects; // 安全套接层重定向 - boolean followRedirects;// 本地重定向 - boolean retryOnConnectionFailure;// 重试连接失败 - int callTimeout; - int connectTimeout; - int readTimeout; - int writeTimeout; - int pingInterval; - - // 这里是默认配置的构建参数 - public Builder() { - dispatcher = new Dispatcher(); - protocols = DEFAULT_PROTOCOLS; - connectionSpecs = DEFAULT_CONNECTION_SPECS; - ... - } - - // 这里传入自己配置的构建参数 - Builder(OkHttpClient okHttpClient) { - this.dispatcher = okHttpClient.dispatcher; - this.proxy = okHttpClient.proxy; - this.protocols = okHttpClient.protocols; - this.connectionSpecs = okHttpClient.connectionSpecs; - this.interceptors.addAll(okHttpClient.interceptors); - this.networkInterceptors.addAll(okHttpClient.networkInterceptors); - ... - } -``` - -### 同步请求流程 - -``` -Response response = client.newCall(request).execute(); - -/** -* Prepares the {@code request} to be executed at some point in the future. -*/ -@Override public Call newCall(Request request) { - return RealCall.newRealCall(this, request, false /* for web socket */); -} - -// RealCall为真正的请求执行者 -static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) { - // Safely publish the Call instance to the EventListener. - RealCall call = new RealCall(client, originalRequest, forWebSocket); - call.eventListener = client.eventListenerFactory().create(call); - return call; -} - -@Override public Response execute() throws IOException { - synchronized (this) { - // 每个Call只能执行一次 - if (executed) throw new IllegalStateException("Already Executed"); - executed = true; - } - captureCallStackTrace(); - timeout.enter(); - eventListener.callStart(this); - try { - // 通知dispatcher已经进入执行状态 - client.dispatcher().executed(this); - // 通过一系列的拦截器请求处理和响应处理得到最终的返回结果 - Response result = getResponseWithInterceptorChain(); - if (result == null) throw new IOException("Canceled"); - return result; - } catch (IOException e) { - e = timeoutExit(e); - eventListener.callFailed(this, e); - throw e; - } finally { - // 通知 dispatcher 自己已经执行完毕 - client.dispatcher().finished(this); - } -} - -Response getResponseWithInterceptorChain() throws IOException { - // Build a full stack of interceptors. - List interceptors = new ArrayList<>(); - // 在配置 OkHttpClient 时设置的 interceptors; - interceptors.addAll(client.interceptors()); - // 负责失败重试以及重定向 - interceptors.add(retryAndFollowUpInterceptor); - // 请求时,对必要的Header进行一些添加,接收响应时,移除必要的Header - interceptors.add(new BridgeInterceptor(client.cookieJar())); - // 负责读取缓存直接返回、更新缓存 - interceptors.add(new CacheInterceptor(client.internalCache())); - // 负责和服务器建立连接 - interceptors.add(new ConnectInterceptor(client)); - if (!forWebSocket) { - // 配置 OkHttpClient 时设置的 networkInterceptors - interceptors.addAll(client.networkInterceptors()); - } - // 负责向服务器发送请求数据、从服务器读取响应数据 - interceptors.add(new CallServerInterceptor(forWebSocket)); - - Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0, - originalRequest, this, eventListener, client.connectTimeoutMillis(), - client.readTimeoutMillis(), client.writeTimeoutMillis()); - - // 使用责任链模式开启链式调用 - return chain.proceed(originalRequest); -} - -// StreamAllocation 对象,它相当于一个管理类,维护了服务器连接、并发流 -// 和请求之间的关系,该类还会初始化一个 Socket 连接对象,获取输入/输出流对象。 -public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, - RealConnection connection) throws IOException { - ... - - // Call the next interceptor in the chain. - // 实例化下一个拦截器对应的RealIterceptorChain对象 - RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec, - connection, index + 1, request, call, eventListener, connectTimeout, readTimeout, - writeTimeout); - // 得到当前的拦截器 - Interceptor interceptor = interceptors.get(index); - // 调用当前拦截器的intercept()方法,并将下一个拦截器的RealIterceptorChain对象传递下去,最后得到响应 - Response response = interceptor.intercept(next); - - ... - - return response; -} -``` - -### 异步请求流程 - -``` -Request request = new Request.Builder() - .url("http://publicobject.com/helloworld.txt") - .build(); - -client.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { - e.printStackTrace(); - } - - @Override - public void onResponse(Call call, Response response) throws IOException { - ... - } - -void enqueue(AsyncCall call) { - synchronized (this) { - readyAsyncCalls.add(call); - } - promoteAndExecute(); -} - -// 正在准备中的异步请求队列 -private final Deque readyAsyncCalls = new ArrayDeque<>(); - -// 运行中的异步请求 -private final Deque runningAsyncCalls = new ArrayDeque<>(); - -// 同步请求 -private final Deque runningSyncCalls = new ArrayDeque<>(); - -// Promotes eligible calls from {@link #readyAsyncCalls} to {@link #runningAsyncCalls} and runs -// them on the executor service. Must not be called with synchronization because executing calls -// can call into user code. -private boolean promoteAndExecute() { - assert (!Thread.holdsLock(this)); - - List executableCalls = new ArrayList<>(); - boolean isRunning; - synchronized (this) { - for (Iterator i = readyAsyncCalls.iterator(); i.hasNext(); ) { - AsyncCall asyncCall = i.next(); - - // 如果其中的runningAsynCalls不满,且call占用的host小于最大数量,则将call加入到runningAsyncCalls中执行, - // 同时利用线程池执行call;否者将call加入到readyAsyncCalls中。 - if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity. - if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity. - - i.remove(); - executableCalls.add(asyncCall); - runningAsyncCalls.add(asyncCall); - } - isRunning = runningCallsCount() > 0; - } - - for (int i = 0, size = executableCalls.size(); i < size; i++) { - AsyncCall asyncCall = executableCalls.get(i); - asyncCall.executeOn(executorService()); - } - - return isRunning; -} -``` - -最后,我们在看看AsynCall的代码。 - -``` -final class AsyncCall extends NamedRunnable { - private final Callback responseCallback; - - AsyncCall(Callback responseCallback) { - super("OkHttp %s", redactedUrl()); - this.responseCallback = responseCallback; - } - - String host() { - return originalRequest.url().host(); - } - - Request request() { - return originalRequest; - } - - RealCall get() { - return RealCall.this; - } - - /** - * Attempt to enqueue this async call on {@code executorService}. This will attempt to clean up - * if the executor has been shut down by reporting the call as failed. - */ - void executeOn(ExecutorService executorService) { - assert (!Thread.holdsLock(client.dispatcher())); - boolean success = false; - try { - executorService.execute(this); - success = true; - } catch (RejectedExecutionException e) { - InterruptedIOException ioException = new InterruptedIOException("executor rejected"); - ioException.initCause(e); - eventListener.callFailed(RealCall.this, ioException); - responseCallback.onFailure(RealCall.this, ioException); - } finally { - if (!success) { - client.dispatcher().finished(this); // This call is no longer running! - } - } - } - - @Override protected void execute() { - boolean signalledCallback = false; - timeout.enter(); - try { - // 跟同步执行一样,最后都会调用到这里 - Response response = getResponseWithInterceptorChain(); - if (retryAndFollowUpInterceptor.isCanceled()) { - signalledCallback = true; - responseCallback.onFailure(RealCall.this, new IOException("Canceled")); - } else { - signalledCallback = true; - responseCallback.onResponse(RealCall.this, response); - } - } catch (IOException e) { - e = timeoutExit(e); - if (signalledCallback) { - // Do not signal the callback twice! - Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e); - } else { - eventListener.callFailed(RealCall.this, e); - responseCallback.onFailure(RealCall.this, e); - } - } finally { - client.dispatcher().finished(this); - } - } -} -``` - -从上面的源码可以知道,拦截链的处理OKHttp帮我们默认做了五步拦截处理,其中RetryAndFollowUpInterceptor、BridgeInterceptor、CallServerInterceptor内部的源码很简洁易懂,此处不再多说。 - -## 网络请求缓存处理之CacheInterceptor - -``` -@Override public Response intercept(Chain chain) throws IOException { - // 根据request得到cache中缓存的response - Response cacheCandidate = cache != null - ? cache.get(chain.request()) - : null; - - long now = System.currentTimeMillis(); - - // request判断缓存的策略,是否要使用了网络,缓存或两者都使用 - CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); - Request networkRequest = strategy.networkRequest; - Response cacheResponse = strategy.cacheResponse; - - if (cache != null) { - cache.trackResponse(strategy); - } - - if (cacheCandidate != null && cacheResponse == null) { - closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it. - } - - // If we're forbidden from using the network and the cache is insufficient, fail. - if (networkRequest == null && cacheResponse == null) { - return new Response.Builder() - .request(chain.request()) - .protocol(Protocol.HTTP_1_1) - .code(504) - .message("Unsatisfiable Request (only-if-cached)") - .body(Util.EMPTY_RESPONSE) - .sentRequestAtMillis(-1L) - .receivedResponseAtMillis(System.currentTimeMillis()) - .build(); - } - - // If we don't need the network, we're done. - if (networkRequest == null) { - return cacheResponse.newBuilder() - .cacheResponse(stripBody(cacheResponse)) - .build(); - } - - Response networkResponse = null; - try { - // 调用下一个拦截器,决定从网络上来得到response - networkResponse = chain.proceed(networkRequest); - } finally { - // If we're crashing on I/O or otherwise, don't leak the cache body. - if (networkResponse == null && cacheCandidate != null) { - closeQuietly(cacheCandidate.body()); - } - } - - // If we have a cache response too, then we're doing a conditional get. - // 如果本地已经存在cacheResponse,那么让它和网络得到的networkResponse做比较,决定是否来更新缓存的cacheResponse - if (cacheResponse != null) { - if (networkResponse.code() == HTTP_NOT_MODIFIED) { - Response response = cacheResponse.newBuilder() - .headers(combine(cacheResponse.headers(), networkResponse.headers())) - .sentRequestAtMillis(networkResponse.sentRequestAtMillis()) - .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis()) - .cacheResponse(stripBody(cacheResponse)) - .networkResponse(stripBody(networkResponse)) - .build(); - networkResponse.body().close(); - - // Update the cache after combining headers but before stripping the - // Content-Encoding header (as performed by initContentStream()). - cache.trackConditionalCacheHit(); - cache.update(cacheResponse, response); - return response; - } else { - closeQuietly(cacheResponse.body()); - } - } - - Response response = networkResponse.newBuilder() - .cacheResponse(stripBody(cacheResponse)) - .networkResponse(stripBody(networkResponse)) - .build(); - - if (cache != null) { - if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) { - // Offer this request to the cache. - // 缓存未经缓存过的response - CacheRequest cacheRequest = cache.put(response); - return cacheWritingResponse(cacheRequest, response); - } - - if (HttpMethod.invalidatesCache(networkRequest.method())) { - try { - cache.remove(networkRequest); - } catch (IOException ignored) { - // The cache cannot be written. - } - } - } - - return response; -} -``` - -缓存拦截器会根据请求的信息和缓存的响应的信息来判断是否存在缓存可用,如果有可以使用的缓存,那么就返回该缓存给用户,否则就继续使用责任链模式来从服务器中获取响应。当获取到响应的时候,又会把响应缓存到磁盘上面。 - -## ConnectInterceptor之连接池 - -``` -@Override public Response intercept(Chain chain) throws IOException { - RealInterceptorChain realChain = (RealInterceptorChain) chain; - Request request = realChain.request(); - StreamAllocation streamAllocation = realChain.streamAllocation(); - - // We need the network to satisfy this request. Possibly for validating a conditional GET. - boolean doExtensiveHealthChecks = !request.method().equals("GET"); - // HttpCodec是对 HTTP 协议操作的抽象,有两个实现:Http1Codec和Http2Codec,顾名思义,它们分别对应 HTTP/1.1 和 HTTP/2 版本的实现。在这个方法的内部实现连接池的复用处理 - HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks); - RealConnection connection = streamAllocation.connection(); - - return realChain.proceed(request, streamAllocation, httpCodec, connection); -} - - - -// Returns a connection to host a new stream. This // prefers the existing connection if it exists, -// then the pool, finally building a new connection. -// 调用 streamAllocation 的 newStream() 方法的时候,最终会经过一系列 -// 的判断到达 StreamAllocation 中的 findConnection() 方法 -private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, - int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException { - ... - - // Attempt to use an already-allocated connection. We need to be careful here because our - // already-allocated connection may have been restricted from creating new streams. - // 尝试使用已分配的连接,已经分配的连接可能已经被限制创建新的流 - releasedConnection = this.connection; - // 释放当前连接的资源,如果该连接已经被限制创建新的流,就返回一个Socket以关闭连接 - toClose = releaseIfNoNewStreams(); - if (this.connection != null) { - // We had an already-allocated connection and it's good. - result = this.connection; - releasedConnection = null; - } - if (!reportedAcquired) { - // If the connection was never reported acquired, don't report it as released! - // 如果该连接从未被标记为获得,不要标记为发布状态,reportedAcquired 通过 acquire() 方法修改 - releasedConnection = null; - } - - if (result == null) { - // Attempt to get a connection from the pool. - // 尝试供连接池中获取一个连接 - Internal.instance.get(connectionPool, address, this, null); - if (connection != null) { - foundPooledConnection = true; - result = connection; - } else { - selectedRoute = route; - } - } - } - // 关闭连接 - closeQuietly(toClose); - - if (releasedConnection != null) { - eventListener.connectionReleased(call, releasedConnection); - } - if (foundPooledConnection) { - eventListener.connectionAcquired(call, result); - } - if (result != null) { - // If we found an already-allocated or pooled connection, we're done. - // 如果已经从连接池中获取到了一个连接,就将其返回 - return result; - } - - // If we need a route selection, make one. This is a blocking operation. - boolean newRouteSelection = false; - if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) { - newRouteSelection = true; - routeSelection = routeSelector.next(); - } - - synchronized (connectionPool) { - if (canceled) throw new IOException("Canceled"); - - if (newRouteSelection) { - // Now that we have a set of IP addresses, make another attempt at getting a connection from - // the pool. This could match due to connection coalescing. - // 根据一系列的 IP地址从连接池中获取一个链接 - List routes = routeSelection.getAll(); - for (int i = 0, size = routes.size(); i < size;i++) { - Route route = routes.get(i); - // 从连接池中获取一个连接 - Internal.instance.get(connectionPool, address, this, route); - if (connection != null) { - foundPooledConnection = true; - result = connection; - this.route = route; - break; - } - } - } - - if (!foundPooledConnection) { - if (selectedRoute == null) { - selectedRoute = routeSelection.next(); - } - - // Create a connection and assign it to this allocation immediately. This makes it possible - // for an asynchronous cancel() to interrupt the handshake we're about to do. - // 在连接池中如果没有该连接,则创建一个新的连接,并将其分配,这样我们就可以在握手之前进行终端 - route = selectedRoute; - refusedStreamCount = 0; - result = new RealConnection(connectionPool, selectedRoute); - acquire(result, false); - } - } - // If we found a pooled connection on the 2nd time around, we're done. - if (foundPooledConnection) { - // 如果我们在第二次的时候发现了一个池连接,那么我们就将其返回 - eventListener.connectionAcquired(call, result); - return result; - } - - // Do TCP + TLS handshakes. This is a blocking operation. - // 进行 TCP 和 TLS 握手 - result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, - connectionRetryEnabled, call, eventListener); - routeDatabase().connected(result.route()); - - Socket socket = null; - synchronized (connectionPool) { - reportedAcquired = true; - - // Pool the connection. - // 将该连接放进连接池中 - Internal.instance.put(connectionPool, result); - - // If another multiplexed connection to the same address was created concurrently, then - // release this connection and acquire that one. - // 如果同时创建了另一个到同一地址的多路复用连接,释放这个连接并获取那个连接 - if (result.isMultiplexed()) { - socket = Internal.instance.deduplicate(connectionPool, address, this); - result = connection; - } - } - closeQuietly(socket); - - eventListener.connectionAcquired(call, result); - return result; -} -``` - -从以上的源码分析可知: - -- 判断当前的连接是否可以使用:流是否已经被关闭,并且已经被限制创建新的流; -- 如果当前的连接无法使用,就从连接池中获取一个连接; -- 连接池中也没有发现可用的连接,创建一个新的连接,并进行握手,然后将其放到连接池中。 - -在从连接池中获取一个连接的时候,使用了 Internal 的 get() 方法。Internal 有一个静态的实例,会在 OkHttpClient 的静态代码快中被初始化。我们会在 Internal 的 get() 中调用连接池的 get() 方法来得到一个连接。并且,从中我们明白了连接复用的一个好处就是省去了进行 TCP 和 TLS 握手的一个过程。因为建立连接本身也是需要消耗一些时间的,连接被复用之后可以提升我们网络访问的效率。 - -接下来详细分析下ConnectionPool是如何实现连接管理的。 - -OkHttp 的缓存管理分成两个步骤,一边当我们创建了一个新的连接的时候,我们要把它放进缓存里面;另一边,我们还要来对缓存进行清理。在 ConnectionPool 中,当我们向连接池中缓存一个连接的时候,只要调用双端队列的 add() 方法,将其加入到双端队列即可,而清理连接缓存的操作则交给线程池来定时执行。 - -``` -private final Deque connections = new ArrayDeque<>(); - -void put(RealConnection connection) { -assert (Thread.holdsLock(this)); - if (!cleanupRunning) { - cleanupRunning = true; - // 使用线程池执行清理任务 - executor.execute(cleanupRunnable); - } - // 将新建的连接插入到双端队列中 - connections.add(connection); -} - - private final Runnable cleanupRunnable = new Runnable() { -@Override public void run() { - while (true) { - // 内部调用 cleanup() 方法来清理无效的连接 - long waitNanos = cleanup(System.nanoTime()); - if (waitNanos == -1) return; - if (waitNanos > 0) { - long waitMillis = waitNanos / 1000000L; - waitNanos -= (waitMillis * 1000000L); - synchronized (ConnectionPool.this) { - try { - ConnectionPool.this.wait(waitMillis, (int) waitNanos); - } catch (InterruptedException ignored) { - } - } - } - } -}; - -long cleanup(long now) { - int inUseConnectionCount = 0; - int idleConnectionCount = 0; - RealConnection longestIdleConnection = null; - long longestIdleDurationNs = Long.MIN_VALUE; - - // Find either a connection to evict, or the time that the next eviction is due. - synchronized (this) { - // 遍历所有的连接 - for (Iterator i = connections.iterator(); i.hasNext(); ) { - RealConnection connection = i.next(); - - // If the connection is in use, keep searching. - // 遍历所有的连接 - if (pruneAndGetAllocationCount(connection, now) > 0) { - inUseConnectionCount++; - continue; - } - - idleConnectionCount++; - - // If the connection is ready to be evicted, we're done. - // 如果找到了一个可以被清理的连接,会尝试去寻找闲置时间最久的连接来释放 - long idleDurationNs = now - connection.idleAtNanos; - if (idleDurationNs > longestIdleDurationNs) { - longestIdleDurationNs = idleDurationNs; - longestIdleConnection = connection; - } - } - - // maxIdleConnections 表示最大允许的闲置的连接的数量,keepAliveDurationNs表示连接允许存活的最长的时间。 - // 默认空闲连接最大数目为5个,keepalive 时间最长为5分钟。 - if (longestIdleDurationNs >= this.keepAliveDurationNs - || idleConnectionCount > this.maxIdleConnections) { - // We've found a connection to evict. Remove it from the list, then close it below (outside - // of the synchronized block). - // 该连接的时长超出了最大的活跃时长或者闲置的连接数量超出了最大允许的范围,直接移除 - connections.remove(longestIdleConnection); - } else if (idleConnectionCount > 0) { - // A connection will be ready to evict soon. - // 闲置的连接的数量大于0,停顿指定的时间(等会儿会将其清理掉,现在还不是时候) - return keepAliveDurationNs - longestIdleDurationNs; - } else if (inUseConnectionCount > 0) { - // All connections are in use. It'll be at least the keep alive duration 'til we run again. - // 所有的连接都在使用中,5分钟后再清理 - return keepAliveDurationNs; - } else { - // No connections, idle or in use. - // 没有连接 - cleanupRunning = false; - return -1; - } -} -``` - -从以上的源码分析可知,首先会对缓存中的连接进行遍历,以寻找一个闲置时间最长的连接,然后根据该连接的闲置时长和最大允许的连接数量等参数来决定是否应该清理该连接。同时注意上面的方法的返回值是一个时间,如果闲置时间最长的连接仍然需要一段时间才能被清理的时候,会返回这段时间的时间差,然后会在这段时间之后再次对连接池进行清理。 - - - -> 经过上面对OKHttp内部工作机制的一系列分析,相信你已经对OKHttp已经有了一个比较深入的了解了。首先,我们会在请求的时候初始化一个Call的实例,然后执行它的execute()方法或enqueue()方法,内部最后都会执行到getResponseWithInterceptorChain()方法,这个方法里面通过拦截器组成的责任链,依次经过用户自定义普通拦截器、重试拦截器、桥接拦截器、缓存拦截器、连接拦截器和用户自定义网络拦截器以及访问服务器拦截器等拦截处理过程,来获取到一个响应并交给用户。 -> -> 其中,除了OKHttp的内部请求流程这点之外,缓存和连接这两部分内容也是两个很重要的点,相信经过讲解,大家对这三部分重点内容已经有了自己的理解。 - -# Retrofit - -## 基本使用流程 - -### 定义HTTP API,用于描述请求 - -``` -public interface GitHubService { - - @GET("users/{user}/repos") - Call> listRepos(@Path("user") String user); -} -``` - -### 创建Retrofit并生成API的实现 - -> (**注意:** 方法上面的注解表示请求的接口部分,返回类型是请求的返回值类型,方法的参数即是请求的参数) - -``` -// 1.Retrofit构建过程 -Retrofit retrofit = new Retrofit.Builder() -.baseUrl("https://api.github.com/") -.build(); - -// 2.创建网络请求接口类实例过程 -GitHubService service = retrofit.create(GitHubService.class); -``` - -### 调用API方法,生成Call,执行请求 - -``` -// 3.生成并执行请求过程 -Call> repos = service.listRepos("octocat"); -repos.execute() or repos.enqueue() -``` - -Retrofit的基本使用流程很简洁,但是简洁并不代表简单,Retrofit为了实现这种简洁的使用流程,内部使用了优秀的架构设计和大量的设计模式,在分析过Retrofit最新版的源码和大量优秀的Retrofit源码分析文章后发现,要想真正理解Retrofit内部的核心源码流程和设计思想,首先,需要对这九大设计模式有一定的了解,如下: - -``` -1.Retrofit构建过程 -建造者模式、工厂方法模式 - -2.创建网络请求接口实例过程 -外观模式、代理模式、单例模式、策略模式、装饰模式(建造者模式) - -3.生成并执行请求过程 -适配器模式(代理模式、装饰模式) -``` - -其次,需要对OKHttp源码有一定的了解。让我们按以上流程去深入Retrofit源码内部,领悟它带给我们的**设计之美**。 - -## Retrofit构建过程 - -### Retrofit核心对象解析 - -首先Retrofit中有一个全局变量非常关键,在V2.5之前的版本,使用的是LinkedHashMap(),它是一个网络请求配置对象,是由网络请求接口中方法注解进行解析后得到的。 - -``` -public final class Retrofit { - - // 网络请求配置对象,存储网络请求相关的配置,如网络请求的方法、数据转换器、网络请求适配器、网络请求工厂、基地址等 - private final Map> serviceMethodCache = new ConcurrentHashMap<>(); -``` - -Retrofit使用了建造者模式通过内部类Builder类建立一个Retrofit实例,如下: - -``` -public static final class Builder { - - // 平台类型对象(Platform -> Android) - private final Platform platform; - // 网络请求工厂,默认使用OkHttpCall(工厂方法模式) - private @Nullable okhttp3.Call.Factory callFactory; - // 网络请求的url地址 - private @Nullable HttpUrl baseUrl; - // 数据转换器工厂的集合 - private final List converterFactories = new ArrayList<>(); - // 网络请求适配器工厂的集合,默认是ExecutorCallAdapterFactory - private final List callAdapterFactories = new ArrayList<>(); - // 回调方法执行器,在 Android 上默认是封装了 handler 的 MainThreadExecutor, 默认作用是:切换线程(子线程 -> 主线程) - private @Nullable Executor callbackExecutor; - // 一个开关,为true则会缓存创建的ServiceMethod - private boolean validateEagerly; -``` - -### Builder内部构造 - -下面看看Builder内部构造做了什么。 - -``` -public static final class Builder { - - ... - - Builder(Platform platform) { - this.platform = platform; - } - - - public Builder() { - this(Platform.get()); - } - - ... - -} - - -class Platform { - - private static final Platform PLATFORM = findPlatform(); - - static Platform get() { - return PLATFORM; - } - - private static Platform findPlatform() { - try { - // 使用JVM加载类的方式判断是否是Android平台 - Class.forName("android.os.Build"); - if (Build.VERSION.SDK_INT != 0) { - return new Android(); - } - } catch (ClassNotFoundException ignored) { - } - try { - // 同时支持Java平台 - Class.forName("java.util.Optional"); - return new Java8(); - } catch (ClassNotFoundException ignored) { - } - return new Platform(); - } - -static class Android extends Platform { - - ... - - - @Override public Executor defaultCallbackExecutor() { - //切换线程(子线程 -> 主线程) - return new MainThreadExecutor(); - } - - // 创建默认的网络请求适配器工厂,如果是Android7.0或Java8上,则使 - // 用了并发包中的CompletableFuture保证了回调的同步 - // 在Retrofit中提供了四种CallAdapterFactory(策略模式): - // ExecutorCallAdapterFactory(默认)、GuavaCallAdapterFactory、 - // va8CallAdapterFactory、RxJavaCallAdapterFactory - @Override List defaultCallAdapterFactories( - @Nullable Executor callbackExecutor) { - if (callbackExecutor == null) throw new AssertionError(); - ExecutorCallAdapterFactory executorFactory = new ExecutorCallAdapterFactory(callbackExecutor); - return Build.VERSION.SDK_INT >= 24 - ? asList(CompletableFutureCallAdapterFactory.INSTANCE, executorFactory) - : singletonList(executorFactory); - } - - ... - - @Override List defaultConverterFactories() { - return Build.VERSION.SDK_INT >= 24 - ? singletonList(OptionalConverterFactory.INSTANCE) - : Collections.emptyList(); - } - - ... - - static class MainThreadExecutor implements Executor { - - // 获取Android 主线程的Handler - private final Handler handler = new Handler(Looper.getMainLooper()); - - @Override public void execute(Runnable r) { - - // 在UI线程对网络请求返回数据处理 - handler.post(r); - } - } -} -``` - -可以看到,在Builder内部构造时设置了默认Platform、callAdapterFactories和callbackExecutor。 - -### 添加baseUrl - -很简单,就是将String类型的url转换为OkHttp的HttpUrl过程如下: - -``` -/** - * Set the API base URL. - * - * @see #baseUrl(HttpUrl) - */ -public Builder baseUrl(String baseUrl) { - checkNotNull(baseUrl, "baseUrl == null"); - return baseUrl(HttpUrl.get(baseUrl)); -} - -public Builder baseUrl(HttpUrl baseUrl) { - checkNotNull(baseUrl, "baseUrl == null"); - List pathSegments = baseUrl.pathSegments(); - if (!"".equals(pathSegments.get(pathSegments.size() - 1))) { - throw new IllegalArgumentException("baseUrl must end in /: " + baseUrl); - } - this.baseUrl = baseUrl; - return this; -} -``` - -### 添加GsonConverterFactory - -首先,看到GsonConverterFactory.creat()的源码。 - -``` -public final class GsonConverterFactory extends Converter.Factory { - - public static GsonConverterFactory create() { - return create(new Gson()); - } - - - public static GsonConverterFactory create(Gson gson) { - if (gson == null) throw new NullPointerException("gson == null"); - return new GsonConverterFactory(gson); - } - - private final Gson gson; - - // 创建了一个含有Gson对象实例的GsonConverterFactory - private GsonConverterFactory(Gson gson) { - this.gson = gson; - } - -``` - -然后,看看addConverterFactory()方法内部。 - -``` -public Builder addConverterFactory(Converter.Factory factory) { - converterFactories.add(checkNotNull(factory, "factory null")); - return this; -} -``` - -可知,这一步是将一个含有Gson对象实例的GsonConverterFactory放入到了数据转换器工厂converterFactories里。 - -### build过程 - -``` -public Retrofit build() { - - if (baseUrl == null) { - throw new IllegalStateException("Base URL required."); - } - - okhttp3.Call.Factory callFactory = this.callFactory; - if (callFactory == null) { - // 默认使用okhttp - callFactory = new OkHttpClient(); - } - - Executor callbackExecutor = this.callbackExecutor; - if (callbackExecutor == null) { - // Android默认的callbackExecutor - callbackExecutor = platform.defaultCallbackExecutor(); - } - - // Make a defensive copy of the adapters and add the defaultCall adapter. - List callAdapterFactories = new ArrayList<>(this.callAdapterFactories); - // 添加默认适配器工厂在集合尾部 - callAdapterFactories.addAll(platform.defaultCallAdapterFactorisca llbackExecutor)); - - // Make a defensive copy of the converters. - List converterFactories = new ArrayList<>( - 1 + this.converterFactories.size() + platform.defaultConverterFactoriesSize()); - // Add the built-in converter factory first. This prevents overriding its behavior but also - // ensures correct behavior when using converters thatconsumeall types. - converterFactories.add(new BuiltInConverters()); - converterFactories.addAll(this.converterFactories); - converterFactories.addAll(platform.defaultConverterFactories(); - - return new Retrofit(callFactory, baseUrl, unmodifiableList(converterFactories), - unmodifiableList(callAdapterFactories), callbackExecutor, validateEagerly); - -} -``` - -可以看到,最终我们在Builder类中看到的6大核心对象都已经配置到Retrofit对象中了。 - -## 创建网络请求接口实例过程 - -retrofit.create()使用了外观模式和代理模式创建了网络请求的接口实例,我们分析下create方法。 - -``` -public T create(final Class service) { - Utils.validateServiceInterface(service); - if (validateEagerly) { - // 判断是否需要提前缓存ServiceMethod对象 - eagerlyValidateMethods(service); - } - - // 使用动态代理拿到请求接口所有注解配置后,创建网络请求接口实例 - return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class[] { service }, - new InvocationHandler() { - private final Platform platform = Platform.get(); - private final Object[] emptyArgs = new Object[0]; - - @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args) - throws Throwable { - // If the method is a method from Object then defer to normal invocation. - if (method.getDeclaringClass() == Object.class) { - return method.invoke(this, args); - } - if (platform.isDefaultMethod(method)) { - return platform.invokeDefaultMethod(method, service, proxy, args); - } - return loadServiceMethod(method).invoke(args != null ? args : emptyArgs); - } - }); - } - -private void eagerlyValidateMethods(Class service) { - - Platform platform = Platform.get(); - for (Method method : service.getDeclaredMethods()) { - if (!platform.isDefaultMethod(method)) { - loadServiceMethod(method); - } - } -} -``` - -继续看看loadServiceMethod的内部流程 - -``` -ServiceMethod loadServiceMethod(Method method) { - - ServiceMethod result = serviceMethodCache.get(method); - if (result != null) return result; - - synchronized (serviceMethodCache) { - result = serviceMethodCache.get(method); - if (result == null) { - // 解析注解配置得到了ServiceMethod - result = ServiceMethod.parseAnnotations(this, method); - // 可以看到,最终加入到ConcurrentHashMap缓存中 - serviceMethodCache.put(method, result); - } - } - return result; -} - - -abstract class ServiceMethod { - static ServiceMethod parseAnnotations(Retrofit retrofit, Method method) { - // 通过RequestFactory解析注解配置(工厂模式、内部使用了建造者模式) - RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method); - - Type returnType = method.getGenericReturnType(); - if (Utils.hasUnresolvableType(returnType)) { - throw methodError(method, - "Method return type must not include a type variable or wildcard: %s", returnType); - } - if (returnType == void.class) { - throw methodError(method, "Service methods cannot return void."); - } - - // 最终是通过HttpServiceMethod构建的请求方法 - return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory); - } - - abstract T invoke(Object[] args); -} -``` - -### 请求构造核心流程 - -根据RequestFactory#Builder构造方法和parseAnnotations方法的源码,可知的它的作用就是用来解析注解配置的。 - -``` -Builder(Retrofit retrofit, Method method) { - this.retrofit = retrofit; - this.method = method; - // 获取网络请求接口方法里的注释 - this.methodAnnotations = method.getAnnotations(); - // 获取网络请求接口方法里的参数类型 - this.parameterTypes = method.getGenericParameterTypes(); - // 获取网络请求接口方法里的注解内容 - this.parameterAnnotationsArray = method.getParameterAnnotations(); -} -``` - -接着看HttpServiceMethod.parseAnnotations()的内部流程。 - -``` -static HttpServiceMethod parseAnnotations( - Retrofit retrofit, Method method, RequestFactory requestFactory) { - - //1.根据网络请求接口方法的返回值和注解类型, - // 从Retrofit对象中获取对应的网络请求适配器 - CallAdapter callAdapter = createCallAdapter(retrofit,method); - - // 得到响应类型 - Type responseType = callAdapter.responseType(); - - ... - - //2.根据网络请求接口方法的返回值和注解类型从Retrofit对象中获取对应的数据转换器 - ConverterresponseConverter = - createResponseConverter(retrofit,method, responseType); - - okhttp3.Call.Factory callFactory = retrofit.callFactory; - - return newHttpServiceMethod<>(requestFactory, callFactory, callAdapter,responseConverter); -} -``` - -#### createCallAdapter(retrofit, method) - -``` -private static CallAdapter createCallAdapter( - Retrofit retrofit, Method method) { - - // 获取网络请求接口里方法的返回值类型 - Type returnType = method.getGenericReturnType(); - - // 获取网络请求接口接口里的注解 - Annotation[] annotations = method.getAnnotations(); - try { - //noinspection unchecked - return (CallAdapter) retrofit.callAdapter(returnType, annotations); - } catch (RuntimeException e) { // Wide exception range because factories are user code. - throw methodError(method, e, "Unable to create call adapter for %s", returnType); - } -} - -public CallAdapter callAdapter(Type returnType, Annotation[] annotations) { - return nextCallAdapter(null, returnType, annotations); -} - -public CallAdapter nextCallAdapter(@Nullable CallAdapter.Factory skipPast, Type returnType, - Annotation[] annotations) { - ... - - int start = callAdapterFactories.indexOf(skipPast) + 1; - // 遍历 CallAdapter.Factory 集合寻找合适的工厂 - for (int i = start, count = callAdapterFactories.size(); i adapter = callAdapterFactories.get(i).get(returnType, annotations, this); - if (adapter != null) { - return adapter; - } - } -} -``` - -#### createResponseConverter(Retrofit retrofit, Method method, Type responseType) - -``` - private static Converter createResponseConverter( - Retrofit retrofit, Method method, Type responseType) { - Annotation[] annotations = method.getAnnotations(); - try { - return retrofit.responseBodyConverter(responseType,annotations); - } catch (RuntimeException e) { // Wide exception range because factories are user code. - throw methodError(method, e, "Unable to create converter for%s", responseType); - } -} - -public Converter responseBodyConverter(Type type, Annotation[] annotations) { - return nextResponseBodyConverter(null, type, annotations); -} - -public Converter nextResponseBodyConverter( - @Nullable Converter.Factory skipPast, Type type, Annotation[] annotations) { -... - -int start = converterFactories.indexOf(skipPast) + 1; -// 遍历 Converter.Factory 集合并寻找合适的工厂, 这里是GsonResponseBodyConverter -for (int i = start, count = converterFactories.size(); i < count; i++) { - Converter converter = - converterFactories.get(i).responseBodyConverter(type, annotations, this); - if (converter != null) { - //noinspection unchecked - return (Converter) converter; - } -} -``` - -#### 执行HttpServiceMethod的invoke方法 - -``` -@Override ReturnT invoke(Object[] args) { - return callAdapter.adapt( - new OkHttpCall<>(requestFactory, args, callFactory, responseConverter)); -} -``` - -最终在adapt中创建了一个ExecutorCallbackCall对象,它是一个装饰者,而在它内部真正去执行网络请求的还是OkHttpCall。 - -## 创建网络请求接口类实例并执行请求过程 - -### service.listRepos() - -``` -1、Call> repos = service.listRepos("octocat"); -``` - -service对象是动态代理对象Proxy.newProxyInstance(),当调用getCall()时会被 它拦截,然后调用自身的InvocationHandler#invoke(),得到最终的Call对象。 - -### 同步执行流程 repos.execute() - -``` -@Override public Response execute() throws IOException { - okhttp3.Call call; - - synchronized (this) { - if (executed) throw new IllegalStateException("Already executed."); - executed = true; - - if (creationFailure != null) { - if (creationFailure instanceof IOException) { - throw (IOException) creationFailure; - } else if (creationFailure instanceof RuntimeException) { - throw (RuntimeException) creationFailure; - } else { - throw (Error) creationFailure; - } - } - - call = rawCall; - if (call == null) { - try { - // 创建一个OkHttp的Request对象请求 - call = rawCall = createRawCall(); - } catch (IOException | RuntimeException | Error e) { - throwIfFatal(e); // Do not assign a fatal error to creationFailure. - creationFailure = e; - throw e; - } - } - } - - if (canceled) { - call.cancel(); - } - - // 调用OkHttpCall的execute()发送网络请求(同步), - // 并解析网络请求返回的数据 - return parseResponse(call.execute()); -} - - -private okhttp3.Call createRawCall() throws IOException { - // 创建 一个okhttp3.Request - okhttp3.Call call = - callFactory.newCall(requestFactory.create(args)); - if (call == null) { - throw new NullPointerException("Call.Factory returned null."); - } - return call; -} - - -Response parseResponse(okhttp3.Response rawResponse) throws IOException { - ResponseBody rawBody = rawResponse.body(); - - // Remove the body's source (the only stateful object) so we can pass the response along. - rawResponse = rawResponse.newBuilder() - .body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength())) - .build(); - - // 根据响应返回的状态码进行处理 - int code = rawResponse.code(); - if (code < 200 || code >= 300) { - try { - // Buffer the entire body to avoid future I/O. - ResponseBody bufferedBody = Utils.buffer(rawBody); - return Response.error(bufferedBody, rawResponse); - } finally { - rawBody.close(); - } - } - if (code == 204 || code == 205) { - rawBody.close(); - return Response.success(null, rawResponse); - } - - - ExceptionCatchingResponseBody catchingBody = new ExceptionCatchingResponseBody(rawBody); - try { - // 将响应体转为Java对象 - T body = responseConverter.convert(catchingBody); - - return Response.success(body, rawResponse); - } catch (RuntimeException e) { - // If the underlying source threw an exception, propagate that rather than indicating it was - // a runtime exception. - catchingBody.throwIfCaught(); - throw e; - } -} -``` - -### 异步请求流程 reponse.enqueque - -``` -@Override -public void enqueue(final Callback callback) { - - // 使用静态代理 delegate进行异步请求 - delegate.enqueue(new Callback() { - - @Override - public void onResponse(Call call, finalResponseresponse) { - // 线程切换,在主线程显示结果 - callbackExecutor.execute(new Runnable() { - @Override - public void run() { - if (delegate.isCanceled()) { - callback.onFailure(ExecutorCallbackCall.this, newIOException("Canceled")); - } else { - callback.onResponse(ExecutorCallbackCall.this,respons); - } - } - }); - } - @Override - public void onFailure(Call call, final Throwable t) { - callbackExecutor.execute(new Runnable() { - @Override public void run() { - callback.onFailure(ExecutorCallbackCall.this, t); - } - }); - } - }); -} -``` - -看看 delegate.enqueue 内部流程。 - -``` -@Override -public void enqueue(final Callback callback) { - - okhttp3.Call call; - Throwable failure; - - synchronized (this) { - if (executed) throw new IllegalStateException("Already executed."); - executed = true; - - call = rawCall; - failure = creationFailure; - if (call == null && failure == null) { - try { - // 创建OkHttp的Request对象,再封装成OkHttp.call - // 方法同发送同步请求,此处上面已分析 - call = rawCall = createRawCall(); - } catch (Throwable t) { - failure = creationFailure = t; - } - } - -@Override public void enqueue(final Callback callback) { - checkNotNull(callback, "callback == null"); - - okhttp3.Call call; - Throwable failure; - - ... - - call.enqueue(new okhttp3.Callback() { - @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) { - Response response; - try { - // 此处上面已分析 - response = parseResponse(rawResponse); - } catch (Throwable e) { - throwIfFatal(e); - callFailure(e); - return; - } - - try { - callback.onResponse(OkHttpCall.this, response); - } catch (Throwable t) { - t.printStackTrace(); - } - } - - @Override public void onFailure(okhttp3.Call call, IOException e) { - callFailure(e); - } - - private void callFailure(Throwable e) { - try { - callback.onFailure(OkHttpCall.this, e); - } catch (Throwable t) { - t.printStackTrace(); - } - } - }); -} -``` - -## Retrofit源码流程图 - -建议大家自己主动配合着Retrofit最新版的源码一步步去彻底地认识它,只有这样,你才能看到它真实的内心,附上一张Retrofit源码流程图,要注意的是,这是V2.5之前版本的流程,但是,在看完上面的源码分析后,我们知道,主体流程是没有变化的。 - -![image](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7ab58d6242cc4fae96cd73eb198e325a~tplv-k3u1fbpfcp-zoom-1.image) - - - -> 从本质上来说,Retrofit虽然只是一个RESTful 的HTTP 网络请求框架的封装库。但是,它内部通过 大量的设计模式 封装了 OkHttp,让使用者感到它非常简洁、易懂。它内部主要是用动态代理的方式,动态将网络请求接口的注解解析成HTTP请求,最后执行请求的过程。 - -# Glide - -## 基本使用流程 - -Glide最基本的使用流程就是下面这行代码,其它所有扩展的额外功能都是以其建造者链式调用的基础上增加的。 - -``` -GlideApp.with(context).load(url).into(iv); -``` - -其中的GlideApp是注解处理器自动生成的,要使用GlideApp,必须先配置应用的AppGlideModule模块,里面可以为空配置,也可以根据实际情况添加指定配置。 - -``` -@GlideModule -public class MyAppGlideModule extends AppGlideModule { - - @Override - public void applyOptions(Context context, GlideBuilder builder) { - // 实际使用中根据情况可以添加如下配置 - - - - - - - - } -} -``` - -接下来,本文将针对Glide的最新源码版本V4.8.0对Glide加载网络图片的流程进行详细地分析与讲解,力争做到让读者朋友们知其然也知其所以然。 - -## GlideApp.with(context)源码详解 - -首先,用这份Glide框架图让我们对Glide的总体框架有一个初步的了解。 - -![image](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2478933d44534c2fabb6466049b9f3b7~tplv-k3u1fbpfcp-zoom-1.image) - -从GlideApp.with这行代码开始,内部主线执行流程如下。 - -### GlideApp#with - -``` -return (GlideRequests) Glide.with(context); -``` - -### Glide#with - -``` -return getRetriever(context).get(context); - -return Glide.get(context).getRequestManagerRetriever(); - -// 外部使用了双重检锁的同步方式确保同一时刻只执一次Glide的初始化 -checkAndInitializeGlide(context); - -initializeGlide(context); - -// 最终执行到Glide的另一个重载方法 -initializeGlide(context, new GlideBuilder()); - -@SuppressWarnings("deprecation") - private static void initializeGlide(@NonNull Context context, @NonNull GlideBuilder builder) { - Context applicationContext = context.getApplicationContext(); - // 1、获取前面应用中带注解的GlideModule - GeneratedAppGlideModule annotationGeneratedModule = getAnnotationGeneratedGlideModules(); - // 2、如果GlideModule为空或者可配置manifest里面的标志为true,则获取manifest里面 - // 配置的GlideModule模块(manifestModules)。 - List manifestModules = Collections.emptyList(); - if (annotationGeneratedModule == null || annotationGeneratedModule.isManifestParsingEnabled( )) { - manifestModules = new ManifestParser(applicationContext).parse(); - } - - ... - - RequestManagerRetriever.RequestManagerFactory factory = - annotationGeneratedModule != null - ? annotationGeneratedModule.getRequestManag erFactory() : null; - builder.setRequestManagerFactory(factory); - for (com.bumptech.glide.module.GlideModule module : manifestModules) { - module.applyOptions(applicationContext, builder); - } - if (annotationGeneratedModule != null) { - annotationGeneratedModule.applyOptions(applicatio nContext, builder); - } - // 3、初始化各种配置信息 - Glide glide = builder.build(applicationContext); - // 4、把manifestModules以及annotationGeneratedModule里面的配置信息放到builder - // 里面(applyOptions)替换glide默认组件(registerComponents) - for (com.bumptech.glide.module.GlideModule module : manifestModules) { - module.registerComponents(applicationContext, glide, glide.registry); - } - if (annotationGeneratedModule != null) { - annotationGeneratedModule.registerComponents(appl icationContext, glide, glide.registry); - } - applicationContext.registerComponentCallbacks(glide ); - Glide.glide = glide; -} -``` - -### GlideBuilder#build - -``` -@NonNull - Glide build(@NonNull Context context) { - // 创建请求图片线程池sourceExecutor - if (sourceExecutor == null) { - sourceExecutor = GlideExecutor.newSourceExecutor(); - } - - // 创建硬盘缓存线程池diskCacheExecutor - if (diskCacheExecutor == null) { - diskCacheExecutor = GlideExecutor.newDiskCacheExecutor(); - } - - // 创建动画线程池animationExecutor - if (animationExecutor == null) { - animationExecutor = GlideExecutor.newAnimationExecutor(); - } - - if (memorySizeCalculator == null) { - memorySizeCalculator = new MemorySizeCalculator.Builder(context).build(); - } - - if (connectivityMonitorFactory == null) { - connectivityMonitorFactory = new DefaultConnectivityMonitorFactory(); - } - - if (bitmapPool == null) { - // 依据设备的屏幕密度和尺寸设置各种pool的size - int size = memorySizeCalculator.getBitmapPoolSize(); - if (size > 0) { - // 创建图片线程池LruBitmapPool,缓存所有被释放的bitmap - // 缓存策略在API大于19时,为SizeConfigStrategy,小于为AttributeStrategy。 - // 其中SizeConfigStrategy是以bitmap的size和config为key,value为bitmap的HashMap - bitmapPool = new LruBitmapPool(size); - } else { - bitmapPool = new BitmapPoolAdapter(); - } - } - - // 创建对象数组缓存池LruArrayPool,默认4M - if (arrayPool == null) { - arrayPool = new LruArrayPool(memorySizeCalculator.getArrayPoolSiz eInBytes()); - } - - // 创建LruResourceCache,内存缓存 - if (memoryCache == null) { - memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCa cheSize()); - } - - if (diskCacheFactory == null) { - diskCacheFactory = new InternalCacheDiskCacheFactory(context); - } - - // 创建任务和资源管理引擎(线程池,内存缓存和硬盘缓存对象) - if (engine == null) { - engine = - new Engine( - memoryCache, - diskCacheFactory, - diskCacheExecutor, - sourceExecutor, - GlideExecutor.newUnlimitedSourceExecutor( ), - GlideExecutor.newAnimationExecutor(), - isActiveResourceRetentionAllowed); - } - - RequestManagerRetriever requestManagerRetriever = - new RequestManagerRetriever(requestManagerFactory); - - return new Glide( - context, - engine, - memoryCache, - bitmapPool, - arrayPool, - requestManagerRetriever, - connectivityMonitorFactory, - logLevel, - defaultRequestOptions.lock(), - defaultTransitionOptions); -} -``` - -### Glide#Glide构造方法 - -``` -Glide(...) { - ... - // 注册管理任务执行对象的类(Registry) - // Registry是一个工厂,而其中所有注册的对象都是一个工厂员工,当任务分发时, - // 根据当前任务的性质,分发给相应员工进行处理 - registry = new Registry(); - - ... - - // 这里大概有60余次的append或register员工组件(解析器、编解码器、工厂类、转码类等等组件) - registry - .append(ByteBuffer.class, new ByteBufferEncoder()) - .append(InputStream.class, new StreamEncoder(arrayPool)) - - // 根据给定子类产出对应类型的target(BitmapImageViewTarget / DrawableImageViewTarget) - ImageViewTargetFactory imageViewTargetFactory = new ImageViewTargetFactory(); - - glideContext = - new GlideContext( - context, - arrayPool, - registry, - imageViewTargetFactory, - defaultRequestOptions, - defaultTransitionOptions, - engine, - logLevel); -} -``` - -### RequestManagerRetriever#get - -``` -@NonNull -public RequestManager get(@NonNull Context context) { - if (context == null) { - throw new IllegalArgumentException("You cannot start a load on a null Context"); - } else if (Util.isOnMainThread() && !(context instanceof Application)) { - // 如果当前线程是主线程且context不是Application走相应的get重载方法 - if (context instanceof FragmentActivity) { - return get((FragmentActivity) context); - } else if (context instanceof Activity) { - return get((Activity) context); - } else if (context instanceof ContextWrapper) { - return get(((ContextWrapper) context).getBaseContext()); - } - } - - // 否则直接将请求与ApplicationLifecycle关联 - return getApplicationManager(context); -} -``` - -这里总结一下,对于当前传入的context是application或当前线程是子线程时,请求的生命周期和ApplicationLifecycle关联,否则,context是FragmentActivity或Fragment时,在当前组件添加一个SupportFragment(SupportRequestManagerFragment),context是Activity时,在当前组件添加一个Fragment(RequestManagerFragment)。 - -### GlideApp#with小结 - -1. 初始化各式各样的配置信息(包括缓存,请求线程池,大小,图片格式等等)以及glide对象。 - -2. 将glide请求和application/SupportFragment/Fragment的生命周期绑定在一块。 - -### with方法的执行流程 - -![image](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a307a034fea446a486c4f988849751a0~tplv-k3u1fbpfcp-zoom-1.image) - -## load(url)源码详解 - -### GlideRequest(RequestManager)#load - -``` -return (GlideRequest) super.load(string); - -return asDrawable().load(string); - -// 1、asDrawable部分 -return (GlideRequest) super.asDrawable(); - -return as(Drawable.class); - -// 最终返回了一个GlideRequest(RequestManager的子类) -return new GlideRequest<>(glide, this, resourceClass, context); - -// 2、load部分 -return (GlideRequest) super.load(string); - -return loadGeneric(string); - -@NonNull -private RequestBuilder loadGeneric(@Nullable Object model) { - // model则为设置的url - this.model = model; - // 记录url已设置 - isModelSet = true; - return this; -} -``` - -可以看到,load这部分的源码很简单,就是给GlideRequest(RequestManager)设置了要请求的mode(url),并记录了url已设置的状态。 - -### load方法的执行流程 - -![image](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/33114794d81a421ea4345806826919b7~tplv-k3u1fbpfcp-zoom-1.image) - -## into(iv)源码详解 - -真正复杂的地方要开始了。 - -### RequestBuilder.into - -``` - @NonNull -public ViewTarget into(@NonNull ImageView view) { - Util.assertMainThread(); - Preconditions.checkNotNull(view); - - RequestOptions requestOptions = this.requestOptions; - if (!requestOptions.isTransformationSet() - && requestOptions.isTransformationAllowed() - && view.getScaleType() != null) { - // Clone in this method so that if we use this RequestBuilder to load into a View and then - // into a different target, we don't retain the transformation applied based on the previous - // View's scale type. - switch (view.getScaleType()) { - // 这个RequestOptions里保存了要设置的scaleType,Glide自身封装了CenterCrop、CenterInside、 - // FitCenter、CenterInside四种规格。 - case CENTER_CROP: - requestOptions = requestOptions.clone().optionalCenterCrop(); - break; - case CENTER_INSIDE: - requestOptions = requestOptions.clone().optionalCenterInside() ; - break; - case FIT_CENTER: - case FIT_START: - case FIT_END: - requestOptions = requestOptions.clone().optionalFitCenter(); - break; - case FIT_XY: - requestOptions = requestOptions.clone().optionalCenterInside() ; - break; - case CENTER: - case MATRIX: - default: - // Do nothing. - } - } - - // 注意,这个transcodeClass是指的drawable或bitmap - return into( - glideContext.buildImageViewTarget(view, transcodeClass), - /*targetListener=*/ null, - requestOptions); -} -``` - -### GlideContext#buildImageViewTarget - -``` -return imageViewTargetFactory.buildTarget(imageView, transcodeClass); -``` - -### ImageViewTargetFactory#buildTarget - -``` -@NonNull -@SuppressWarnings("unchecked") -public ViewTarget buildTarget(@NonNull ImageView view, - @NonNull Class clazz) { - // 返回展示Bimtap/Drawable资源的目标对象 - if (Bitmap.class.equals(clazz)) { - return (ViewTarget) new BitmapImageViewTarget(view); - } else if (Drawable.class.isAssignableFrom(clazz)) { - return (ViewTarget) new DrawableImageViewTarget(view); - } else { - throw new IllegalArgumentException( - "Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)"); - } -} -``` - -可以看到,Glide内部只维护了两种target,一种是BitmapImageViewTarget,另一种则是DrawableImageViewTarget,接下来继续深入。 - -### RequestBuilder#into - -``` -private > Y into( - @NonNull Y target, - @Nullable RequestListener targetListener, - @NonNull RequestOptions options) { - Util.assertMainThread(); - Preconditions.checkNotNull(target); - if (!isModelSet) { - throw new IllegalArgumentException("You must call #load() before calling #into()"); - } - - options = options.autoClone(); - // 分析1.建立请求 - Request request = buildRequest(target, targetListener, options); - - Request previous = target.getRequest(); - if (request.isEquivalentTo(previous) - && !isSkipMemoryCacheWithCompletePreviousReques t(options, previous)) { - request.recycle(); - // If the request is completed, beginning again will ensure the result is re-delivered, - // triggering RequestListeners and Targets. If the request is failed, beginning again will - // restart the request, giving it another chance to complete. If the request is already - // running, we can let it continue running without interruption. - if (!Preconditions.checkNotNull(previous).isRunni ng()) { - // Use the previous request rather than the new one to allow for optimizations like skipping - // setting placeholders, tracking and un-tracking Targets, and obtaining View dimensions - // that are done in the individual Request. - previous.begin(); - } - return target; - } - - requestManager.clear(target); - target.setRequest(request); - // 分析2.真正追踪请求的地方 - requestManager.track(target, request); - - return target; -} - -// 分析1 -private Request buildRequest( - Target target, - @Nullable RequestListener targetListener, - RequestOptions requestOptions) { - return buildRequestRecursive( - target, - targetListener, - /*parentCoordinator=*/ null, - transitionOptions, - requestOptions.getPriority(), - requestOptions.getOverrideWidth(), - requestOptions.getOverrideHeight(), - requestOptions); -} - -// 分析1 -private Request buildRequestRecursive( - Target target, - @Nullable RequestListener targetListener, - @Nullable RequestCoordinator parentCoordinator, - TransitionOptions transitionOptions, - Priority priority, - int overrideWidth, - int overrideHeight, - RequestOptions requestOptions) { - - // Build the ErrorRequestCoordinator first if necessary so we can update parentCoordinator. - ErrorRequestCoordinator errorRequestCoordinator = null; - if (errorBuilder != null) { - // 创建errorRequestCoordinator(异常处理对象) - errorRequestCoordinator = new ErrorRequestCoordinator(parentCoordinator); - parentCoordinator = errorRequestCoordinator; - } - - // 递归建立缩略图请求 - Request mainRequest = - buildThumbnailRequestRecursive( - target, - targetListener, - parentCoordinator, - transitionOptions, - priority, - overrideWidth, - overrideHeight, - requestOptions); - - if (errorRequestCoordinator == null) { - return mainRequest; - } - - ... - - Request errorRequest = errorBuilder.buildRequestRecursive( - target, - targetListener, - errorRequestCoordinator, - errorBuilder.transitionOptions, - errorBuilder.requestOptions.getPriority(), - errorOverrideWidth, - errorOverrideHeight, - errorBuilder.requestOptions); - errorRequestCoordinator.setRequests(mainRequest, errorRequest); - return errorRequestCoordinator; -} - -// 分析1 -private Request buildThumbnailRequestRecursive( - Target target, - RequestListener targetListener, - @Nullable RequestCoordinator parentCoordinator, - TransitionOptions transitionOptions, - Priority priority, - int overrideWidth, - int overrideHeight, - RequestOptions requestOptions) { - if (thumbnailBuilder != null) { - // Recursive case: contains a potentially recursive thumbnail request builder. - - ... - - ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator); - // 获取一个正常请求对象 - Request fullRequest = - obtainRequest( - target, - targetListener, - requestOptions, - coordinator, - transitionOptions, - priority, - overrideWidth, - overrideHeight); - isThumbnailBuilt = true; - // Recursively generate thumbnail requests. - // 使用递归的方式建立一个缩略图请求对象 - Request thumbRequest = - thumbnailBuilder.buildRequestRecursive( - target, - targetListener, - coordinator, - thumbTransitionOptions, - thumbPriority, - thumbOverrideWidth, - thumbOverrideHeight, - thumbnailBuilder.requestOptions); - isThumbnailBuilt = false; - // coordinator(ThumbnailRequestCoordinator)是作为两者的协调者, - // 能够同时加载缩略图和正常的图的请求 - coordinator.setRequests(fullRequest, thumbRequest); - return coordinator; - } else if (thumbSizeMultiplier != null) { - // Base case: thumbnail multiplier generates a thumbnail request, but cannot recurse. - // 当设置了缩略的比例thumbSizeMultiplier(0 ~ 1)时, - // 不需要递归建立缩略图请求 - ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator); - Request fullRequest = - obtainRequest( - target, - targetListener, - requestOptions, - coordinator, - transitionOptions, - priority, - overrideWidth, - overrideHeight); - RequestOptions thumbnailOptions = requestOptions.clone() - .sizeMultiplier(thumbSizeMultiplier); - - Request thumbnailRequest = - obtainRequest( - target, - targetListener, - thumbnailOptions, - coordinator, - transitionOptions, - getThumbnailPriority(priority), - overrideWidth, - overrideHeight); - - coordinator.setRequests(fullRequest, thumbnailRequest); - return coordinator; - } else { - // Base case: no thumbnail. - // 没有缩略图请求时,直接获取一个正常图请求 - return obtainRequest( - target, - targetListener, - requestOptions, - parentCoordinator, - transitionOptions, - priority, - overrideWidth, - overrideHeight); - } -} - -private Request obtainRequest( - Target target, - RequestListener targetListener, - RequestOptions requestOptions, - RequestCoordinator requestCoordinator, - TransitionOptions transitionOptions, - Priority priority, - int overrideWidth, - int overrideHeight) { - // 最终实际返回的是一个SingleRequest对象(将制定的资源加载进对应的Target - return SingleRequest.obtain( - context, - glideContext, - model, - transcodeClass, - requestOptions, - overrideWidth, - overrideHeight, - priority, - target, - targetListener, - requestListeners, - requestCoordinator, - glideContext.getEngine(), - transitionOptions.getTransitionFactory()); -} -``` - -从上源码分析可知,我们在分析1处的buildRequest()方法里建立了请求,且最多可同时进行缩略图和正常图的请求,最后,调用了requestManager.track(target, request)方法,接着看看track里面做了什么。 - -### RequestManager#track - -``` -// 分析2 -void track(@NonNull Target target, @NonNull Request request) { - // 加入一个target目标集合(Set) - targetTracker.track(target); - - requestTracker.runRequest(request); -} -``` - -### RequestTracker#runRequest - -``` -/** -* Starts tracking the given request. -*/ -// 分析2 -public void runRequest(@NonNull Request request) { - requests.add(request); - if (!isPaused) { - // 如果不是暂停状态则开始请求 - request.begin(); - } else { - request.clear(); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Paused, delaying request"); - } - // 否则清空请求,加入延迟请求队列(为了对这些请求维持一个强引用,使用了ArrayList实现) - pendingRequests.add(request); - } -} -``` - -### SingleRequest#begin - -``` -// 分析2 -@Override -public void begin() { - - ... - - if (model == null) { - - ... - // model(url)为空,回调加载失败 - onLoadFailed(new GlideException("Received null model"), logLevel); - return; - } - - if (status == Status.RUNNING) { - throw new IllegalArgumentException("Cannot restart a running request"); - } - - - if (status == Status.COMPLETE) { - onResourceReady(resource, DataSource.MEMORY_CACHE); - return; - } - - status = Status.WAITING_FOR_SIZE; - if (Util.isValidDimensions(overrideWidth, overrideHeight)) { - // 当使用override() API为图片指定了一个固定的宽高时直接执行onSizeReady, - // 最终的核心处理位于onSizeReady - onSizeReady(overrideWidth, overrideHeight); - } else { - // 根据imageView的宽高算出图片的宽高,最终也会走到onSizeReady - target.getSize(this); - } - - if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE) - && canNotifyStatusChanged()) { - // 预先加载设置的缩略图 - target.onLoadStarted(getPlaceholderDrawable()); - } - if (IS_VERBOSE_LOGGABLE) { - logV("finished run method in " + LogTime.getElapsedMillis(startTime)); - } -} -``` - -从requestManager.track(target, request)开始,最终会执行到SingleRequest#begin()方法的onSizeReady,可以猜到(因为后面只做了预加载缩略图的处理),真正的请求就是从这里开始的,咱们进去一探究竟~ - -### SingleRequest#onSizeReady - -``` -// 分析2 -@Override -public void onSizeReady(int width, int height) { - stateVerifier.throwIfRecycled(); - - ... - - status = Status.RUNNING; - - float sizeMultiplier = requestOptions.getSizeMultiplier(); - this.width = maybeApplySizeMultiplier(width, sizeMultiplier); - this.height = maybeApplySizeMultiplier(height, sizeMultiplier); - - ... - - // 根据给定的配置进行加载,engine是一个负责加载、管理活跃和缓存资源的引擎类 - loadStatus = engine.load( - glideContext, - model, - requestOptions.getSignature(), - this.width, - this.height, - requestOptions.getResourceClass(), - transcodeClass, - priority, - requestOptions.getDiskCacheStrategy(), - requestOptions.getTransformations(), - requestOptions.isTransformationRequired(), - requestOptions.isScaleOnlyOrNoTransform(), - requestOptions.getOptions(), - requestOptions.isMemoryCacheable(), - requestOptions.getUseUnlimitedSourceGeneratorsP ool(), - requestOptions.getUseAnimationPool(), - requestOptions.getOnlyRetrieveFromCache(), - this); - - ... -} -``` - -终于看到Engine类了,感觉距离成功不远了,继续~ - -### Engine#load - -``` -public LoadStatus load( - GlideContext glideContext, - Object model, - Key signature, - int width, - int height, - Class resourceClass, - Class transcodeClass, - Priority priority, - DiskCacheStrategy diskCacheStrategy, - Map, Transformation> transformations, - boolean isTransformationRequired, - boolean isScaleOnlyOrNoTransform, - Options options, - boolean isMemoryCacheable, - boolean useUnlimitedSourceExecutorPool, - boolean useAnimationPool, - boolean onlyRetrieveFromCache, - ResourceCallback cb) { - - ... - - // 先从弱引用中查找,如果有的话回调onResourceReady并直接返回 - EngineResource active = loadFromActiveResources(key, isMemoryCacheable); - if (active != null) { - cb.onResourceReady(active, DataSource.MEMORY_CACHE); - if (VERBOSE_IS_LOGGABLE) { - logWithTimeAndKey("Loaded resource from active resources", startTime, key); - } - return null; - } - - // 没有再从内存中查找,有的话会取出并放到ActiveResources(内部维护的弱引用缓存map)里面 - EngineResource cached = loadFromCache(key, isMemoryCacheable); - if (cached != null) { - cb.onResourceReady(cached, DataSource.MEMORY_CACHE); - if (VERBOSE_IS_LOGGABLE) { - logWithTimeAndKey("Loaded resource from cache", startTime, key); - } - return null; - } - - EngineJob current = jobs.get(key, onlyRetrieveFromCache); - if (current != null) { - current.addCallback(cb); - if (VERBOSE_IS_LOGGABLE) { - logWithTimeAndKey("Added to existing load", startTime, key); - } - return new LoadStatus(cb, current); - } - - // 如果内存中没有,则创建engineJob(decodejob的回调类,管理下载过程以及状态) - EngineJob engineJob = - engineJobFactory.build( - key, - isMemoryCacheable, - useUnlimitedSourceExecutorPool, - useAnimationPool, - onlyRetrieveFromCache); - - // 创建解析工作对象 - DecodeJob decodeJob = - decodeJobFactory.build( - glideContext, - model, - key, - signature, - width, - height, - resourceClass, - transcodeClass, - priority, - diskCacheStrategy, - transformations, - isTransformationRequired, - isScaleOnlyOrNoTransform, - onlyRetrieveFromCache, - options, - engineJob); - - // 放在Jobs内部维护的HashMap中 - jobs.put(key, engineJob); - - // 关注点8 后面分析会用到 - // 注册ResourceCallback接口 - engineJob.addCallback(cb); - // 内部开启线程去请求 - engineJob.start(decodeJob); - - if (VERBOSE_IS_LOGGABLE) { - logWithTimeAndKey("Started new load", startTime, key); - } - return new LoadStatus(cb, engineJob); -} - -public void start(DecodeJob decodeJob) { - this.decodeJob = decodeJob; - // willDecodeFromCache方法内部根据不同的阶段stage,如果是RESOURCE_CACHE/DATA_CACHE则返回true,使用diskCacheExecutor,否则调用getActiveSourceExecutor,内部会根据相应的条件返回sourceUnlimitedExecutor/animationExecutor/sourceExecutor - GlideExecutor executor = - decodeJob.willDecodeFromCache() - ? diskCacheExecutor - : getActiveSourceExecutor(); - executor.execute(decodeJob); -} -``` - -可以看到,最终Engine(引擎)类内部会执行到自身的start方法,它会根据不同的配置采用不同的线程池使用diskCacheExecutor/sourceUnlimitedExecutor/animationExecutor/sourceExecutor来执行最终的解码任务decodeJob。 - -### DecodeJob#run - -``` -runWrapped(); - -private void runWrapped() { - switch (runReason) { - case INITIALIZE: - stage = getNextStage(Stage.INITIALIZE); - // 关注点1 - currentGenerator = getNextGenerator(); - // 关注点2 内部会调用相应Generator的startNext() - runGenerators(); - break; - case SWITCH_TO_SOURCE_SERVICE: - runGenerators(); - break; - case DECODE_DATA: - // 关注点3 将获取的数据解码成对应的资源 - decodeFromRetrievedData(); - break; - default: - throw new IllegalStateException("Unrecognized run reason: " + runReason); - } -} - -// 关注点1,完整情况下,会异步依次生成这里的ResourceCacheGenerator、DataCacheGenerator和SourceGenerator对象,并在之后执行其中的startNext() -private DataFetcherGenerator getNextGenerator() { - switch (stage) { - case RESOURCE_CACHE: - return new ResourceCacheGenerator(decodeHelper, this); - case DATA_CACHE: - return new DataCacheGenerator(decodeHelper, this); - case SOURCE: - return new SourceGenerator(decodeHelper, this); - case FINISHED: - return null; - default: - throw new IllegalStateException("Unrecognized stage: " + stage); - } -} -``` - -### SourceGenerator#startNext - -``` -// 关注点2 -@Override -public boolean startNext() { - // dataToCache数据不为空的话缓存到硬盘(第一执行该方法是不会调用的) - if (dataToCache != null) { - Object data = dataToCache; - dataToCache = null; - cacheData(data); - } - - if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) { - return true; - } - sourceCacheGenerator = null; - - loadData = null; - boolean started = false; - while (!started && hasNextModelLoader()) { - // 关注点4 getLoadData()方法内部会在modelLoaders里面找到ModelLoder对象 - // (每个Generator对应一个ModelLoader), - // 并使用modelLoader.buildLoadData方法返回一个loadData列表 - loadData = helper.getLoadData().get(loadDataListIndex++); - if (loadData != null - && (helper.getDiskCacheStrategy().isDataCache able(loadData.fetcher.getDataSource()) - || helper.hasLoadPath(loadData.fetcher.getDat aClass()))) { - started = true; - // 关注点6 通过loadData对象的fetcher对象(有关注点3的分析可知其实现类为HttpUrlFetcher)的 - // loadData方法来获取图片数据 - loadData.fetcher.loadData(helper.getPriority(), this); - } - } - return started; -} -``` - -### DecodeHelper#getLoadData - -``` -List> getLoadData() { - if (!isLoadDataSet) { - isLoadDataSet = true; - loadData.clear(); - List> modelLoaders = glideContext.getRegistry().getModelLoaders(model) ; - //noinspection ForLoopReplaceableByForEach to improve perf - for (int i = 0, size = modelLoaders.size(); i < size; i++) { - ModelLoader modelLoader = modelLoaders.get(i); - // 注意:这里最终是通过HttpGlideUrlLoader的buildLoadData获取到实际的loadData对象 - LoadData current = - modelLoader.buildLoadData(model, width, height, options); - if (current != null) { - loadData.add(current); - } - } - } - return loadData; -} -``` - -### HttpGlideUrlLoader#buildLoadData - -``` -@Override -public LoadData buildLoadData(@NonNull GlideUrl model, int width, int height, - @NonNull Options options) { - // GlideUrls memoize parsed URLs so caching them saves a few object instantiations and time - // spent parsing urls. - GlideUrl url = model; - if (modelCache != null) { - url = modelCache.get(model, 0, 0); - if (url == null) { - // 关注点5 - modelCache.put(model, 0, 0, model); - url = model; - } - } - int timeout = options.get(TIMEOUT); - // 注意,这里创建了一个DataFetcher的实现类HttpUrlFetcher - return new LoadData<>(url, new HttpUrlFetcher(url, timeout)); -} - -// 关注点5 -public void put(A model, int width, int height, B value) { - ModelKey key = ModelKey.get(model, width, height); - // 最终是通过LruCache来缓存对应的值,key是一个ModelKey对象(由model、width、height三个属性组成) - cache.put(key, value); -} -``` - -从这里的分析,我们明白了HttpUrlFetcher实际上就是最终的请求执行者,而且,我们知道了Glide会使用LruCache来对解析后的url来进行缓存,以便后续可以省去解析url的时间。 - -### HttpUrlFetcher#loadData - -``` -@Override -public void loadData(@NonNull Priority priority, - @NonNull DataCallback callback) { - long startTime = LogTime.getLogTime(); - try { - // 关注点6 - // loadDataWithRedirects内部是通过HttpURLConnection网络请求数据 - InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders()); - // 请求成功回调onDataReady() - callback.onDataReady(result); - } catch (IOException e) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Failed to load data for url", e); - } - callback.onLoadFailed(e); - } finally { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Finished http url fetcher fetch in " + LogTime.getElapsedMillis(startTime)); - } - } -} - -private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, - Map headers) throws IOException { - - ... - - urlConnection.connect(); - // Set the stream so that it's closed in cleanup to avoid resource leaks. See #2352. - stream = urlConnection.getInputStream(); - if (isCancelled) { - return null; - } - final int statusCode = urlConnection.getResponseCode(); - // 只要是2xx形式的状态码则判断为成功 - if (isHttpOk(statusCode)) { - // 从urlConnection中获取资源流 - return getStreamForSuccessfulRequest(urlConnection); - } else if (isHttpRedirect(statusCode)) { - - ... - - // 重定向请求 - return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers); - } else if (statusCode == INVALID_STATUS_CODE) { - throw new HttpException(statusCode); - } else { - throw new HttpException(urlConnection.getResponseMessage(), statusCode); - } -} - -private InputStream getStreamForSuccessfulRequest(HttpURLConnection urlConnection) - throws IOException { - if (TextUtils.isEmpty(urlConnection.getContentEncoding())) { - int contentLength = urlConnection.getContentLength(); - stream = ContentLengthInputStream.obtain(urlConnection.getInputStr eam(), contentLength); - } else { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Got non empty content encoding: " + urlConnection.getContentEncoding()); - } - stream = urlConnection.getInputStream(); - } - return stream; -} -``` - -在HttpUrlFetcher#loadData方法的loadDataWithRedirects里面,Glide通过原生的HttpURLConnection进行请求后,并调用getStreamForSuccessfulRequest()方法获取到了最终的图片流。 - -### DecodeJob#run - -在我们通过HtttpUrlFetcher的loadData()方法请求得到对应的流之后,我们还必须对流进行处理得到最终我们想要的资源。这里我们回到第10步DecodeJob#run方法的关注点3处,这行代码将会对流进行解码。 - -``` -decodeFromRetrievedData(); -``` - -接下来,继续看看他内部的处理。 - -``` -private void decodeFromRetrievedData() { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - logWithTimeAndKey("Retrieved data", startFetchTime, - "data: " + currentData - + ", cache key: " + currentSourceKey - + ", fetcher: " + currentFetcher); - } - Resource resource = null; - try { - // 核心代码 - // 从数据中解码得到资源 - resource = decodeFromData(currentFetcher, currentData, currentDataSource); - } catch (GlideException e) { - e.setLoggingDetails(currentAttemptingKey, currentDataSource); - throwables.add(e); - } - if (resource != null) { - // 关注点8 - // 编码和发布最终得到的Resource对象 - notifyEncodeAndRelease(resource, currentDataSource); - } else { - runGenerators(); - } -} - - private Resource decodeFromData(DataFetcher fetcher, Data data, - DataSource dataSource) throws GlideException { - try { - if (data == null) { - return null; - } - long startTime = LogTime.getLogTime(); - // 核心代码 - // 进一步包装了解码方法 - Resource result = decodeFromFetcher(data, dataSource); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - logWithTimeAndKey("Decoded result " + result, startTime); - } - return result; - } finally { - fetcher.cleanup(); - } -} - -@SuppressWarnings("unchecked") -private Resource decodeFromFetcher(Data data, DataSource dataSource) - throws GlideException { - LoadPath path = decodeHelper.getLoadPath((Class) data.getClass()); - // 核心代码 - // 将解码任务分发给LoadPath - return runLoadPath(data, dataSource, path); -} - -private Resource runLoadPath(Data data, DataSource dataSource, - LoadPath path) throws GlideException { - Options options = getOptionsWithHardwareConfig(dataSource); - // 将数据进一步包装 - DataRewinder rewinder = glideContext.getRegistry().getRewinder(data); - try { - // ResourceType in DecodeCallback below is required for compilation to work with gradle. - // 核心代码 - // 将解码任务分发给LoadPath - return path.load( - rewinder, options, width, height, new DecodeCallback(dataSource)); - } finally { - rewinder.cleanup(); - } -} -``` - -### LoadPath#load - -``` -public Resource load(DataRewinder rewinder, @NonNull Options options, int width, - int height, DecodePath.DecodeCallback decodeCallback) throws GlideException { -List throwables = Preconditions.checkNotNull(listPool.acquire()); -try { - // 核心代码 - return loadWithExceptionList(rewinder, options, width, height, decodeCallback, throwables); -} finally { - listPool.release(throwables); -} -``` - -} - -``` -private Resource loadWithExceptionList(DataRewinder rewinder, - @NonNull Options options, - int width, int height, DecodePath.DecodeCallback decodeCallback, - List exceptions) throws GlideException { - Resource result = null; - //noinspection ForLoopReplaceableByForEach to improve perf - for (int i = 0, size = decodePaths.size(); i < size; i++) { - DecodePath path = decodePaths.get(i); - try { - // 核心代码 - // 将解码任务又进一步分发给DecodePath的decode方法去解码 - result = path.decode(rewinder, width, height, options, decodeCallback); - } catch (GlideException e) { - exceptions.add(e); - } - if (result != null) { - break; - } - } - - if (result == null) { - throw new GlideException(failureMessage, new ArrayList<>(exceptions)); - } - - return result; -} -``` - -### DecodePath#decode - -``` -public Resource decode(DataRewinder rewinder, int width, int height, - @NonNull Options options, DecodeCallback callback) throws GlideException { - // 核心代码 - // 继续调用DecodePath的decodeResource方法去解析出数据 - Resource decoded = decodeResource(rewinder, width, height, options); - Resource transformed = callback.onResourceDecoded(decoded); - return transcoder.transcode(transformed, options); -} - -@NonNull -private Resource decodeResource(DataRewinder rewinder, int width, - int height, @NonNull Options options) throws GlideException { - List exceptions = Preconditions.checkNotNull(listPool.acquire()); - try { - // 核心代码 - return decodeResourceWithList(rewinder, width, height, options, exceptions); - } finally { - listPool.release(exceptions); - } -} - -@NonNull -private Resource decodeResourceWithList(DataRewinder rewinder, int width, - int height, @NonNull Options options, List exceptions) throws GlideException { - Resource result = null; - //noinspection ForLoopReplaceableByForEach to improve perf - for (int i = 0, size = decoders.size(); i < size; i++) { - ResourceDecoder decoder = decoders.get(i); - try { - DataType data = rewinder.rewindAndGet(); - if (decoder.handles(data, options)) { - // 获取包装的数据 - data = rewinder.rewindAndGet(); - // 核心代码 - // 根据DataType和ResourceType的类型分发给不同的解码器Decoder - result = decoder.decode(data, width, height, options); - } - } catch (IOException | RuntimeException | OutOfMemoryError e) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Failed to decode data for " + decoder, e); - } - exceptions.add(e); - } - - if (result != null) { - break; - } - } - - if (result == null) { - throw new GlideException(failureMessage, new ArrayList<>(exceptions)); - } - return result; -} -``` - -可以看到,经过一连串的嵌套调用,最终执行到了decoder.decode()这行代码,decode是一个ResourceDecoder接口(资源解码器),根据不同的DataType和ResourceType它会有不同的实现类,这里的实现类是ByteBufferBitmapDecoder,接下来让我们来看看这个解码器内部的解码流程。 - -### ByteBufferBitmapDecoder#decode - -``` -/** - * Decodes {@link android.graphics.Bitmap Bitmaps} from {@link java.nio.ByteBuffer ByteBuffers}. - */ -public class ByteBufferBitmapDecoder implements ResourceDecoder { - - ... - - @Override - public Resource decode(@NonNull ByteBuffer source, int width, int height, - @NonNull Options options) - throws IOException { - InputStream is = ByteBufferUtil.toStream(source); - // 核心代码 - return downsampler.decode(is, width, height, options); - } -} -``` - -可以看到,最终是使用了一个downsampler,它是一个压缩器,主要是对流进行解码,压缩,圆角等处理。 - -### DownSampler#decode - -``` -public Resource decode(InputStream is, int outWidth, int outHeight, - Options options) throws IOException { - return decode(is, outWidth, outHeight, options, EMPTY_CALLBACKS); -} - - @SuppressWarnings({"resource", "deprecation"}) -public Resource decode(InputStream is, int requestedWidth, int requestedHeight, - Options options, DecodeCallbacks callbacks) throws IOException { - Preconditions.checkArgument(is.markSupported(), "You must provide an InputStream that supports" - + " mark()"); - - ... - - try { - // 核心代码 - Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions, - downsampleStrategy, decodeFormat, isHardwareConfigAllowed, requestedWidth, - requestedHeight, fixBitmapToRequestedDimensions, callbacks); - // 关注点7 - // 解码得到Bitmap对象后,包装成BitmapResource对象返回, - // 通过内部的get方法得到Resource对象 - return BitmapResource.obtain(result, bitmapPool); - } finally { - releaseOptions(bitmapFactoryOptions); - byteArrayPool.put(bytesForOptions); - } -} - -private Bitmap decodeFromWrappedStreams(InputStream is, - BitmapFactory.Options options, DownsampleStrategy downsampleStrategy, - DecodeFormat decodeFormat, boolean isHardwareConfigAllowed, int requestedWidth, - int requestedHeight, boolean fixBitmapToRequestedDimensions, - DecodeCallbacks callbacks) throws IOException { - - // 省去计算压缩比例等一系列非核心逻辑 - ... - - // 核心代码 - Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool); - callbacks.onDecodeComplete(bitmapPool, downsampled); - - ... - - // Bimtap旋转处理 - ... - - return rotated; -} - -private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options, - DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException { - - ... - - TransformationUtils.getBitmapDrawableLock().lock(); - try { - // 核心代码 - result = BitmapFactory.decodeStream(is, null, options); - } catch (IllegalArgumentException e) { - ... - } finally { - TransformationUtils.getBitmapDrawableLock().unlock(); - } - - if (options.inJustDecodeBounds) { - is.reset(); - } - return result; -} -``` - -从以上源码流程我们知道,最后是在DownSampler的decodeStream()方法中使用了BitmapFactory.decodeStream()来得到Bitmap对象。然后,我们来分析下图片时如何显示的,我们回到步骤19的DownSampler#decode方法,看到关注点7,这里是将Bitmap包装成BitmapResource对象返回,通过内部的get方法可以得到Resource对象,再回到步骤15的DecodeJob#run方法,这是使用了notifyEncodeAndRelease()方法对Resource对象进行了发布。 - -### DecodeJob#notifyEncodeAndRelease - -``` -private void notifyEncodeAndRelease(Resource resource, DataSource dataSource) { - - ... - - notifyComplete(result, dataSource); - - ... - -} - -private void notifyComplete(Resource resource, DataSource dataSource) { - setNotifiedOrThrow(); - callback.onResourceReady(resource, dataSource); -} -``` - -从以上EngineJob的源码可知,它实现了DecodeJob.CallBack这个接口。 - -``` -class EngineJob implements DecodeJob.Callback, - Poolable { - ... -} -``` - -### EngineJob#onResourceReady - -``` -@Override -public void onResourceReady(Resource resource, DataSource dataSource) { - this.resource = resource; - this.dataSource = dataSource; - MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget(); -} - -private static class MainThreadCallback implements Handler.Callback{ - - ... - - @Override - public boolean handleMessage(Message message) { - EngineJob job = (EngineJob) message.obj; - switch (message.what) { - case MSG_COMPLETE: - // 核心代码 - job.handleResultOnMainThread(); - break; - ... - } - return true; - } -} -``` - -从以上源码可知,通过主线程Handler对象进行切换线程,然后在主线程调用了handleResultOnMainThread这个方法。 - -``` -@Synthetic -void handleResultOnMainThread() { - ... - - //noinspection ForLoopReplaceableByForEach to improve perf - for (int i = 0, size = cbs.size(); i < size; i++) { - ResourceCallback cb = cbs.get(i); - if (!isInIgnoredCallbacks(cb)) { - engineResource.acquire(); - cb.onResourceReady(engineResource, dataSource); - } - } - - ... -} -``` - -这里又通过一个循环调用了所有ResourceCallback的方法,让我们回到步骤9处Engine#load方法的关注点8这行代码,这里对ResourceCallback进行了注册,在步骤8出SingleRequest#onSizeReady方法里的engine.load中,我们看到最后一个参数,传入的是this,可以明白,engineJob.addCallback(cb)这里的cb的实现类就是SingleRequest。接下来,让我们看看SingleRequest的onResourceReady方法。 - -### SingleRequest#onResourceReady - -``` -/** - * A callback method that should never be invoked directly. - */ -@SuppressWarnings("unchecked") -@Override -public void onResourceReady(Resource resource, DataSource dataSource) { - ... - - // 从Resource中得到Bitmap对象 - Object received = resource.get(); - - ... - - onResourceReady((Resource) resource, (R) received, dataSource); -} - -private void onResourceReady(Resource resource, R resultDataSource dataSource) { - - ... - - try { - ... - - if (!anyListenerHandledUpdatingTarget) { - Transition animation = - animationFactory.build(dataSource, isFirstResource); - // 核心代码 - target.onResourceReady(result, animation); - } - } finally { - isCallingCallbacks = false; - } - - notifyLoadSuccess(); -} -``` - -在SingleRequest#onResourceReady方法中又调用了target.onResourceReady(result, animation)方法,这里的target其实就是我们在into方法中建立的那个BitmapImageViewTarget,看到BitmapImageViewTarget类,我们并没有发现onResourceReady方法,但是我们从它的子类ImageViewTarget中发现了onResourceReady方法,从这里继续往下看。 - -### ImageViewTarget#onResourceReady - -``` -public abstract class ImageViewTarget extends ViewTarget -implements Transition.ViewAdapter { - - ... - - @Override - public void onResourceReady(@NonNull Z resource, @Nullable Transition transition) { - if (transition == null || !transition.transition(resource, this)) { - // 核心代码 - setResourceInternal(resource); - } else { - maybeUpdateAnimatable(resource); - } - } - - ... - - private void setResourceInternal(@Nullable Z resource) { - // Order matters here. Set the resource first to make sure that the Drawable has a valid and - // non-null Callback before starting it. - // 核心代码 - setResource(resource); - maybeUpdateAnimatable(resource); - } - - // 核心代码 - protected abstract void setResource(@Nullable Z resource); -} -``` - -这里我们在回到BitmapImageViewTarget的setResource方法中,终于看到Bitmap被设置到了当前的imageView上了。 - -``` -public class BitmapImageViewTarget extends ImageViewTarget { - - ... - - - @Override - protected void setResource(Bitmap resource) { - view.setImageBitmap(resource); - } -} -``` - -到这里,我们的分析就结束了,从以上的分析可知,Glide将大部分的逻辑处理都放在了最后一个into方法中,里面经过了20多个分析步骤才将请求图片流、解码出图片,到最终设置到对应的imageView上。 - -## 完整Glide加载流程图 - -![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f9da2924eeab4ef79249b5836fd916da~tplv-k3u1fbpfcp-zoom-1.image) - - - -> 可以看到,Glide最核心的逻辑都聚集在into()方法中,它里面的设计精巧而复杂,这部分的源码分析非常耗时,但是,如果你真真正正地去一步步去深入其中,你也许在Android进阶之路上将会有顿悟的感觉。 - -# GreenDao - -## 基本使用流程 - -### 导入GreenDao的代码生成插件和库 - -``` -// 项目下的build.gradle -buildscript { - ... - dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' - classpath 'org.greenrobot:greendao-gradle-plugin:3.2.1' - } -} - -// app模块下的build.gradle -apply plugin: 'com.android.application' -apply plugin: 'org.greenrobot.greendao' - -... - -dependencies { - ... - compile 'org.greenrobot:greendao:3.2.0' -} -``` - -### 创建一个实体类,这里为HistoryData - -``` -@Entity -public class HistoryData { - - @Id(autoincrement = true) - private Long id; - - private long date; - - private String data; -} -``` - -### 选择ReBuild Project,HistoryData会被自动添加Set/get方法,并生成整个项目的DaoMaster、DaoSession类,以及与该实体HistoryData对应的HistoryDataDao。 - -![image](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/75ea30aea6034e939918d0da2b43c9d9~tplv-k3u1fbpfcp-zoom-1.image) - -``` -@Entity -public class HistoryData { - - @Id(autoincrement = true) - private Long id; - - private long date; - - private String data; - - @Generated(hash = 1371145256) - public HistoryData(Long id, long date, String data) { - this.id = id; - this.date = date; - this.data = data; - } - - @Generated(hash = 422767273) - public HistoryData() { - } - - public Long getId() { - return this.id; - } - - public void setId(Long id) { - this.id = id; - } - - public long getDate() { - return this.date; - } - - public void setDate(long date) { - this.date = date; - } - - public String getData() { - return this.data; - } - - public void setData(String data) { - this.data = data; - } -} -``` - -这里点明一下这几个类的作用: - -- DaoMaster:所有Dao类的主人,负责整个库的运行,内部的静态抽象子类DevOpenHelper继承并重写了Android的SqliteOpenHelper。 -- DaoSession:作为一个会话层的角色,用于生成相应的Dao对象、Dao对象的注册,操作Dao的具体对象。 -- xxDao(HistoryDataDao):生成的Dao对象,用于进行具体的数据库操作。 - -### 获取并使用相应的Dao对象进行增删改查操作 - -``` -DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(this, Constants.DB_NAME); -SQLiteDatabase database = devOpenHelper.getWritableDatabase(); -DaoMaster daoMaster = new DaoMaster(database); -mDaoSession = daoMaster.newSession(); -HistoryDataDao historyDataDao = daoSession.getHistoryDataDao(); - -// 省略创建historyData的代码 -... - -// 增 -historyDataDao.insert(historyData); - -// 删 -historyDataDao.delete(historyData); - -// 改 -historyDataDao.update(historyData); - -// 查 -List historyDataList = historyDataDao.loadAll(); -``` - -本节将会以上述使用流程来对GreenDao的源码进行逐步分析,最后会分析下GreenDao中一些优秀的特性,让大家对GreenDao的理解有更一步的加深。 - -## GreenDao使用流程分析 - -### 创建数据库帮助类对象DaoMaster.DevOpenHelper - -``` -DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(this, Constants.DB_NAME); -``` - -创建GreenDao内部实现的数据库帮助类对象devOpenHelper,核心源码如下: - -``` -public class DaoMaster extends AbstractDaoMaster { - - ... - - public static abstract class OpenHelper extends DatabaseOpenHelper { - - ... - - @Override - public void onCreate(Database db) { - Log.i("greenDAO", "Creating tables for schema version " + SCHEMA_VERSION); - createAllTables(db, false); - } - } - - public static class DevOpenHelper extends OpenHelper { - - ... - - @Override - public void onUpgrade(Database db, int oldVersion, int newVersion) { - Log.i("greenDAO", "Upgrading schema from version " + oldVersion + " to " + newVersion + " by dropping all tables"); - dropAllTables(db, true); - onCreate(db); - } - } -} -``` - -DevOpenHelper自身实现了更新的逻辑,这里是弃置了所有的表,并且调用了OpenHelper实现的onCreate方法用于创建所有的表,其中DevOpenHelper继承于OpenHelper,而OpenHelper自身又继承于DatabaseOpenHelper,那么,这个DatabaseOpenHelper这个类的作用是什么呢? - -``` -public abstract class DatabaseOpenHelper extends SQLiteOpenHelper { - - ... - - // 关注点1 - public Database getWritableDb() { - return wrap(getWritableDatabase()); - } - - public Database getReadableDb() { - return wrap(getReadableDatabase()); - } - - protected Database wrap(SQLiteDatabase sqLiteDatabase) { - return new StandardDatabase(sqLiteDatabase); - } - - ... - - // 关注点2 - public Database getEncryptedWritableDb(String password) { - EncryptedHelper encryptedHelper = checkEncryptedHelper(); - return encryptedHelper.wrap(encryptedHelper.getWritableDatabase(password)); - } - - public Database getEncryptedReadableDb(String password) { - EncryptedHelper encryptedHelper = checkEncryptedHelper(); - return encryptedHelper.wrap(encryptedHelper.getReadableDatabase(password)); - } - - ... - - private class EncryptedHelper extends net.sqlcipher.database.SQLiteOpenHelper { - - ... - - - protected Database wrap(net.sqlcipher.database.SQLiteDatabase sqLiteDatabase) { - return new EncryptedDatabase(sqLiteDatabase); - } - } -``` - -其实,DatabaseOpenHelper也是实现了SQLiteOpenHelper的一个帮助类,它内部可以获取到两种不同的数据库类型,一种是标准型的数据库**StandardDatabase**,另一种是加密型的数据库**EncryptedDatabase**,从以上源码可知,它们内部都通过wrap这样一个包装的方法,返回了对应的数据库类型,我们大致看一下StandardDatabase和EncryptedDatabase的内部实现。 - -``` -public class StandardDatabase implements Database { - - // 这里的SQLiteDatabase是android.database.sqlite.SQLiteDatabase包下的 - private final SQLiteDatabase delegate; - - public StandardDatabase(SQLiteDatabase delegate) { - this.delegate = delegate; - } - - @Override - public Cursor rawQuery(String sql, String[] selectionArgs) { - return delegate.rawQuery(sql, selectionArgs); - } - - @Override - public void execSQL(String sql) throws SQLException { - delegate.execSQL(sql); - } - - ... -} - -public class EncryptedDatabaseStatement implements DatabaseStatement { - - // 这里的SQLiteStatement是net.sqlcipher.database.SQLiteStatement包下的 - private final SQLiteStatement delegate; - - public EncryptedDatabaseStatement(SQLiteStatement delegate) { - this.delegate = delegate; - } - - @Override - public void execute() { - delegate.execute(); - } - - ... -} -``` - -StandardDatabase和EncryptedDatabase这两个类内部都使用了**代理模式**给相同的接口添加了不同的具体实现,StandardDatabase自然是使用的Android包下的SQLiteDatabase,而EncryptedDatabaseStatement为了实现加密数据库的功能,则使用了一个叫做**sqlcipher**的数据库加密三方库,**如果你项目下的数据库需要保存比较重要的数据,则可以使用getEncryptedWritableDb方法来代替getdWritableDb方法对数据库进行加密,这样,我们之后的数据库操作则会以代理模式的形式间接地使用sqlcipher提供的API去操作数据库**。 - -### 创建DaoMaster对象 - -``` -SQLiteDatabase database = devOpenHelper.getWritableDatabase(); -DaoMaster daoMaster = new DaoMaster(database); -``` - -首先,DaoMaster作为所有Dao对象的主人,它内部肯定是需要一个SQLiteDatabase对象的,因此,先由DaoMaster的帮助类对象devOpenHelper的getWritableDatabase方法得到一个标准的数据库类对象database,再由此创建一个DaoMaster对象。 - -``` -public class DaoMaster extends AbstractDaoMaster { - - ... - - public DaoMaster(SQLiteDatabase db) { - this(new StandardDatabase(db)); - } - - public DaoMaster(Database db) { - super(db, SCHEMA_VERSION); - registerDaoClass(HistoryDataDao.class); - } - - ... -} -``` - -在DaoMaster的构造方法中,它首先执行了super(db, SCHEMA_VERSION)方法,即它的父类AbstractDaoMaster的构造方法。 - -``` -public abstract class AbstractDaoMaster { - - ... - - public AbstractDaoMaster(Database db, int schemaVersion) { - this.db = db; - this.schemaVersion = schemaVersion; - - daoConfigMap = new HashMap>, DaoConfig>(); - } - - protected void registerDaoClass(Class> daoClass) { - DaoConfig daoConfig = new DaoConfig(db, daoClass); - daoConfigMap.put(daoClass, daoConfig); - } - - ... -} -``` - -在AbstractDaoMaster对象的构造方法中,除了记录当前的数据库对象db和版本schemaVersion之外,还创建了一个类型为**HashMap``, DaoConfig>()的daoConfigMap对象用于保存每一个DAO对应的数据配置对象DaoConfig,并且Daoconfig对象存储了对应的Dao对象所必需的数据**。最后,在DaoMaster的构造方法中使用了registerDaoClass(HistoryDataDao.class)方法将HistoryDataDao类对象进行了注册,实际上,就是为HistoryDataDao这个Dao对象创建了相应的DaoConfig对象并将它放入daoConfigMap对象中保存起来。 - -### 创建DaoSession对象 - -``` -mDaoSession = daoMaster.newSession(); -``` - -在DaoMaster对象中使用了newSession方法新建了一个DaoSession对象。 - -``` -public DaoSession newSession() { - return new DaoSession(db, IdentityScopeType.Session, daoConfigMap); -} -``` - -在DaoSeesion的构造方法中,又做了哪些事情呢? - -``` -public class DaoSession extends AbstractDaoSession { - - ... - - public DaoSession(Database db, IdentityScopeType type, Map>, DaoConfig> - daoConfigMap) { - super(db); - - historyDataDaoConfig = daoConfigMap.get(HistoryDataDao.class).clone(); - historyDataDaoConfig.initIdentityScope(type); - - historyDataDao = new HistoryDataDao(historyDataDaoConfig, this); - - registerDao(HistoryData.class, historyDataDao); - } - - ... -} -``` - -首先,调用了父类AbstractDaoSession的构造方法。 - -``` -public class AbstractDaoSession { - - ... - - public AbstractDaoSession(Database db) { - this.db = db; - this.entityToDao = new HashMap, AbstractDao>(); - } - - protected void registerDao(Class entityClass, AbstractDao dao) { - entityToDao.put(entityClass, dao); - } - - ... -} -``` - -在AbstractDaoSession构造方法里面**创建了一个实体与Dao对象的映射集合**。接下来,在DaoSession的构造方法中还做了2件事: - -1. **创建每一个Dao对应的DaoConfig对象**,这里是historyDataDaoConfig,**并且根据IdentityScopeType的类型初始化创建一个相应的IdentityScope**,根据type的不同,它有两种类型,分别是**IdentityScopeObject**和**IdentityScopeLong**,它的作用是根据主键缓存对应的实体数据。当主键是数字类型的时候,如long/Long、int/Integer、short/Short、byte/Byte,则使用IdentityScopeLong缓存实体数据,当主键不是数字类型的时候,则使用IdentityScopeObject缓存实体数据。 - -2. **根据DaoSession对象和每一个Dao对应的DaoConfig对象,创建与之对应的historyDataDao对象**,由于这个项目只创建了一个实体类HistoryData,因此这里只有一个Dao对象historyDataDao,然后就是注册Dao对象,其实就是将实体和对应的Dao对象放入entityToDao这个映射集合中保存起来了。 - -### 插入源码分析 - -``` -HistoryDataDao historyDataDao = daoSession.getHistoryDataDao(); - -// 增 -historyDataDao.insert(historyData); -``` - -这里首先在会话层DaoSession中获取了我们要操作的Dao对象HistoryDataDao,然后插入了一个我们预先创建好的historyData实体对象。其中HistoryDataDao继承了AbstractDao 。 - -``` -public class HistoryDataDao extends AbstractDao { - ... -} -``` - -那么,这个AbstractDao是干什么的呢? - -``` -public abstract class AbstractDao { - - ... - - public List loadAll() { - Cursor cursor = db.rawQuery(statements.getSelectAll(), null); - return loadAllAndCloseCursor(cursor); - } - - ... - - public long insert(T entity) { - return executeInsert(entity, statements.getInsertStatement(), true); - } - - ... - - public void delete(T entity) { - assertSinglePk(); - K key = getKeyVerified(entity); - deleteByKey(key); - } - - ... - -} -``` - -看到这里,根据程序员优秀的直觉,大家应该能猜到,AbstractDao是所有Dao对象的基类,它实现了实体数据的操作如增删改查。我们接着分析insert是如何实现的,在AbstractDao的insert方法中又调用了executeInsert这个方法。在这个方法中,第二个参里的statements是一个**TableStatements**对象,它是在AbstractDao初始化构造器时从DaoConfig对象中取出来的,是一个**根据指定的表格创建SQL语句的一个帮助类**。使用statements.getInsertStatement()则是获取了一个插入的语句。而第三个参数则是判断是否是主键的标志。 - -``` -public class TableStatements { - - ... - - public DatabaseStatement getInsertStatement() { - if (insertStatement == null) { - String sql = SqlUtils.createSqlInsert("INSERT INTO ", tablename, allColumns); - DatabaseStatement newInsertStatement = db.compileStatement(sql); - ... - } - return insertStatement; - } - - ... -} -``` - -在TableStatements的getInsertStatement方法中,主要做了两件事: - -1. **使用SqlUtils创建了插入的sql语句**。 - -2. **根据不同的数据库类型(标准数据库或加密数据库)将sql语句编译成当前数据库对应的语句**。 - -我们继续往下分析executeInsert的执行流程。 - -``` -private long executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach) { - long rowId; - if (db.isDbLockedByCurrentThread()) { - rowId = insertInsideTx(entity, stmt); - } else { - db.beginTransaction(); - try { - rowId = insertInsideTx(entity, stmt); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - if (setKeyAndAttach) { - updateKeyAfterInsertAndAttach(entity, rowId, true); - } - return rowId; -} -``` - -这里首先是判断数据库是否被当前线程锁定,如果是,则直接插入数据,否则为了避免死锁,则开启一个数据库事务,再进行插入数据的操作。最后如果设置了主键,则在插入数据之后更新主键的值并将对应的实体缓存到相应的identityScope中,这一块的代码流程如下所示: - -``` -protected void updateKeyAfterInsertAndAttach(T entity, long rowId, boolean lock) { - if (rowId != -1) { - K key = updateKeyAfterInsert(entity, rowId); - attachEntity(key, entity, lock); - } else { - ... - } -} - -protected final void attachEntity(K key, T entity, boolean lock) { - attachEntity(entity); - if (identityScope != null && key != null) { - if (lock) { - identityScope.put(key, entity); - } else { - identityScope.putNoLock(key, entity); - } - } -} -``` - -接着,我们还是继续追踪主线流程,在executeInsert这个方法中调用了insertInsideTx进行数据的插入。 - -``` -private long insertInsideTx(T entity, DatabaseStatement stmt) { - synchronized (stmt) { - if (isStandardSQLite) { - SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement(); - bindValues(rawStmt, entity); - return rawStmt.executeInsert(); - } else { - bindValues(stmt, entity); - return stmt.executeInsert(); - } - } -} -``` - -为了防止并发,这里使用了悲观锁保证了数据的一致性,在AbstractDao这个类中,大量使用了这种锁保证了它的线程安全性。接着,如果当前是标准数据库,则直接获取stmt这个DatabaseStatement类对应的原始语句进行实体字段属性的绑定和最后的执行插入操作。如果是加密数据库,则直接使用当前的加密数据库所属的插入语句进行实体字段属性的绑定和执行最后的插入操作。其中bindValues这个方法对应的实现类就是我们的HistoryDataDao类。 - -``` -public class HistoryDataDao extends AbstractDao { - - ... - - @Override - protected final void bindValues(DatabaseStatement stmt, HistoryData entity) { - stmt.clearBindings(); - - Long id = entity.getId(); - if (id != null) { - stmt.bindLong(1, id); - } - stmt.bindLong(2, entity.getDate()); - - String data = entity.getData(); - if (data != null) { - stmt.bindString(3, data); - } - } - - @Override - protected final void bindValues(SQLiteStatement stmt, HistoryData entity) { - stmt.clearBindings(); - - Long id = entity.getId(); - if (id != null) { - stmt.bindLong(1, id); - } - stmt.bindLong(2, entity.getDate()); - - String data = entity.getData(); - if (data != null) { - stmt.bindString(3, data); - } - } - - ... -} -``` - -可以看到,这里对HistoryData的所有字段使用对应的数据库语句进行了绑定操作。这里最后再提及一下,**如果当前数据库是加密型时,则会使用最开始提及的DatabaseStatement的加密实现类EncryptedDatabaseStatement应用代理模式去使用sqlcipher这个加密型数据库的insert方法**。 - -### 查询源码分析 - -经过对插入源码的分析,相信大家对GreenDao内部的机制已经有了一些自己的理解,由于删除和更新内部的流程比较简单,且与插入源码有异曲同工之妙,这里就不再赘述了。最后再分析下查询的源码,查询的流程调用链较长,所以将它的核心流程源码直接给出。 - -``` -List historyDataList = historyDataDao.loadAll(); - -public List loadAll() { - Cursor cursor = db.rawQuery(statements.getSelectAll(), null); - return loadAllAndCloseCursor(cursor); -} - -protected List loadAllAndCloseCursor(Cursor cursor) { - try { - return loadAllFromCursor(cursor); - } finally { - cursor.close(); - } -} - -protected List loadAllFromCursor(Cursor cursor) { - int count = cursor.getCount(); - ... - boolean useFastCursor = false; - if (cursor instanceof CrossProcessCursor) { - window = ((CrossProcessCursor) cursor).getWindow(); - if (window != null) { - if (window.getNumRows() == count) { - cursor = new FastCursor(window); - useFastCursor = true; - } else { - ... - } - } - } - - if (cursor.moveToFirst()) { - ... - try { - if (!useFastCursor && window != null && identityScope != null) { - loadAllUnlockOnWindowBounds(cursor, window, list); - } else { - do { - list.add(loadCurrent(cursor, 0, false)); - } while (cursor.moveToNext()); - } - } finally { - ... - } - } - return list; -} -``` - -最终,loadAll方法将会调用到loadAllFromCursor这个方法,首先,如果**当前的游标cursor是跨进程的cursor**,并且cursor的行数没有偏差的话,则使用一个加快版的**FastCursor**对象进行游标遍历。接着,不管是执行loadAllUnlockOnWindowBounds这个方法还是直接加载当前的数据列表list.add(loadCurrent(cursor, 0, false)),最后都会调用到这行list.add(loadCurrent(cursor, 0, false))代码,很明显,loadCurrent方法就是加载数据的方法。 - -``` -final protected T loadCurrent(Cursor cursor, int offset, boolean lock) { - if (identityScopeLong != null) { - ... - T entity = lock ? identityScopeLong.get2(key) : identityScopeLong.get2NoLock(key); - if (entity != null) { - return entity; - } else { - entity = readEntity(cursor, offset); - attachEntity(entity); - if (lock) { - identityScopeLong.put2(key, entity); - } else { - identityScopeLong.put2NoLock(key, entity); - } - return entity; - } - } else if (identityScope != null) { - ... - T entity = lock ? identityScope.get(key) : identityScope.getNoLock(key); - if (entity != null) { - return entity; - } else { - entity = readEntity(cursor, offset); - attachEntity(key, entity, lock); - return entity; - } - } else { - ... - T entity = readEntity(cursor, offset); - attachEntity(entity); - return entity; - } -} -``` - -#### loadCurrent方法内部的执行策略 - -**首先,如果有实体数据缓存identityScopeLong/identityScope,则先从缓存中取,如果缓存中没有,会使用该实体对应的Dao对象,这里的是HistoryDataDao,它在内部根据游标取出的数据新建了一个新的HistoryData实体对象返回。** - -``` -@Override -public HistoryData readEntity(Cursor cursor, int offset) { - HistoryData entity = new HistoryData( // - cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), // id - cursor.getLong(offset + 1), // date - cursor.isNull(offset + 2) ? null : cursor.getString(offset + 2) // data - ); - return entity; -} -``` - -**最后,如果是非identityScopeLong缓存类型,即是属于identityScope的情况下,则还会在identityScope中将上面获得的数据进行缓存。如果没有实体数据缓存的话,则直接调用readEntity组装数据返回即可。** - -注意:对于GreenDao缓存的特性,可能会出现没有拿到最新数据的bug,因此,如果遇到这种情况,可以使用DaoSession的clear方法删除缓存。 - -## GreenDao是如何与ReactiveX结合? - -首先,看下与rx结合的使用流程: - -``` -RxDao xxDao = daoSession.getHistoryDataDao().rx(); -xxDao.insert(historyData) - .observerOn(AndroidSchedulers.mainThread()) - .subscribe(new Action1() { - @Override - public void call(HistoryData entity) { - // insert success - } - }); -``` - -在AbstractDao对象的.rx()方法中,创建了一个默认执行在io线程的rxDao对象。 - -``` -@Experimental -public RxDao rx() { - if (rxDao == null) { - rxDao = new RxDao<>(this, Schedulers.io()); - } - return rxDao; -} -``` - -接着分析rxDao的insert方法。 - -``` -@Experimental -public Observable insert(final T entity) { - return wrap(new Callable() { - @Override - public T call() throws Exception { - dao.insert(entity); - return entity; - } - }); -} -``` - -起实质作用的就是这个wrap方法了,在这个方法里面主要是调用了RxUtils.fromCallable(callable)这个方法。 - -``` -@Internal -class RxBase { - - ... - - protected Observable wrap(Callable callable) { - return wrap(RxUtils.fromCallable(callable)); - } - - protected Observable wrap(Observable observable) { - if (scheduler != null) { - return observable.subscribeOn(scheduler); - } else { - return observable; - } - } - - ... -} -``` - -在RxUtils的fromCallable这个方法内部,其实就是**使用defer这个延迟操作符来进行被观察者事件的发送,主要目的就是为了确保Observable被订阅后才执行**。最后,如果调度器scheduler存在的话,将通过外部的wrap方法将执行环境调度到io线程。 - -``` -@Internal -class RxUtils { - - @Internal - static Observable fromCallable(final Callable callable) { - return Observable.defer(new Func0>() { - - @Override - public Observable call() { - T result; - try { - result = callable.call(); - } catch (Exception e) { - return Observable.error(e); - } - return Observable.just(result); - } - }); - } -} -``` - - - -> 在分析完GreenDao的核心源码之后发现,GreenDao作为最好的数据库框架之一,是有一定道理的。 -> -> **首先,它通过使用自身的插件配套相应的freemarker模板生成所需的静态代码,避免了反射等消耗性能的操作。** -> -> **其次,它内部提供了实体数据的映射缓存机制,能够进一步加快查询速度。对于不同数据库对应的SQL语句,也使用了不同的DataBaseStatement实现类结合代理模式进行了封装,屏蔽了数据库操作等繁琐的细节。** -> -> **最后,它使用了sqlcipher提供了加密数据库的功能,在一定程度确保了安全性,同时,结合RxJava,我们便能更简洁地实现异步的数据库操作**。 - -# RxJava - -## RxJava到底是什么? - -RxJava是基于Java虚拟机上的响应式扩展库,它通过**使用可观察的序列将异步和基于事件的程序组合起来**。 与此同时,它**扩展了观察者模式来支持数据/事件序列**,并且添加了操作符,这些**操作符允许你声明性地组合序列**,同时抽象出要关注的问题:比如低级线程、同步、线程安全和并发数据结构等。 - -从RxJava的官方定义来看,我们如果要想真正地理解RxJava,就必须对它以下两个部分进行深入的分析: - -1. **订阅流程** - -2. **线程切换** - -当然,RxJava操作符的源码也是很不错的学习资源,特别是FlatMap、Zip等操作符的源码,有很多可以借鉴的地方,但是它们内部的实现比较复杂。 - -## RxJava的订阅流程 - -首先给出RxJava消息订阅的例子: - -``` -Observable.create(newObservableOnSubscribe() { - @Override - public void subscribe(ObservableEmitteremitter) throws Exception { - emitter.onNext("1"); - emitter.onNext("2"); - emitter.onNext("3"); - emitter.onComplete(); - } -}).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - Log.d(TAG, "onSubscribe"); - } - @Override - public void onNext(String s) { - Log.d(TAG, "onNext : " + s); - } - @Override - public void onError(Throwable e) { - Log.d(TAG, "onError : " + e.toString()); - } - @Override - public void onComplete() { - Log.d(TAG, "onComplete"); - } -}); -``` - -可以看到,这里首先创建了一个被观察者,然后创建一个观察者订阅了这个被观察者,因此下面分两个部分对RxJava的订阅流程进行分析: - -1. **创建被观察者过程** - -2. **订阅过程** - -### 创建被观察者过程 - -首先,上面使用了Observable类的create()方法创建了一个被观察者,看看里面做了什么。 - -#### Observable#create() - -``` -// 省略一些检测性的注解 -public static Observable create(ObservableOnSubscribe source) { - ObjectHelper.requireNonNull(source, "source is null"); - return RxJavaPlugins.onAssembly(new ObservableCreate(source)); -} -``` - -在Observable的create()里面实际上是创建了一个新的ObservableCreate对象,同时,把我们定义好的ObservableOnSubscribe对象传入了ObservableCreate对象中,最后调用了RxJavaPlugins.onAssembly()方法。接下来看看这个ObservableCreate是干什么的。 - -#### ObservableCreate - -``` -public final class ObservableCreate extends Observable { - - final ObservableOnSubscribe source; - - public ObservableCreate(ObservableOnSubscribe source) { - this.source = source; - } - - ... -} -``` - -这里仅仅是把ObservableOnSubscribe这个对象保存在ObservableCreate中了。然后看看RxJavaPlugins.onAssembly()这个方法的处理。 - -#### RxJavaPlugins#onAssembly() - -``` -public static Observable onAssembly(@NonNull Observable source) { - - // 应用hook函数的一些处理,一般用到不到 - ... - return source; -} -``` - -最终仅仅是把我们的ObservableCreate给返回了。 - -#### 创建被观察者过程小结 - -从以上分析可知,Observable.create()方法仅仅是**先将我们自定义的ObservableOnSubscribe对象重新包装成了一个ObservableCreate对象**。 - -### 订阅过程 - -接着,看看Observable.subscribe()的订阅过程是如何实现的。 - -#### Observable#subscribe() - -``` -public final void subscribe(Observer observer) { - ... - - // 1 - observer = RxJavaPlugins.onSubscribe(this,observer); - - ... - - // 2 - subscribeActual(observer); - - ... -} -``` - -在注释1处,在Observable的subscribe()方法内部首先调用了RxJavaPlugins的onSubscribe()方法。 - -#### RxJavaPlugins#onSubscribe() - -``` -public static Observer onSubscribe(@NonNull Observable source, @NonNull Observer observer) { - - // 应用hook函数的一些处理,一般用到不到 - ... - - return observer; -} -``` - -除去hook应用的逻辑,这里仅仅是将observer返回了。接着来分析下注释2处的subscribeActual()方法, - -#### Observable#subscribeActual() - -``` -protected abstract void subscribeActual(Observer observer); -``` - -这是一个抽象的方法,很明显,它对应的具体实现类就是我们在第一步创建的ObservableCreate类,接下来看到ObservableCreate的subscribeActual()方法。 - -#### ObservableCreate#subscribeActual() - -``` -@Override -protected void subscribeActual(Observer observer) { - // 1 - CreateEmitter parent = new CreateEmitter(observer); - // 2 - observer.onSubscribe(parent); - - try { - // 3 - source.subscribe(parent); - } catch (Throwable ex) { - Exceptions.throwIfFatal(ex); - parent.onError(ex); - } -} -``` - -在注释1处,首先新创建了一个CreateEmitter对象,同时传入了我们自定义的observer对象进去。 - -##### CreateEmitter - -``` -static final class CreateEmitter -extends AtomicReference -implements ObservableEmitter, Disposable { - - ... - - final Observer observer; - - CreateEmitter(Observer observer) { - this.observer = observer; - } - - ... -} -``` - -从上面可以看出,**CreateEmitter通过继承了Java并发包中的原子引用类AtomicReference保证了事件流切断状态Dispose的一致性**(这里不理解的话,看到后面讲解Dispose的时候就明白了),并**实现了ObservableEmitter接口和Disposable接口**,接着我们分析下注释2处的observer.onSubscribe(parent),这个onSubscribe回调的含义其实就是**告诉观察者已经成功订阅了被观察者**。再看到注释3处的source.subscribe(parent)这行代码,这里的source其实是ObservableOnSubscribe对象,我们看到ObservableOnSubscribe的subscribe()方法。 - -##### ObservableOnSubscribe#subscribe() - -``` -Observable observable = Observable.create(new ObservableOnSubscribe() { - @Override - public voidsubscribe(ObservableEmitter emitter) throws Exception { - emitter.onNext("1"); - emitter.onNext("2"); - emitter.onNext("3"); - emitter.onComplete(); - } -}); -``` - -这里面使用到了ObservableEmitter的onNext()方法将事件流发送出去,最后调用了onComplete()方法完成了订阅过程。ObservableEmitter是一个抽象类,实现类就是我们传入的CreateEmitter对象,接下来我们看看CreateEmitter的onNext()方法和onComplete()方法的处理。 - -##### CreateEmitter#onNext() && CreateEmitter#onComplete() - -``` -static final class CreateEmitter -extends AtomicReference -implements ObservableEmitter, Disposable { - -... - -@Override -public void onNext(T t) { - ... - - if (!isDisposed()) { - //调用观察者的onNext() - observer.onNext(t); - } -} - -@Override -public void onComplete() { - if (!isDisposed()) { - try { - observer.onComplete(); - } finally { - dispose(); - } - } -} - - -... - -} -``` - -在CreateEmitter的onNext和onComplete方法中首先都要经过一个**isDisposed**的判断,作用就是看**当前的事件流是否被切断(废弃)掉了**,默认是不切断的,如果想要切断,可以调用Disposable的dispose()方法将此状态设置为切断(废弃)状态。继续看看这个isDisposed内部的处理。 - -##### ObservableEmitter#isDisposed() - -``` -@Override -public boolean isDisposed() { - return DisposableHelper.isDisposed(get()); -} -``` - -注意到这里通过get()方法首先从ObservableEmitter的AtomicReference中拿到了保存的Disposable状态。然后交给了DisposableHelper进行判断处理。接下来看看DisposableHelper的处理。 - -##### DisposableHelper#isDisposed() && DisposableHelper#set() - -``` -public enum DisposableHelper implements Disposable { - - DISPOSED; - - public static boolean isDisposed(Disposable d) { - // 1 - return d == DISPOSED; - } - - public static boolean set(AtomicReference field, Disposable d) { - for (;;) { - Disposable current = field.get(); - if (current == DISPOSED) { - if (d != null) { - d.dispose(); - } - return false; - } - // 2 - if (field.compareAndSet(current, d)) { - if (current != null) { - current.dispose(); - } - return true; - } - } - } - - ... - - public static boolean dispose(AtomicReference field) { - Disposable current = field.get(); - Disposable d = DISPOSED; - if (current != d) { - // ... - current = field.getAndSet(d); - if (current != d) { - if (current != null) { - current.dispose(); - } - return true; - } - } - return false; - } - - ... -} -``` - -DisposableHelper是一个枚举类,内部只有一个值即DISPOSED, 从上面的分析可知它就是用来**标记事件流被切断(废弃)状态的**。先看到注释2和注释3处的代码**field.compareAndSet(current, d)和field.getAndSet(d)**,这里使用了**原子引用AtomicReference内部包装的[CAS](https://www.jianshu.com/p/ab2c8fce878b)方法处理了标志Disposable的并发读写问题**。最后看到注释3处,将我们传入的CreateEmitter这个原子引用类保存的Dispable状态和DisposableHelper内部的DISPOSED进行比较,如果相等,就证明数据流被切断了。为了更进一步理解Disposed的作用,再来看看CreateEmitter中剩余的关键方法。 - -##### CreateEmitter - -``` -@Override -public void onNext(T t) { - ... - // 1 - if (!isDisposed()) { - observer.onNext(t); - } -} - -@Override -public void onError(Throwable t) { - if (!tryOnError(t)) { - // 2 - RxJavaPlugins.onError(t); - } -} - -@Override -public boolean tryOnError(Throwable t) { - ... - // 3 - if (!isDisposed()) { - try { - observer.onError(t); - } finally { - // 4 - dispose(); - } - return true; - } - return false; -} - -@Override -public void onComplete() { - // 5 - if (!isDisposed()) { - try { - observer.onComplete(); - } finally { - // 6 - dispose(); - } - } -} -``` - -在注释1、3、5处,onNext()和onError()、onComplete()方法首先都会判断事件流是否被切断,如果事件流此时被切断了,那么onNext()和onComplete()则会退出方法体,不做处理,**onError()则会执行到RxJavaPlugins.onError(t)这句代码,内部会直接抛出异常,导致崩溃**。如果事件流没有被切断,那么在onError()和onComplete()内部最终会调用到注释4、6处的这句dispose()代码,将事件流进行切断,由此可知,**onError()和onComplete()只能调用一个,如果先执行的是onComplete(),再调用onError()的话就会导致异常崩溃**。 - -## RxJava的线程切换 - -首先给出RxJava线程切换的例子: - -``` -Observable.create(new ObservableOnSubscribe() { - @Override - public voidsubscribe(ObservableEmitteremitter) throws Exception { - emitter.onNext("1"); - emitter.onNext("2"); - emitter.onNext("3"); - emitter.onComplete(); - } -}) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - Log.d(TAG, "onSubscribe"); - } - @Override - public void onNext(String s) { - Log.d(TAG, "onNext : " + s); - } - @Override - public void onError(Throwable e) { - Log.d(TAG, "onError : " +e.toString()); - } - @Override - public void onComplete() { - Log.d(TAG, "onComplete"); - } -}); -``` - -可以看到,RxJava的线程切换主要**分为subscribeOn()和observeOn()方法**,首先,来分析下subscribeOn()方法。 - -### subscribeOn(Schedulers.io()) - -在Schedulers.io()方法中,我们需要先传入一个Scheduler调度类,这里是传入了一个调度到io子线程的调度类,我们看看这个Schedulers.io()方法内部是怎么构造这个调度器的。 - -### Schedulers#io() - -``` -static final Scheduler IO; - -... - -public static Scheduler io() { - // 1 - return RxJavaPlugins.onIoScheduler(IO); -} - -static { - ... - - // 2 - IO = RxJavaPlugins.initIoScheduler(new IOTask()); -} - -static final class IOTask implements Callable { - @Override - public Scheduler call() throws Exception { - // 3 - return IoHolder.DEFAULT; - } -} - -static final class IoHolder { - // 4 - static final Scheduler DEFAULT = new IoScheduler(); -} -``` - -Schedulers这个类的代码很多,这里我只拿出有关Schedulers.io这个方法涉及的逻辑代码进行讲解。首先,在注释1处,同前面分析的订阅流程的处理一样,只是一个处理hook的逻辑,最终返回的还是传入的这个IO对象。再看到注释2处,**在Schedulers的静态代码块中将IO对象进行了初始化,其实质就是新建了一个IOTask的静态内部类**,在IOTask的call方法中,也就是注释3处,可以了解到使用了静态内部类的方式把创建的IOScheduler对象给返回出去了。绕了这么大圈子,**Schedulers.io方法其实质就是返回了一个IOScheduler对象**。 - -### Observable#subscribeOn() - -``` - public final Observable subscribeOn(Scheduler scheduler) { - ... - - return RxJavaPlugins.onAssembly(new ObservableSubscribeOn(this, scheduler)); -} -``` - -在subscribeOn()方法里面,又将ObservableCreate包装成了一个ObservableSubscribeOn对象。我们关注到ObservableSubscribeOn类。 - -### ObservableSubscribeOn - -``` -public final class ObservableSubscribeOn extends AbstractObservableWithUpstream { - final Scheduler scheduler; - - public ObservableSubscribeOn(ObservableSource source, Scheduler scheduler) { - // 1 - super(source); - this.scheduler = scheduler; - } - - @Override - public void subscribeActual(final Observer observer) { - // 2 - final SubscribeOnObserver parent = new SubscribeOnObserver(observer); - - // 3 - observer.onSubscribe(parent); - - // 4 - parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent))); - } - -... -} -``` - -首先,在注释1处,将传进来的source和scheduler保存起来。接着,等到实际订阅的时候,就会执行到这个subscribeActual方法,在注释2处,将我们自定义的Observer包装成了一个SubscribeOnObserver对象。在注释3处,通知观察者订阅了被观察者。在注释4处,内部先创建了一个SubscribeTask对象,来看看它的实现。 - -### ObservableSubscribeOn#SubscribeTask - -``` -final class SubscribeTask implements Runnable { - private final SubscribeOnObserver parent; - - SubscribeTask(SubscribeOnObserver parent) { - this.parent = parent; - } - - @Override - public void run() { - source.subscribe(parent); - } -} -``` - -SubscribeTask是ObservableSubscribeOn的内部类,它实质上就是一个任务类,在它的run方法中会执行到source.subscribe(parent)的订阅方法,**这个source其实就是我们在ObservableSubscribeOn构造方法中传进来的ObservableCreate对象**。接下来看看scheduler.scheduleDirect()内部的处理。 - -### Scheduler#scheduleDirect() - -``` -public Disposable scheduleDirect(@NonNull Runnable run) { - return scheduleDirect(run, 0L, TimeUnit.NANOSECONDS); -} - -public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) { - - // 1 - final Worker w = createWorker(); - - // 2 - final Runnable decoratedRun = RxJavaPlugins.onSchedule(run); - - // 3 - DisposeTask task = new DisposeTask(decoratedRun, w); - - // 4 - w.schedule(task, delay, unit); - - return task; -} -``` - -这里最后会执行到上面这个scheduleDirect()重载方法。首先,在注释1处,会调用createWorker()方法创建一个工作者对象Worker,它是一个抽象类,这里的实现类就是IoScheduler,下面看看IoScheduler类的createWorker()方法。 - -#### IOScheduler#createWorker() - -``` -final AtomicReference pool; - -... - -public IoScheduler(ThreadFactory threadFactory) { - this.threadFactory = threadFactory; - this.pool = new AtomicReference(NONE); - start(); -} - -... - -@Override -public Worker createWorker() { - // 1 - return new EventLoopWorker(pool.get()); -} - -static final class EventLoopWorker extends Scheduler.Worker { - ... - - EventLoopWorker(CachedWorkerPool pool) { - this.pool = pool; - this.tasks = new CompositeDisposable(); - // 2 - this.threadWorker = pool.get(); - } - -} -``` - -首先,在注释1处调用了pool.get()这个方法,**pool是一个CachedWorkerPool类型的原子引用对象**,它的作用就是**用于缓存工作者对象Worker的**。然后,将得到的CachedWorkerPool传入新创建的EventLoopWorker对象中。重点关注一下注释2处,这里将CachedWorkerPool缓存的threadWorker对象保存起来了。 - -下面继续分析3.6处代码段的注释2处的代码,这里又是一个关于hook的封装处理,最终还是返回的当前的Runnable对象。在注释3处新建了一个切断任务DisposeTask将decoratedRun和w对象包装了起来。最后在注释4处调用了工作者的schedule()方法。下面来分析下它内部的处理。 - -#### IoScheduler#schedule() - -``` -@Override -public Disposable schedule(@NonNull Runnableaction, long delayTime, @NonNull TimeUnit unit){ - ... - - return threadWorker.scheduleActual(action,delayTime, unit, tasks); -} -``` - -内部调用了threadWorker的scheduleActual()方法,实际上是调用到了父类NewThreadWorker的scheduleActual()方法,继续看看NewThreadWorker的scheduleActual()方法中做的事情。 - -#### NewThreadWorker#scheduleActual() - -``` -public NewThreadWorker(ThreadFactory threadFactory) { - executor = SchedulerPoolFactory.create(threadFactory); -} - - -@NonNull -public ScheduledRunnable scheduleActual(final Runnable run, long delayTime, @NonNull TimeUnit unit, @Nullable DisposableContainer parent) { - Runnable decoratedRun = RxJavaPlugins.onSchedule(run); - - // 1 - ScheduledRunnable sr = new ScheduledRunnable(decoratedRun, parent); - - - if (parent != null) { - if (!parent.add(sr)) { - return sr; - } - } - - Future f; - try { - // 2 - if (delayTime <= 0) { - // 3 - f = executor.submit((Callable)sr); - } else { - // 4 - f = executor.schedule((Callable)sr, delayTime, unit); - } - sr.setFuture(f); - } catch (RejectedExecutionException ex) { - if (parent != null) { - parent.remove(sr); - } - RxJavaPlugins.onError(ex); - } - - return sr; -} -``` - -在NewThreadWorker的scheduleActual()方法的内部,在注释1处首先会新建一个ScheduledRunnable对象,将Runnable对象和parent包装起来了,**这里parent是一个DisposableContainer对象,它实际的实现类是CompositeDisposable类,它是一个保存所有事件流是否被切断状态的容器,其内部的实现是使用了RxJava自己定义的一个简单的OpenHashSet类进行存储**。最后注释2处,判断是否设置了延迟时间,如果设置了,则调用线程池的submit()方法立即进行线程切换,否则,调用schedule()方法进行延时执行线程切换。 - -### 为什么多次执行subscribeOn(),只有第一次有效? - -从上面的分析,可以很容易了解到**被观察者被订阅时是从最外面的一层(ObservableSubscribeOn)通知到里面的一层(ObservableOnSubscribe)**,当连续执行了到多次subscribeOn()的时候,其实就是先执行倒数第一次的subscribeOn()方法,直到最后一次执行的subscribeOn()方法,这样肯定会覆盖前面的线程切换。 - -### observeOn(AndroidSchedulers.mainThread()) - -``` -public final Observable observeOn(Scheduler scheduler) { - return observeOn(scheduler, false, bufferSize()); -} - -public final Observable observeOn(Scheduler scheduler, boolean delayError, int bufferSize) { - .... - - return RxJavaPlugins.onAssembly(new ObservableObserveOn(this, scheduler, delayError, bufferSize)); -} -``` - -可以看到,observeOn()方法内部最终也是返回了一个ObservableObserveOn对象,直接来看看ObservableObserveOn的subscribeActual()方法。 - -### ObservableObserveOn#subscribeActual() - -``` -@Override -protected void subscribeActual(Observer observer) { - // 1 - if (scheduler instanceof TrampolineScheduler) { - // 2 - source.subscribe(observer); - } else { - // 3 - Scheduler.Worker w = scheduler.createWorker(); - // 4 - source.subscribe(new ObserveOnObserver(observer, w, delayError, bufferSize)); - } -} -``` - -首先,在注释1处,判断指定的调度器是不是TrampolineScheduler,这是一个不进行线程切换,立即执行当前代码的调度器。如果是,则会直接调用ObservableSubscribeOn的subscribe()方法,如果不是,则会在注释3处创建一个工作者对象。然后,在注释4处创建一个新的ObserveOnObserver将SubscribeOnobserver对象包装起来,并传入ObservableSubscribeOn的subscribe()方法进行订阅。接下来看看ObserveOnObserver类的重点方法。 - -### ObserveOnObserver - -``` -@Override -public void onNext(T t) { - ... - if (sourceMode != QueueDisposable.ASYNC) { - // 1 - queue.offer(t); - } - schedule(); -} - -@Override -public void onError(Throwable t) { - ... - schedule(); -} - -@Override -public void onComplete() { - ... - schedule(); -} - -``` - -去除非主线逻辑的代码,在ObserveOnObserver的onNext()和onError()、onComplete()方法中最后都会调用到schedule()方法。接着看schedule()方法,其中**onNext()还会把消息存放到队列中**。 - -### ObserveOnObserver#schedule() - -``` -void schedule() { - if (getAndIncrement() == 0) { - worker.schedule(this); - } -} -``` - -这里使用了worker进行调度ObserveOnObserver这个实现了Runnable的任务。worker就是在AndroidSchedulers.mainThread()中创建的,内部其实就是**使用Handler进行线程切换的**,此处不再赘述了。接着看ObserveOnObserver的run()方法。 - -### ObserveOnObserver#run() - -``` -@Override -public void run() { - // 1 - if (outputFused) { - drainFused(); - } else { - // 2 - drainNormal(); - } -} -``` - -在注释1处会**先判断outputFused这个标志位,它表示事件流是否被融化掉,默认是false,所以,最后会执行到drainNormal()方法**。接着看看drainNormal()方法内部的处理。 - -### ObserveOnObserver#drainNormal() - -``` -void drainNormal() { - int missed = 1; - - final SimpleQueue q = queue; - - // 1 - final Observer a = downstream; - - ... - - // 2 - v = q.poll(); - - ... - // 3 - a.onNext(v); - - ... -} -``` - -在注释1处,这里的downstream实际上是从外面传进来的SubscribeOnObserver对象。在注释2处将队列中的消息取出来,接着在注释3处调用了SubscribeOnObserver的onNext方法。**最终,会从我们包装类的最外层一直调用到最里面的我们自定义的Observer中的onNext()方法,所以,在observeOn()方法下面的链式代码都会执行到它所指定的线程中,噢,原来如此**。 - - - -> 很多人使用RxJava也已经挺长时间了,但是一直没有去深入去了解过它的内部实现原理,**如今细细品尝,的确是酣畅淋漓**。 - -# LeakCanary - -## 原理概述 - -查看Leakcanary官方的github仓库,最重要的便是对**Leakcanary是如何起作用的**(即原理)这一问题进行了阐述,把它翻译成了易于理解的文字,主要分为如下7个步骤: - -1. RefWatcher.watch()创建了一个KeyedWeakReference用于去观察对象。 - -2. 然后,在后台线程中,它会检测引用是否被清除了,并且是否没有触发GC。 - -3. 如果引用仍然没有被清除,那么它将会把堆栈信息保存在文件系统中的.hprof文件里。 - -4. HeapAnalyzerService被开启在一个独立的进程中,并且HeapAnalyzer使用了HAHA开源库解析了指定时刻的堆栈快照文件heap dump。 - -5. 从heap dump中,HeapAnalyzer根据一个独特的引用key找到了KeyedWeakReference,并且定位了泄露的引用。 - -6. HeapAnalyzer为了确定是否有泄露,计算了到GC Roots的最短强引用路径,然后建立了导致泄露的链式引用。 - -7. 这个结果被传回到app进程中的DisplayLeakService,然后一个泄露通知便展现出来了。 - -官方的原理简单来解释就是这样的:**在一个Activity执行完onDestroy()之后,将它放入WeakReference中,然后将这个WeakReference类型的Activity对象与ReferenceQueque关联。这时再从ReferenceQueque中查看是否有没有该对象,如果没有,执行gc,再次查看,还是没有的话则判断发生内存泄露了。最后用HAHA这个开源库去分析dump之后的heap内存。** - -## 简单示例 - -下面这段是Leakcanary官方仓库的示例代码: - -首先在你项目app下的build.gradle中配置: - -``` -dependencies { - debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.2' - releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2' - // 可选,如果你使用支持库的fragments的话 - debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.2' -} -``` - -然后在你的Application中配置: - -``` -public class WanAndroidApp extends Application { - - private RefWatcher refWatcher; - - public static RefWatcher getRefWatcher(Context context) { - WanAndroidApp application = (WanAndroidApp) context.getApplicationContext(); - return application.refWatcher; - } - - @Override public void onCreate() { - super.onCreate(); - if (LeakCanary.isInAnalyzerProcess(this)) { - // 1 - return; - } - // 2 - refWatcher = LeakCanary.install(this); - } -} -``` - -在注释1处,会首先判断当前进程是否是Leakcanary专门用于分析heap内存的而创建的那个进程,即HeapAnalyzerService所在的进程,如果是的话,则不进行Application中的初始化功能。如果是当前应用所处的主进程的话,则会执行注释2处的LeakCanary.install(this)进行LeakCanary的安装。只需这样简单的几行代码,我们就可以在应用中检测是否产生了内存泄露了。当然,这样使用只会检测Activity和标准Fragment是否发生内存泄漏,如果要检测V4包的Fragment在执行完onDestroy()之后是否发生内存泄露的话,则需要在Fragment的onDestroy()方法中加上如下两行代码去监视当前的Fragment: - -``` -RefWatcher refWatcher = WanAndroidApp.getRefWatcher(_mActivity); -refWatcher.watch(this); -``` - -上面的**RefWatcher其实就是一个引用观察者对象,是用于监测当前实例对象的引用状态的**。从以上的分析可以了解到,核心代码就是LeakCanary.install(this)这行代码,接下来,就从这里出发将LeakCanary一步一步进行拆解。 - -## 源码分析 - -### LeakCanary#install() - -``` -public static @NonNull RefWatcher install(@NonNull Application application) { - return refWatcher(application).listenerServiceClass(DisplayLeakService.class) - .excludedRefs(AndroidExcludedRefs.createAppDefaults().build()) - .buildAndInstall(); -} -``` - -在install()方法中的处理,可以分解为如下四步: - -1. **refWatcher(application)** - -2. **链式调用listenerServiceClass(DisplayLeakService.class)** - -3. **链式调用excludedRefs(AndroidExcludedRefs.createAppDefaults().build())** - -4. **链式调用buildAndInstall()** - -首先,我们来看下第一步,这里调用了LeakCanary类的refWatcher方法,如下所示: - -``` -public static @NonNull AndroidRefWatcherBuilder refWatcher(@NonNull Context context) { - return new AndroidRefWatcherBuilder(context); -} -``` - -然后新建了一个AndroidRefWatcherBuilder对象,再看看AndroidRefWatcherBuilder这个类。 - -### AndroidRefWatcherBuilder - -``` -/** A {@link RefWatcherBuilder} with appropriate Android defaults. */ -public final class AndroidRefWatcherBuilder extends RefWatcherBuilder { - -... - - AndroidRefWatcherBuilder(@NonNull Context context) { - this.context = context.getApplicationContext(); - } - -... -} -``` - -在AndroidRefWatcherBuilder的构造方法中仅仅是将外部传入的applicationContext对象保存起来了。**AndroidRefWatcherBuilder是一个适配Android平台的引用观察者构造器对象,它继承了RefWatcherBuilder,RefWatcherBuilder是一个负责建立引用观察者RefWatcher实例的基类构造器**。继续看看RefWatcherBuilder这个类。 - -### RefWatcherBuilder - -``` -public class RefWatcherBuilder> { - - ... - - public RefWatcherBuilder() { - heapDumpBuilder = new HeapDump.Builder(); - } - - ... -} -``` - -在RefWatcher的基类构造器RefWatcherBuilder的构造方法中新建了一个HeapDump的构造器对象。其中**HeapDump就是一个保存heap dump信息的数据结构**。 - -接着来分析下install()方法中的链式调用的listenerServiceClass(DisplayLeakService.class)这部分逻辑。 - -### AndroidRefWatcherBuilder#listenerServiceClass() - -``` -public @NonNull AndroidRefWatcherBuilder listenerServiceClass( - @NonNull Class listenerServiceClass) { - return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass)); -} -``` - -在这里,传入了一个DisplayLeakService的Class对象,它的作用是展示泄露分析的结果日志,然后会展示一个用于跳转到显示泄露界面DisplayLeakActivity的通知。在listenerServiceClass()这个方法中新建了一个ServiceHeapDumpListener对象,下面看看它内部的操作。 - -### ServiceHeapDumpListener - -``` -public final class ServiceHeapDumpListener implements HeapDump.Listener { - - ... - - public ServiceHeapDumpListener(@NonNull final Context context, - @NonNull final Class listenerServiceClass) { - this.listenerServiceClass = checkNotNull(listenerServiceClass, "listenerServiceClass"); - this.context = checkNotNull(context, "context").getApplicationContext(); - } - - ... -} -``` - -可以看到这里仅仅是在ServiceHeapDumpListener中保存了DisplayLeakService的Class对象和application对象。它的作用就是接收一个heap dump去分析。 - -然后我们继续看install()方法链式调用.excludedRefs(AndroidExcludedRefs.createAppDefaults().build())的这部分代码。先看AndroidExcludedRefs.createAppDefaults()。 - -### AndroidExcludedRefs#createAppDefaults() - -``` -public enum AndroidExcludedRefs { - - ... - - public static @NonNull ExcludedRefs.Builder createAppDefaults() { - return createBuilder(EnumSet.allOf(AndroidExcludedRefs.class)); - } - - public static @NonNull ExcludedRefs.Builder createBuilder(EnumSet refs) { - ExcludedRefs.Builder excluded = ExcludedRefs.builder(); - for (AndroidExcludedRefs ref : refs) { - if (ref.applies) { - ref.add(excluded); - ((ExcludedRefs.BuilderWithParams) excluded).named(ref.name()); - } - } - return excluded; - } - - ... -} -``` - -先来说下**AndroidExcludedRefs**这个类,它是一个enum类,它**声明了Android SDK和厂商定制的SDK中存在的内存泄露的case**,根据AndroidExcludedRefs这个类的类名就可看出这些case**都会被Leakcanary的监测过滤掉**。目前这个版本是有**46种**这样的**case**被包含在内,后续可能会一直增加。然后EnumSet.allOf(AndroidExcludedRefs.class)这个方法将会返回一个包含AndroidExcludedRefs元素类型的EnumSet。Enum是一个抽象类,在这里具体的实现类是**通用正规型的RegularEnumSet,如果Enum里面的元素个数大于64,则会使用存储大数据量的JumboEnumSet**。最后,在createBuilder这个方法里面构建了一个排除引用的建造器excluded,将各式各样的case分门别类地保存起来再返回出去。 - -最后看到链式调用的最后一步buildAndInstall()。 - -### AndroidRefWatcherBuilder#buildAndInstall() - -``` -private boolean watchActivities = true; -private boolean watchFragments = true; - -public @NonNull RefWatcher buildAndInstall() { - // 1 - if (LeakCanaryInternals.installedRefWatcher != null) { - throw new UnsupportedOperationException("buildAndInstall() should only be called once."); - } - - // 2 - RefWatcher refWatcher = build(); - if (refWatcher != DISABLED) { - // 3 - LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true); - if (watchActivities) { - // 4 - ActivityRefWatcher.install(context, refWatcher); - } - if (watchFragments) { - // 5 - FragmentRefWatcher.Helper.install(context, refWatcher); - } - } - // 6 - LeakCanaryInternals.installedRefWatcher = refWatcher; - return refWatcher; -} -``` - -首先,在注释1处,会判断LeakCanaryInternals.installedRefWatcher是否已经被赋值,如果被赋值了,则会抛出异常,警告 buildAndInstall()这个方法应该仅仅只调用一次,在此方法结束时,即在注释6处,该LeakCanaryInternals.installedRefWatcher才会被赋值。再来看注释2处,调用了AndroidRefWatcherBuilder其基类RefWatcherBuilder的build()方法,看看它是如何建造的。 - -### RefWatcherBuilder#build() - -``` -public final RefWatcher build() { - if (isDisabled()) { - return RefWatcher.DISABLED; - } - - if (heapDumpBuilder.excludedRefs == null) { - heapDumpBuilder.excludedRefs(defaultExcludedRefs()); - } - - HeapDump.Listener heapDumpListener = this.heapDumpListener; - if (heapDumpListener == null) { - heapDumpListener = defaultHeapDumpListener(); - } - - DebuggerControl debuggerControl = this.debuggerControl; - if (debuggerControl == null) { - debuggerControl = defaultDebuggerControl(); - } - - HeapDumper heapDumper = this.heapDumper; - if (heapDumper == null) { - heapDumper = defaultHeapDumper(); - } - - WatchExecutor watchExecutor = this.watchExecutor; - if (watchExecutor == null) { - watchExecutor = defaultWatchExecutor(); - } - - GcTrigger gcTrigger = this.gcTrigger; - if (gcTrigger == null) { - gcTrigger = defaultGcTrigger(); - } - - if (heapDumpBuilder.reachabilityInspectorClasses == null) { - heapDumpBuilder.reachabilityInspectorClasses(defa ultReachabilityInspectorClasses()); - } - - return new RefWatcher(watchExecutor, debuggerControl, gcTrigger, heapDumper, heapDumpListener, - heapDumpBuilder); -} -``` - -可以看到,**RefWatcherBuilder包含了以下7个组成部分:** - -1. **excludedRefs : 记录可以被忽略的泄漏路径**。 - -2. **heapDumpListener : 转储堆信息到hprof文件,并在解析完 hprof 文件后进行回调,最后通知 DisplayLeakService 弹出泄漏提醒**。 - -3. debuggerControl : 判断是否处于调试模式,调试模式中不会进行内存泄漏检测。为什么呢?因为**在调试过程中可能会保留上一个引用从而导致错误信息上报**。 - -4. **heapDumper : 堆信息转储者,负责dump 内存泄漏处的 heap 信息到 hprof 文件**。 - -5. **watchExecutor : 线程控制器,在 onDestroy() 之后并且在主线程空闲时执行内存泄漏检测**。 - -6. **gcTrigger : 用于 GC,watchExecutor 首次检测到可能的内存泄漏,会主动进行 GC,GC 之后会再检测一次,仍然泄漏的判定为内存泄漏,最后根据heapDump信息生成相应的泄漏引用链**。 - -7. **reachabilityInspectorClasses : 用于要进行可达性检测的类列表。** - -最后,会使用建造者模式将这些组成部分构建成一个新的RefWatcher并将其返回。 - -继续看回到AndroidRefWatcherBuilder的注释3处的 LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true)这行代码。 - -### LeakCanaryInternals#setEnabledAsync() - -``` -public static void setEnabledAsync(Context context, final Class componentClass, -final boolean enabled) { - final Context appContext = context.getApplicationContext(); - AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() { - @Override public void run() { - setEnabledBlocking(appContext, componentClass, enabled); - } - }); -} -``` - -在这里直接使用了**AsyncTask内部自带的THREAD_POOL_EXECUTOR线程池**进行阻塞式地显示DisplayLeakActivity。 - -然后再继续看AndroidRefWatcherBuilder的注释4处的代码。 - -### ActivityRefWatcher#install() - -``` -public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) { - Application application = (Application) context.getApplicationContext(); - // 1 - ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher); - - // 2 - application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks); -} -``` - -可以看到,在注释1处创建一个自己的activityRefWatcher实例,并在注释2处调用了application的registerActivityLifecycleCallbacks()方法,这样就能够监听activity对应的生命周期事件了。继续看看activityRefWatcher.lifecycleCallbacks里面的操作。 - -``` -private final Application.ActivityLifecycleCallbacks lifecycleCallbacks = - new ActivityLifecycleCallbacksAdapter() { - @Override public void onActivityDestroyed(Activity activity) { - refWatcher.watch(activity); - } -}; - -public abstract class ActivityLifecycleCallbacksAdapter -implements Application.ActivityLifecycleCallbacks { - -} -``` - -很明显,这里**实现并重写了Application的ActivityLifecycleCallbacks的onActivityDestroyed()方法,这样便能在所有Activity执行完onDestroyed()方法之后调用 refWatcher.watch(activity)这行代码进行内存泄漏的检测了**。 - -再看到注释5处的FragmentRefWatcher.Helper.install(context, refWatcher)这行代码, - -### FragmentRefWatcher.Helper#install() - -``` -public interface FragmentRefWatcher { - - void watchFragments(Activity activity); - - final class Helper { - - private static final String SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME = - "com.squareup.leakcanary.internal.SupportFragmentRefWatcher"; - - public static void install(Context context, RefWatcher refWatcher) { - List fragmentRefWatchers = new ArrayList<>(); - - // 1 - if (SDK_INT >= O) { - fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher)); - } - - // 2 - try { - Class fragmentRefWatcherClass = Class.forName(SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME); - Constructor constructor = - fragmentRefWatcherClass.getDeclaredConstructor(RefWatcher.class); - FragmentRefWatcher supportFragmentRefWatcher = - (FragmentRefWatcher) constructor.newInstance(refWatcher); - fragmentRefWatchers.add(supportFragmentRefWatcher); - } catch (Exception ignored) { - } - - if (fragmentRefWatchers.size() == 0) { - return; - } - - Helper helper = new Helper(fragmentRefWatchers); - - // 3 - Application application = (Application) context.getApplicationContext(); - application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks); - } - - ... -} -``` - -这里面的逻辑很简单,首先在注释1处将Android标准的Fragment的RefWatcher类,即AndroidOfFragmentRefWatcher添加到新创建的fragmentRefWatchers中。在注释2处**使用反射将leakcanary-support-fragment包下面的SupportFragmentRefWatcher添加进来,如果你在app的build.gradle下没有添加下面这行引用的话,则会拿不到此类,即LeakCanary只会检测Activity和标准Fragment这两种情况**。 - -``` -debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.2' -``` - -继续看到注释3处helper.activityLifecycleCallbacks里面的代码。 - -``` -private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = - new ActivityLifecycleCallbacksAdapter() { - @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - for (FragmentRefWatcher watcher : fragmentRefWatchers) { - watcher.watchFragments(activity); - } - } -}; -``` - -可以看到,在Activity执行完onActivityCreated()方法之后,会调用指定watcher的watchFragments()方法,注意,这里的watcher可能有两种,但不管是哪一种,都会使用当前传入的activity获取到对应的FragmentManager/SupportFragmentManager对象,调用它的registerFragmentLifecycleCallbacks()方法,在对应的onDestroyView()和onDestoryed()方法执行完后,分别使用refWatcher.watch(view)和refWatcher.watch(fragment)进行内存泄漏的检测,代码如下所示。 - -``` -@Override public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) { - View view = fragment.getView(); - if (view != null) { - refWatcher.watch(view); - } -} - -@Override -public void onFragmentDestroyed(FragmentManagerfm, Fragment fragment) { - refWatcher.watch(fragment); -} -``` - -注意,下面到真正关键的地方了,接下来分析refWatcher.watch()这行代码。 - -### RefWatcher#watch() - -``` -public void watch(Object watchedReference, String referenceName) { - if (this == DISABLED) { - return; - } - checkNotNull(watchedReference, "watchedReference"); - checkNotNull(referenceName, "referenceName"); - final long watchStartNanoTime = System.nanoTime(); - // 1 - String key = UUID.randomUUID().toString(); - // 2 - retainedKeys.add(key); - // 3 - final KeyedWeakReference reference = - new KeyedWeakReference(watchedReference, key, referenceName, queue); - - // 4 - ensureGoneAsync(watchStartNanoTime, reference); -} -``` - -注意到在注释1处**使用随机的UUID保证了每个检测对象对应 key 的唯一性**。在注释2处将生成的key添加到类型为CopyOnWriteArraySet的Set集合中。在注释3处新建了一个自定义的弱引用KeyedWeakReference,看看它内部的实现。 - -### KeyedWeakReference - -``` -final class KeyedWeakReference extends WeakReference { - public final String key; - public final String name; - - KeyedWeakReference(Object referent, String key, String name, - ReferenceQueue referenceQueue) { - // 1 - super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue")); - this.key = checkNotNull(key, "key"); - this.name = checkNotNull(name, "name"); - } -} -``` - -可以看到,**在KeyedWeakReference内部,使用了key和name标识了一个被检测的WeakReference对象**。在注释1处,**将弱引用和引用队列 ReferenceQueue 关联起来,如果弱引用reference持有的对象被GC回收,JVM就会把这个弱引用加入到与之关联的引用队列referenceQueue中。即 KeyedWeakReference 持有的 Activity 对象如果被GC回收,该对象就会加入到引用队列 referenceQueue 中**。 - -接着回到RefWatcher.watch()里注释4处的ensureGoneAsync()方法。 - -### RefWatcher#ensureGoneAsync() - -``` -private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) { - // 1 - watchExecutor.execute(new Retryable() { - @Override public Retryable.Result run() { - // 2 - return ensureGone(reference watchStartNanoTime); - } - }); -} -``` - -在ensureGoneAsync()方法中,在注释1处使用 watchExecutor 执行了注释2处的 ensureGone 方法,watchExecutor 是 AndroidWatchExecutor 的实例。 - -下面看看watchExecutor内部的逻辑。 - -### AndroidWatchExecutor - -``` -public final class AndroidWatchExecutor implements WatchExecutor { - - ... - - public AndroidWatchExecutor(long initialDelayMillis) { - mainHandler = new Handler(Looper.getMainLooper()); - HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME); - handlerThread.start(); - // 1 - backgroundHandler = new Handler(handlerThread.getLooper()); - this.initialDelayMillis = initialDelayMillis; - maxBackoffFactor = Long.MAX_VALUE / initialDelayMillis; - } - - @Override public void execute(@NonNull Retryable retryable) { - // 2 - if (Looper.getMainLooper().getThread() == Thread.currentThread()) { - waitForIdle(retryable, 0); - } else { - postWaitForIdle(retryable, 0); - } - } - - ... -} -``` - -在注释1处**AndroidWatchExecutor的构造方法**中,注意到这里**使用HandlerThread的looper新建了一个backgroundHandler**,后面会用到。在注释2处,会判断当前线程是否是主线程,如果是,则直接调用waitForIdle()方法,如果不是,则调用postWaitForIdle(),来看看这个方法。 - -``` -private void postWaitForIdle(final Retryable retryable, final int failedAttempts) { - mainHandler.post(new Runnable() { - @Override public void run() { - waitForIdle(retryable, failedAttempts); - } - }); -} -``` - -很清晰,这里使用了在构造方法中用主线程looper构造的mainHandler进行post,那么waitForIdle()最终也会在主线程执行。接着看看waitForIdle()的实现。 - -``` -private void waitForIdle(final Retryable retryable, final int failedAttempts) { - Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { - @Override public boolean queueIdle() { - postToBackgroundWithDelay(retryable, failedAttempts); - return false; - } - }); -} -``` - -这里**MessageQueue.IdleHandler()回调方法的作用是当 looper 空闲的时候,会回调 queueIdle 方法,利用这个机制我们可以实现第三方库的延迟初始化**,然后执行内部的postToBackgroundWithDelay()方法。接下来看看它的实现。 - -``` -private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) { - long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor); - // 1 - long delayMillis = initialDelayMillis * exponentialBackoffFactor; - // 2 - backgroundHandler.postDelayed(new Runnable() { - @Override public void run() { - // 3 - Retryable.Result result = retryable.run(); - // 4 - if (result == RETRY) { - postWaitForIdle(retryable, failedAttempts + 1); - } - } - }, delayMillis); -} -``` - -先看到注释4处,可以明白,postToBackgroundWithDelay()是一个递归方法,如果result 一直等于RETRY的话,则会一直执行postWaitForIdle()方法。在回到注释1处,这里initialDelayMillis 的默认值是 5s,因此delayMillis就是5s。在注释2处,使用了在构造方法中用HandlerThread的looper新建的backgroundHandler进行异步延时执行retryable的run()方法。这个run()方法里执行的就是RefWatcher的ensureGoneAsync()方法中注释2处的ensureGone()这行代码,继续看它内部的逻辑。 - -### RefWatcher#ensureGone() - -``` -Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) { - long gcStartNanoTime = System.nanoTime(); - long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime); - - // 1 - removeWeaklyReachableReferences(); - - // 2 - if (debuggerControl.isDebuggerAttached()) { - // The debugger can create false leaks. - return RETRY; - } - - // 3 - if (gone(reference)) { - return DONE; - } - - // 4 - gcTrigger.runGc(); - removeWeaklyReachableReferences(); - - // 5 - if (!gone(reference)) { - long startDumpHeap = System.nanoTime(); - long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime); - - File heapDumpFile = heapDumper.dumpHeap(); - if (heapDumpFile == RETRY_LATER) { - // Could not dump the heap. - return RETRY; - } - - long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap); - - HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key) - .referenceName(reference.name) - .watchDurationMs(watchDurationMs) - .gcDurationMs(gcDurationMs) - .heapDumpDurationMs(heapDumpDurationMs) - .build(); - - heapdumpListener.analyze(heapDump); - } - return DONE; -} -``` - -在注释1处,执行了removeWeaklyReachableReferences()这个方法,接下来分析下它的含义。 - -``` -private void removeWeaklyReachableReferences() { - KeyedWeakReference ref; - while ((ref = (KeyedWeakReference) queue.poll()) != null) { - retainedKeys.remove(ref.key); - } -} -``` - -这里使用了while循环遍历 ReferenceQueue ,并从 retainedKeys中移除对应的Reference。 - -再看到注释2处,**当Android设备处于debug状态时,会直接返回RETRY进行延时重试检测的操作**。在注释3处,看看gone(reference)这个方法的逻辑。 - -``` -private boolean gone(KeyedWeakReference reference) { - return !retainedKeys.contains(reference.key); -} -``` - -这里会**判断 retainedKeys 集合中是否还含有 reference,若没有,证明已经被回收了,若含有,可能已经发生内存泄露(或Gc还没有执行回收)**。前面的分析中我们知道了 **reference 被回收的时候,会被加进 referenceQueue 里面,然后我们会调用removeWeaklyReachableReferences()遍历 referenceQueue 移除掉 retainedKeys 里面的 refrence**。 - -接着看到注释4处,执行了gcTrigger的runGc()方法进行垃圾回收,然后使用了removeWeaklyReachableReferences()方法移除已经被回收的引用。这里再深入地分析下runGc()的实现。 - -``` -GcTrigger DEFAULT = new GcTrigger() { - @Override public void runGc() { - // Code taken from AOSP FinalizationTest: - // https://android.googlesource.com/platform/libc ore/+/master/support/src/test/java/libcore/ - // java/lang/ref/FinalizationTester.java - // System.gc() does not garbage collect every time. Runtime.gc() is - // more likely to perform a gc. - Runtime.getRuntime().gc(); - enqueueReferences(); - System.runFinalization(); - } - - private void enqueueReferences() { - // Hack. We don't have a programmatic way to wait for the reference queue daemon to move - // references to the appropriate queues. - try { - Thread.sleep(100); - } catch (InterruptedException e) { - throw new AssertionError(); - } - } -}; -``` - -这里并没有使用System.gc()方法进行回收,因为**system.gc()并不会每次都执行**。而是**从AOSP中拷贝一段GC回收的代码,从而相比System.gc()更能够保证垃圾回收的工作**。 - -最后分析下注释5处的代码处理。首先会判断activity是否被回收,如果还没有被回收,则证明发生内存泄露,进行if判断里面的操作。在里面先调用堆信息转储者heapDumper的dumpHeap()生成相应的 hprof 文件。这里的heapDumper是一个HeapDumper接口,具体的实现是AndroidHeapDumper。我们分析下AndroidHeapDumper的dumpHeap()方法是如何生成hprof文件的。 - -``` -public File dumpHeap() { - File heapDumpFile = leakDirectoryProvider.newHeapDumpFile(); - - if (heapDumpFile == RETRY_LATER) { - return RETRY_LATER; - } - - ... - - try { - Debug.dumpHprofData(heapDumpFile.getAbsolutePath()); - ... - - return heapDumpFile; - } catch (Exception e) { - ... - // Abort heap dump - return RETRY_LATER; - } -} -``` - -这里的核心操作就是**调用了Android SDK的API Debug.dumpHprofData() 来生成 hprof 文件**。 - -如果这个文件等于RETRY_LATER则表示生成失败,直接返回RETRY进行延时重试检测的操作。如果不等于的话,则表示生成成功,最后会**执行heapdumpListener的analyze()对新创建的HeapDump对象进行泄漏分析**。由前面对AndroidRefWatcherBuilder的listenerServiceClass()的分析可知,heapdumpListener的实现 就是ServiceHeapDumpListener,接着看到ServiceHeapDumpListener的analyze方法。 - -### ServiceHeapDumpListener#analyze() - -``` -@Override public void analyze(@NonNull HeapDump heapDump) { - checkNotNull(heapDump, "heapDump"); - HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass); -} -``` - -可以看到,这里**执行了HeapAnalyzerService的runAnalysis()方法,为了避免降低app进程的性能或占用内存,这里将HeapAnalyzerService设置在了一个独立的进程中**。接着继续分析runAnalysis()方法里面的处理。 - -``` -public final class HeapAnalyzerService extends ForegroundService -implements AnalyzerProgressListener { - - ... - - public static void runAnalysis(Context context, HeapDump heapDump, - Class listenerServiceClass) { - ... - - ContextCompat.startForegroundService(context, intent); - } - - ... - - @Override protected void onHandleIntentInForeground(@Nullable Intent intent) { - ... - - // 1 - HeapAnalyzer heapAnalyzer = - new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses); - - // 2 - AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey, - heapDump.computeRetainedHeapSize); - - // 3 - AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result); - } - ... -} -``` - -这里的HeapAnalyzerService实质是一个类型为IntentService的ForegroundService,执行startForegroundService()之后,会回调onHandleIntentInForeground()方法。注释1处,首先会新建一个**HeapAnalyzer**对象,顾名思义,它就是**根据RefWatcher生成的heap dumps信息来分析被怀疑的泄漏是否是真的**。在注释2处,然后会**调用它的checkForLeak()方法去使用haha库解析 hprof文件**,如下所示: - -``` -public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile, - @NonNull String referenceKey, - boolean computeRetainedSize) { - ... - - try { - listener.onProgressUpdate(READING_HEAP_DUMP_FILE); - // 1 - HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile); - - // 2 - HprofParser parser = new HprofParser(buffer); - listener.onProgressUpdate(PARSING_HEAP_DUMP); - Snapshot snapshot = parser.parse(); - - listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS); - // 3 - deduplicateGcRoots(snapshot); - listener.onProgressUpdate(FINDING_LEAKING_REF); - - // 4 - Instance leakingRef = findLeakingReference(referenceKey, snapshot); - - // 5 - if (leakingRef == null) { - return noLeak(since(analysisStartNanoTime)); - } - - // 6 - return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize); - } catch (Throwable e) { - return failure(e, since(analysisStartNanoTime)); - } -} -``` - -在注释1处,会新建一个**内存映射缓存文件buffer**。在注释2处,会**使用buffer新建一个HprofParser解析器去解析出对应的引用内存快照文件snapshot**。在注释3处,**为了减少在Android 6.0版本中重复GCRoots带来的内存压力的影响,使用deduplicateGcRoots()删除了gcRoots中重复的根对象RootObj**。在注释4处,**调用了findLeakingReference()方法将传入的referenceKey和snapshot对象里面所有类实例的字段值对应的keyCandidate进行比较,如果没有相等的,则表示没有发生内存泄漏**,直接调用注释5处的代码返回一个没有泄漏的分析结果AnalysisResult对象。**如果找到了相等的,则表示发生了内存泄漏**,执行注释6处的代码findLeakTrace()方法返回一个有泄漏分析结果的AnalysisResult对象。 - -最后,来分析下HeapAnalyzerService中注释3处的AbstractAnalysisResultService.sendResultToListener()方法,很明显,这里AbstractAnalysisResultService的实现类就是我们刚开始分析的用于展示泄漏路径信息的DisplayLeakService对象。在里面直接**创建一个由PendingIntent构建的泄漏通知用于供用户点击去展示详细的泄漏界面DisplayLeakActivity**。核心代码如下所示: - -``` -public class DisplayLeakService extends AbstractAnalysisResultService { - - @Override - protected final void onHeapAnalyzed(@NonNull AnalyzedHeap analyzedHeap) { - - ... - - boolean resultSaved = false; - boolean shouldSaveResult = result.leakFound || result.failure != null; - if (shouldSaveResult) { - heapDump = renameHeapdump(heapDump); - // 1 - resultSaved = saveResult(heapDump, result); - } - - if (!shouldSaveResult) { - ... - showNotification(null, contentTitle, contentText); - } else if (resultSaved) { - ... - // 2 - PendingIntent pendingIntent = - DisplayLeakActivity.createPendingIntent(this, heapDump.referenceKey); - - ... - - showNotification(pendingIntent, contentTitle, contentText); - } else { - onAnalysisResultFailure(getString(R.string.leak_canary_could_not_save_text)); - } - - ... -} - -@Override protected final void onAnalysisResultFailure(String failureMessage) { - super.onAnalysisResultFailure(failureMessage); - String failureTitle = getString(R.string.leak_canary_result_failure_title); - showNotification(null, failureTitle, failureMessage); -} -``` - -可以看到,只要当分析的堆信息文件保存成功之后,即在注释1处返回的resultSaved为true时,才会执行注释2处的逻辑,即创建一个供用户点击跳转到DisplayLeakActivity的延时通知。 - -## LeakCanary运作流程 - -![image](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d0363331db824e7ab342e4bd74702a93~tplv-k3u1fbpfcp-zoom-1.image) - - - -> 性能优化一直是Android中进阶和深入的方向之一,而内存泄漏一直是性能优化中比较重要的一部分,Android Studio自身提供了MAT等工具去分析内存泄漏,但是分析起来比较耗时耗力,因而才诞生了LeakCanary,它的使用非常简单,但是经过对它的深入分析之后,才发现,**简单的API后面往往藏着许多复杂的逻辑处理,尝试去领悟它们,你可能会发现不一样的世界**。 - -# ButterKnife - -## 简单示例 - -首先看一下ButterKnife的基本使用,如下所示: - -``` -public class CollectFragment extends BaseRootFragment implements CollectContract.View { - - @BindView(R.id.normal_view) - SmartRefreshLayout mRefreshLayout; - @BindView(R.id.collect_recycler_view) - RecyclerView mRecyclerView; - @BindView(R.id.collect_floating_action_btn) - FloatingActionButton mFloatingActionButton; - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(getLayoutId(), container, false); - unBinder = ButterKnife.bind(this, view); - initView(); - return view; - } - - @OnClick({R.id.collect_floating_action_btn}) - void onClick(View view) { - switch (view.getId()) { - case R.id.collect_floating_action_btn: - mRecyclerView.smoothScrollToPosition(0); - break; - default: - break; - } - } - - - @Override - public void onDestroyView() { - super.onDestroyView(); - if (unBinder != null && unBinder != Unbinder.EMPTY) { - unBinder.unbind(); - unBinder = null; - } - } -``` - -可以看到,我们使用了@BindView()替代了findViewById()方法,然后使用了@OnClick替代了setOnClickListener()方法。ButterKnife的初期版本是通过使用注解+反射这样的运行时解析的方式实现上述功能的,后面,为了改善性能,便使用了**注解+APT编译时解析技术并从中生成配套模板代码的方式**来实现。 - -在开始分析之前,可能有同学对APT不是很了解,这里普及一下,APT是Annotation Processing Tool的缩写,即注解处理工具。它的使用步骤通常为如下三个步骤: - -1. **首先,声明注解的生命周期为CLASS,即@Retention(CLASS)**。 - -2. **然后,通过继承AbstractProcessor自定义一个注解处理器**。 - -3. **最后,在编译的时候,编译器会扫描所有带有你要处理的注解的类,最后再调用AbstractProcessor的process方法,对注解进行处理**。 - -下面,正式来解剖一下ButterKnife的心脏。 - -## 源码分析 - -### 模板代码解析 - -首先,在编写好上述的示例代码之后,调用 gradle build 命令,在app/build/generated/source/apt下将可以找到APT为我们生产的配套模板代码CollectFragment_ViewBinding,如下所示: - -``` -public class CollectFragment_ViewBinding implements Unbinder { - private CollectFragment target; - - private View view2131230812; - - @UiThread - public CollectFragment_ViewBinding(final CollectFragment target, View source) { - this.target = target; - - View view; - // 1 - target.mRefreshLayout = Utils.findRequiredViewAsType(source, R.id.normal_view, "field 'mRefreshLayout'", SmartRefreshLayout.class); - target.mRecyclerView = Utils.findRequiredViewAsType(source, R.id.collect_recycler_view, "field 'mRecyclerView'", RecyclerView.class); - view = Utils.findRequiredView(source, R.id.collect_floating_action_btn, "field 'mFloatingActionButton' and method 'onClick'"); - target.mFloatingActionButton = Utils.castView(view, R.id.collect_floating_action_btn, "field 'mFloatingActionButton'", FloatingActionButton.class); - view2131230812 = view; - // 2 - view.setOnClickListener(new DebouncingOnClickListener() { - @Override - public void doClick(View p0) { - target.onClick(p0); - } - }); - } - - @Override - @CallSuper - public void unbind() { - CollectFragment target = this.target; - if (target == null) throw newIllegalStateException("Bindings already cleared."); - this.target = null; - - target.mRefreshLayout = null; - target.mRecyclerView = null; - target.mFloatingActionButton = null; - - view2131230812.setOnClickListener(null); - view2131230812 = null; - } -} -``` - -生成的配套模板CollectFragment_ViewBinding中,在注释1处,使用了ButterKnife内部的工具类Utils的findRequiredViewAsType()方法来寻找控件。在注释2处,使用了view的setOnClickListener()方法来添加了一个去抖动的DebouncingOnClickListener,这样便可以防止重复点击,在重写的doClick()方法内部,直接调用了CollectFragment的onClick方法。最后,再深入看下Utils的findRequiredViewAsType()方法内部的实现。 - -``` -public static T findRequiredViewAsType(View source, @IdRes int id, String who, - Class cls) { - // 1 - View view = findRequiredView(source, id, who); - // 2 - return castView(view, id, who, cls); -} - -public static View findRequiredView(View source, @IdRes int id, String who) { - View view = source.findViewById(id); - if (view != null) { - return view; - } - - ... -} - -public static T castView(View view, @IdRes int id, String who, Class cls) { - try { - return cls.cast(view); - } catch (ClassCastException e) { - ... - } -} -``` - -在注释1处,**最终也是通过View的findViewById()方法找到相应的控件**,在注释2处,**通过相应Class对象的cast方法强转成对应的控件类型**。 - -### ButterKnife 是怎样实现代码注入的 - -接下来,为了使用这套模板代码,我们必须调用ButterKnife的bind()方法实现代码注入,即自动帮我们执行重复繁琐的findViewById和setOnClicklistener操作。下面我们来分析下bind()方法是如何实现注入的。 - -``` -@NonNull @UiThread -public static Unbinder bind(@NonNull Object target, @NonNull View source) { - return createBinding(target, source); -} -``` - -在bind()方法中调用了createBinding(), - -``` -@NonNull @UiThread -public static Unbinder bind(@NonNull Object target, @NonNull View source) { - Class targetClass = target.getClass(); - // 1 - Constructor constructor = findBindingConstructorForClass(targetClass); - - if (constructor == null) { - return Unbinder.EMPTY; - } - - - try { - // 2 - return constructor.newInstance(target, source); - // 3 - } catch (IllegalAccessException e) { - ... -} -``` - -首先,在注释1处,通过 findBindingConstructorForClass() 方法从 Class 中查找 constructor,这里constructor即上文生成的CollectFragment_ViewBinding类。然后,在注释2处,**利用反射来新建 constructor 对象**。最后,如果新建 constructor 对象失败,则会在注释3后面捕获一系列对应的异常进行自定义异常抛出处理。 - -下面,来详细分析下 findBindingConstructorForClass() 方法的实现逻辑。 - -``` -@VisibleForTesting -static final Map, Constructor> BINDINGS = new LinkedHashMap<>(); - -@Nullable @CheckResult @UiThread -private static Constructor findBindingConstructorForClass(Class cls) { - // 1 - Constructor bindingCtor = BINDINGS.get(cls); - if (bindingCtor != null || BINDINGS.containsKey(cls)) { - return bindingCtor; - } - - // 2 - String clsName = cls.getName(); - if (clsName.startsWith("android.") || clsName.startsWith("java.") - || clsName.startsWith("androidx.")) { - return null; - } - - try { - // 3 - Class bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding"); - bindingCtor = (Constructor) bindingClass.getConstructor(cls, View.class); - } catch (ClassNotFoundException e) { - // 4 - bindingCtor = findBindingConstructorForClass(cls.getSuperclass()); - } catch (NoSuchMethodException e) { - throw new RuntimeException("Unable to find binding constructor for " + clsName, e); - } - - // 5 - BINDINGS.put(cls, bindingCtor); - return bindingCtor; -} -``` - -这里,我把多余的log代码删除并把代码格式优化了一下,可以看到,findBindingConstructorForClass() 这个方法中的逻辑瞬间清晰不少,这里**建议以后大家自己在分析源码的时候可以进行这样的优化重整**,会带来不少好处。 - -重新看到 findBindingConstructorForClass() 方法,在注释1处,我们首先从缓存BINDINGS中获取CollectFragment类对象对应的模块类CollectFragment_ViewBinding的构造器对象,这里的BINDINGS是一个LinkedHashMap对象,它保存了上述两者的映射关系。在注释2处,如果是 android,androidx,java 原生的文件,不进行处理。在注释3处,先**通过CollectFragment类对象的类加载器加载出对应的模块类CollectFragment_ViewBinding的类对象**,再通过自身的getConstructor()方法获得相应的构造对象。如果在步骤3中加载不出对应的模板类对象,则会在注释4处使用类似递归的方法重新执行findBindingConstructorForClass()方法。最后,如果找到了bindingCtor模板构造对象,则将它保存在BINDINGS这个LinkedHashMap对象中。 - -**这里总结一下findBindingConstructorForClass()方法的处理:** - -1. **首先从缓存BINDINGS中获取CollectFragment类对象对应的模块类CollectFragment_ViewBinding的构造器对象,获取不到,则继续执行下面的操作**。 - -2. **如果不是android,androidx,java 原生的文件,再进行后面的处理**。 - -3. **通过CollectFragment类对象的类加载器加载出对应的模块类CollectFragment_ViewBinding的类对象,再通过自身的getConstructor()方法获得相应的构造对象,如果获取不到,会抛出异常,在异常的处理中,我们会从当前 class 文件的父类中再去查找。如果找到了,最后会将bindingCtor对象缓存进在BINDINGS对象中**。 - -### ButterKnife是如何在编译时生成代码的? - -在编译的时候,ButterKnife会通过自定义的注解处理器ButterKnifeProcessor的process方法,对编译器扫描到的要处理的类中的注解进行处理,然后,**通过javapoet这个库来动态生成绑定事件或者控件的模板代码**,最后在运行的时候,直接调用bind方法完成绑定即可。 - -首先,先来分析下ButterKnifeProcessor的重写的入口方法init()。 - -``` -@Override public synchronized void init(ProcessingEnvironment env) { - super.init(env); - - String sdk = env.getOptions().get(OPTION_SDK_INT); - if (sdk != null) { - try { - this.sdk = Integer.parseInt(sdk); - } catch (NumberFormatException e) { - ... - } - } - - typeUtils = env.getTypeUtils(); - filer = env.getFiler(); - ... -} -``` - -可以看到,**ProcessingEnviroment对象提供了两大工具类 typeUtils和filer。typeUtils的作用是用来处理TypeMirror,而Filer则是用来创建生成辅助文件**。 - -接着,再来看看被重写的getSupportedAnnotationTypes()方法,这个方法的作用主要是用于指定ButterknifeProcessor注册了哪些注解的。 - -``` -@Override public Set getSupportedAnnotationTypes() { - Set types = new LinkedHashSet<>(); - for (Class annotation : getSupportedAnnotations()) { - types.add(annotation.getCanonicalName()); - } - return types; -} -``` - -这里面首先创建了一个LinkedHashSet对象,然后将getSupportedAnnotations()方法返回的支持注解集合进行遍历一一并添加到types中返回。 - -接着看下getSupportedAnnotations()方法, - -``` -private Set> getSupportedAnnotations() { - Set> annotations = new LinkedHashSet<>(); - - annotations.add(BindAnim.class); - annotations.add(BindArray.class); - annotations.add(BindBitmap.class); - annotations.add(BindBool.class); - annotations.add(BindColor.class); - annotations.add(BindDimen.class); - annotations.add(BindDrawable.class); - annotations.add(BindFloat.class); - annotations.add(BindFont.class); - annotations.add(BindInt.class); - annotations.add(BindString.class); - annotations.add(BindView.class); - annotations.add(BindViews.class); - annotations.addAll(LISTENERS); - - return annotations; -} -``` - -可以看到,这里注册了一系列的Bindxxx注解类和监听列表LISTENERS,接着看一下LISTENERS中包含的监听方法: - -``` -private static final List> LISTENERS = Arrays.asList( - OnCheckedChanged.class, - OnClick.class, - OnEditorAction.class, - OnFocusChange.class, - OnItemClick.class, - OnItemLongClick.class, - OnItemSelected.class, - OnLongClick.class, - OnPageChange.class, - OnTextChanged.class, - OnTouch.class -); -``` - -最后,来分析下整个ButterKnifeProcessor中最关键的方法process()。 - -``` -@Override public boolean process(Set elements, RoundEnvironment env) { - // 1 - Map bindingMap = findAndParseTargets(env); - - for (Map.Entry entry : bindingMap.entrySet()) { - TypeElement typeElement = entry.getKey(); - BindingSet binding = entry.getValue(); - - // 2 - JavaFile javaFile = binding.brewJava(sdk, debuggable); - try { - javaFile.writeTo(filer); - } catch (IOException e) { - ... - } - } - - return false; -} -``` - -首先,在注释1处通过**findAndParseTargets()方法**,知名见义,它应该就是**找到并解析注解目标的关键方法**了,继续看看它内部的处理: - -``` -private Map findAndParseTargets(RoundEnvironment env) { - Map builderMap = new LinkedHashMap<>(); - Set erasedTargetNames = new LinkedHashSet<>(); - - // 1、一系列处理每一个@Bindxxx元素的for循环代码块 - ... - - // Process each @BindView element. - for (Element element : env.getElementsAnnotatedWith(BindView.class)) { - try { - // 2 - parseBindView(element, builderMap, erasedTargetNames); - } catch (Exception e) { - logParsingError(element, BindView.class, e); - } - } - - // Process each @BindViews element. - ... - - // Process each annotation that corresponds to a listener. - for (Class listener : LISTENERS) { - findAndParseListener(env, listener, builderMap, erasedTargetNames); - } - - // 2 - Deque> entries = - new ArrayDeque<>(builderMap.entrySet()); - Map bindingMap = new LinkedHashMap<>(); - while (!entries.isEmpty()) { - Map.Entry entry = entries.removeFirst(); - - TypeElement type = entry.getKey(); - BindingSet.Builder builder = entry.getValue(); - - TypeElement parentType = findParentType(type, erasedTargetNames); - if (parentType == null) { - bindingMap.put(type, builder.build()); - } else { - BindingSet parentBinding = bindingMap.get(parentType); - if (parentBinding != null) { - builder.setParent(parentBinding); - bindingMap.put(type, builder.build()); - } else { - entries.addLast(entry); - } - } - } - return bindingMap; -} -``` - -findAndParseTargets()方法的代码非常多,这里尽可能做了精简。首先,在注释1处,**扫描并处理所有具有@Bindxxx注解和符合LISTENERS监听方法集合的代码,然后在每一个@Bindxxx对应的for循环代码中的parseBindxxx()或findAndParseListener()方法中将解析出的信息放入builderMap这个LinkedHashMap对象中**,其中builderMap是一个key为TypeElement,value为BindingSet.Builder的映射集合,这个 BindSet 是指**的一个类型请求的所有绑定的集合**。在注释3处,首先使用上面的builderMap对象去构建了一个entries对象,它是一个双向队列,能实现两端存取的操作。接着,又新建了一个key为TypeElement,value为BindingSet的LinkedHashMap对象,最后使用了一个while循环从entries的第一个元素开始,这里会判断当前元素类型是否有父类,如果没有,直接构建builder放入bindingMap中,如果有,则将parentBinding添加到BindingSet.Builder这个建造者对象中,然后再创建BindingSet再添加到bindingMap中。 - -接着,分析下注释2处parseBindView是如何对每一个@BindView注解的元素进行处理。 - -``` -private void parseBindView(Element element, Map builderMap, - Set erasedTargetNames) { - TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); - - // 1、首先验证生成的常见代码限制 - ... - - // 2、验证目标类型是否继承自View。 - ... - - // 3 - int id = element.getAnnotation(BindView.class).value(); - BindingSet.Builder builder = builderMap.get(enclosingElement); - Id resourceId = elementToId(element, BindView.class, id); - if (builder != null) { - String existingBindingName = builder.findExistingBindingName(resourceId); - if (existingBindingName != null) { - ... - return; - } - } else { - // 4 - builder = getOrCreateBindingBuilder(builderMap, enclosingElement); - } - - String name = simpleName.toString(); - TypeName type = TypeName.get(elementType); - boolean required = isFieldRequired(element); - - // 5 - builder.addField(resourceId, new FieldViewBinding(name, type, required)); - - // Add the type-erased version to the valid binding targets set. - erasedTargetNames.add(enclosingElement); -} -``` - -首先,在注释1、2处均是一些验证处理操作,如果不符合则会return。然后,看到注释3处,这里获取了BindView要绑定的View的id,然后先从builderMap中获取BindingSet.Builder对象,如果存在,直接return。如果不存在,则会在注释4处的 getOrCreateBindingBuilder()方法生成一个。看一下getOrCreateBindingBuilder()方法: - -``` -private BindingSet.Builder getOrCreateBindingBuilder( - Map builderMap, TypeElement enclosingElement) { - BindingSet.Builder builder = builderMap.get(enclosingElement); - if (builder == null) { - builder = BindingSet.newBuilder(enclosingElement); - builderMap.put(enclosingElement, builder); - } - return builder; -} -``` - -可以看到,这里会再次从buildMap中获取BindingSet.Builder对象,如果没有则直接调用BindingSet的newBuilder()方法新建一个BindingSet.Builder对象并保存在builderMap中,然后,再将新建的builder对象返回。 - -回到parseBindView()方法的注释5处,这里根据view的信息生成一个FieldViewBinding,最后添加到上边生成的builder对象中。 - -最后,再回到我们的process()方法中,现在**所有的绑定的集合数据都放在了bindingMap对象中,这里使用for循环取出每一个BindingSet对象,调用它的brewJava()方法**,看看它内部的处理: - -``` -JavaFile brewJava(int sdk, boolean debuggable) { - TypeSpec bindingConfiguration = createType(sdk, debuggable); - return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration) - .addFileComment("Generated code from Butter Knife. Do not modify!") - .build(); -} - -private TypeSpec createType(int sdk, boolean debuggable) { - TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName()) - .addModifiers(PUBLIC); - if (isFinal) { - result.addModifiers(FINAL); - } - - if (parentBinding != null) { - result.superclass(parentBinding.bindingClassName); - } else { - result.addSuperinterface(UNBINDER); - } - - if (hasTargetField()) { - result.addField(targetTypeName, "target", PRIVATE); - } - - if (isView) { - result.addMethod(createBindingConstructorForView()); - } else if (isActivity) { - result.addMethod(createBindingConstructorForActivity()); - } else if (isDialog) { - result.addMethod(createBindingConstructorForDialog()); - } - if (!constructorNeedsView()) { - // Add a delegating constructor with a target type + view signature for reflective use. - result.addMethod(createBindingViewDelegateConstructor()); - } - result.addMethod(createBindingConstructor(sdk, debuggable)); - - if (hasViewBindings() || parentBinding == null) { - result.addMethod(createBindingUnbindMethod(result)); - } - - return result.build(); -} -``` - -在createType()方法里面使用了java中的javapoet技术生成了一个bindingConfiguration对象,很显然,它里面**保存了所有的绑定配置信息。然后,通过javapoet的builder构造器将上面得到的bindingConfiguration对象构建生成一个JavaFile对象,最终,通过javaFile.writeTo(filer)生成了java源文件**。 - - - -> 从上面的源码分析来看,ButterKnife的执行流程总体可以分为如下两步: - -1. **在编译的时候扫描注解,并通过自定义的ButterKnifeProcessor做相应的处理解析得到bindingMap对象,最后,调用 javapoet 库生成java模板代码**。 - -2. **当我们调用 ButterKnife的bind()方法的时候,它会根据类的全限定类型,找到相应的模板代码,并在其中完成 findViewById 和 setOnClick ,setOnLongClick 等操作**。 - -# Dagger 2 - -## 预备知识 - -### @Inject - -告诉dagger这个字段或类需要依赖注入,然后在需要依赖的地方使用这个注解,dagger会自动生成这个构造器的实例。 - -#### 获取所需依赖: - -- 全局变量注入 -- 方法注入 - -#### 提供所需实例: - -- 构造器注入(如果有多个构造函数,只能注解一个,否则编译报错) - -### @Module - -类注解,表示此类的方法是提供依赖的,它告诉dagger在哪可以找到依赖。用于不能用@Inject提供依赖的地方,如第三方库提供的类,基本数据类型等不能修改源码的情况。 - -注意:**Dagger2会优先在@Module注解的类上查找依赖,没有的情况才会去查询类的@Inject构造方法** - -### @Singleton - -声明这是一个单例,**在确保只有一个Component并且不再重新build()之后,对象只会被初始化一次,之后的每次都会被注入相同的对象**,它就是一个内置的作用域。 - -对于@Singleton,大家可能会产生一些误解,这里详细阐述下: - -- Singleton容易给人造成一种误解就是用Singleton注解后在整个Java代码中都是单例,但**实际上他和Scope一样,只是在同一个Component是单例**。也就是说,如果重新调用了component的build()方法,即使使用了Singleton注解了,但仍然获取的是不同的对象。 -- 它表明了**@Singleton注解只是声明了这是一个单例,为的只是提高代码可读性,其实真正控制对象生命周期的还是Component**。同理,自定义的@ActivityScope 、@ApplicationScope也仅仅是一个声明的作用,**真正控制对象生命周期的还是Component**。 - -### @Providers - -只在@Module中使用,用于提供构造好的实例。一般与@Singleton搭配,用单例方法的形式对外提供依赖,是一种替代@Inject注解构造方法的方式。 - -注意: - -- 使用了@Providers的方法应使用provide作为前缀,使用了@Module的类应使用Module作为后缀。 -- **如果@Providers方法或@Inject构造方法有参数,要保证它能够被dagger获取到,比如通过其它@Providers方法或者@Inject注解构造器的形式得到**。 - -### @Component - -**@Component作为Dagger2的容器总管,它拥有着@Inject与@Module的所有依赖。同时,它也是一枚注射器,用于获取所需依赖和提供所需依赖的桥梁**。这里的桥梁即指@Inject和@Module(或@Inject构造方法)之间的桥梁。定义时需要列出响应的Module组成,此外,还可以使用dependencies继承父Component。 - -#### Component与Module的区别: - -Component既是注射器也是一个容器总管,而module则是作为容器总管Component的子容器,实质是一个用于提供依赖的模块。 - -### @Scope - -注解作用域,通过自定义注解**限定对象作用范围,增强可读性**。 - -@Scope有两种常用的使用场景: - -- **模拟Singleton代表全局单例,与Component生命周期关联**。 -- **模拟局部单例,如登录到退出登录期间**。 - -### @Qualifier - -限定符,利用它**定义注解类以用于区分类的不同实例**。例如:2个方法返回不同的Person对象,比如说小明和小华,为了区分,使用@Qualifier定义的注解类。 - -### dependencies - -使用它表示ChildComponent依赖于FatherComponent,如下所示: - -``` -@Component(modules = ChildModule.class, dependencies = FatherComponent.class) -public interface ChildComponent { - ... -} -``` - -### @SubComponent - -表示是一个子@Component,它能**将应用的不同部分封装起来,用来替代@Dependencies**。 - -## 简单示例 - -### 首先,创建一个BaseActivityComponent的Subcomponent: - -``` -@Subcomponent(modules = {AndroidInjectionModule.class}) -public interface BaseActivityComponent extends AndroidInjector { - - @Subcomponent.Builder - abstract class BaseBuilder extends AndroidInjector.Builder{ - } -} -``` - -这里必须要注解成@Subcomponent.Builder表示是顶级@Subcomponent的内部类。AndroidInjector.Builder的泛型指定了BaseActivity,即表示每一个继承于BaseActivity的Activity都继承于同一个子组件(BaseActivityComponent)。 - -### 然后,创建一个将会导入Subcomponent的公有Module。 - -``` -// 1 -@Module(subcomponents = {BaseActivityComponent.class}) -public abstract class AbstractAllActivityModule { - - @ContributesAndroidInjector(modules = MainActivityModule.class) - abstract MainActivity contributesMainActivityInjector(); - - @ContributesAndroidInjector(modules = SplashActivityModule.class) - abstract SplashActivity contributesSplashActivityInjector(); - - // 一系列的对应Activity的contributesxxxActivityInjector - ... - -} -``` - -在注释1处用subcomponents来表示开放全部依赖给AbstractAllActivityModule,使用Subcomponent的重要原因是它将应用的不同部分封装起来了。**@AppComponent负责维护共享的数据和对象,而不同处则由各自的@Subcomponent维护**。 - -### 接着,配置项目的Application。 - -``` -public class WanAndroidApp extends Application implements HasActivityInjector { - - // 3 - @Inject - DispatchingAndroidInjector mAndroidInjector; - - private static volatile AppComponent appComponent; - - @Override - public void onCreate() { - super.onCreate(); - - ... - // 1 - appComponent = DaggerAppComponent.builder() - .build(); - // 2 - appComponent.inject(this); - - ... - - } - - ... - - // 4 - @Override - public AndroidInjector activityInjector() { - return mAndroidInjector; - } -} -``` - -首先,在注释1处,使用AppModule模块和httpModule模块构建出AppComponent的实现类DaggerAppComponent。这里看一下AppComponent的配置代码: - -``` -@Singleton -@Component(modules = {AndroidInjectionModule.class, - AndroidSupportInjectionModule.class, - AbstractAllActivityModule.class, - AbstractAllFragmentModule.class, - AbstractAllDialogFragmentModule.class} - ) -public interface AppComponent { - - /** - * 注入WanAndroidApp实例 - * - * @param wanAndroidApp WanAndroidApp - */ - void inject(WanAndroidApp wanAndroidApp); - - ... - -} -``` - -可以看到,AppComponent依赖了AndroidInjectionModule模块,它包含了一些基础配置的绑定设置,如activityInjectorFactories、fragmentInjectorFactories等等,而AndroidSupportInjectionModule模块显然就是多了一个supportFragmentInjectorFactories的绑定设置,activityInjectorFactories的内容如所示: - -``` -@Beta -@Module -public abstract class AndroidInjectionModule { - @Multibinds - abstract Map, AndroidInjector.Factory> - activityInjectorFactories(); - - @Multibinds - abstract Map, AndroidInjector.Factory> - fragmentInjectorFactories(); - - ... - -} -``` - -接着,下面依赖的AbstractAllActivityModule、 AbstractAllFragmentModule、AbstractAllDialogFragmentModule则是为项目的所有Activity、Fragment、DialogFragment提供的统一基类抽象Module,这里看下AbstractAllActivityModule的配置: - -``` -@Module(subcomponents = {BaseActivityComponent.class}) -public abstract class AbstractAllActivityModule { - - @ContributesAndroidInjector(modules = MainActivityModule.class) - abstract MainActivity contributesMainActivityInjector(); - - @ContributesAndroidInjector(modules = SplashActivityModule.class) - abstract SplashActivity contributesSplashActivityInjector(); - - ... - -} -``` - -可以看到,项目下的所有xxxActiviity都有对应的contributesxxxActivityInjector()方法提供实例注入。并且,注意到AbstractAllActivityModule这个模块依赖的 subcomponents为BaseActivityComponent,前面说过了,每一个继承于BaseActivity的Activity都继承于BaseActivityComponent这一个subcomponents。同理,AbstractAllFragmentModule与AbstractAllDialogFragmentModule也是类似的实现模式,如下所示: - -``` -// 1 -@Module(c = BaseFragmentComponent.class) -public abstract class AbstractAllFragmentModule { - - @ContributesAndroidInjector(modules = CollectFragmentModule.class) - abstract CollectFragment contributesCollectFragmentInject(); - - @ContributesAndroidInjector(modules = KnowledgeFragmentModule.class) - abstract KnowledgeHierarchyFragment contributesKnowledgeHierarchyFragmentInject(); - - ... - -} - - -// 2 -@Module(subcomponents = BaseDialogFragmentComponent.class) -public abstract class AbstractAllDialogFragmentModule { - - @ContributesAndroidInjector(modules = SearchDialogFragmentModule.class) - abstract SearchDialogFragment contributesSearchDialogFragmentInject(); - - @ContributesAndroidInjector(modules = UsageDialogFragmentModule.class) - abstract UsageDialogFragment contributesUsageDialogFragmentInject(); - -} -``` - -注意到注释1和注释2处的代码,AbstractAllFragmentModule和AbstractAllDialogFragmentModule的subcomponents为BaseFragmentComponent、BaseDialogFragmentComponent,很显然,同AbstractAllActivityModule的子组件BaseActivityComponent一样,它们都是作为一个通用的子组件。 - -然后,回到我们配置项目下的Application下面的注释2处的代码,在这里使用了第一步Dagger为我们构建的DaggerAppComponent对象将当期的Application实例注入了进去,交给了Dagger这个依赖大管家去管理。最终,**Dagger2内部创建的mAndroidInjector对象会在注释3处的地方进行实例赋值。在注释4处,实现HasActivityInjector接口,重写activityInjector()方法,将我们上面得到的mAndroidInjector对象返回**。这里的mAndroidInjector是一个类型为DispatchingAndroidInjector的对象,可以这样理解它:它能够执行Android框架下的核心成员如Activity、Fragment的成员注入,在我们项目下的Application中将DispatchingAndroidInjector的泛型指定为Activity就说明它承担起了所有Activity成员依赖的注入。那么,如何指定某一个Activity能被纳入DispatchingAndroidInjector这个所有Activity的依赖总管的口袋中呢?接着看使用步骤4。 - -### 最后,将目标Activity纳入Activity依赖分配总管DispatchingAndroidInjector的囊中。 - -很简单,只需在目标Activity的onCreate()方法前的super.onCreate(savedInstanceState)前配置一行代码 AndroidInjection.inject(this),如下所示: - -``` -public abstract class BaseActivity extends AbstractSimpleActivity implements - AbstractView { - - ... - @Inject - protected T mPresenter; - - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - AndroidInjection.inject(this); - super.onCreate(savedInstanceState); - } - - ... - -} -``` - -这里使用了@Inject表明了需要注入mPresenter实例,然后,我们需要在具体的Presenter类的构造方法上使用@Inject提供基于当前构造方法的mPresenter实例,如下所示: - -``` -public class MainPresenter extends BasePresenter implements MainContract.Presenter { - - ... - - @Inject - MainPresenter(DataManager dataManager) { - super(dataManager); - this.mDataManager = dataManager; - } - - ... - -} -``` - -从上面的使用流程中,有三个关键的核心实现是我们需要了解的,如下所示: - -- 1、appComponent = DaggerAppComponent.builder().build()这句代码如何构建出DaggerAPPComponent的? -- 2、appComponent.inject(this)是如何将mAndroidInjector实例赋值给当前的Application的? -- 3、在目标Activity下的AndroidInjection.inject(this)这句代码是如何将当前Activity对象纳入依赖分配总管DispatchingAndroidInjector囊中的呢? - -下面我们逐个地来探索其中的奥妙~ - -## 源码分析 - -### DaggerAppComponent.builder().build()是如何构建出DaggerAPPComponent的? - -首先,看到DaggerAppComponent的builder()方法: - -``` -public static Builder builder() { - return new Builder(); -} -``` - -里面直接返回了一个新建的Builder静态内部类对象,看看它的构造方法中做了什么: - -``` -public static final class Builder { - - private Builder() {} - - ... - -} -``` - -看来,Builder的默认构造方法什么也没有做,那么,真正的实现肯定在Builder对象的build()方法中,接着看到build()方法。 - -``` -public static final class Builder { - - ... - - public AppComponent build() { - return new DaggerAppComponent(this); - } - - ... - -} -``` - -在Builder的build()方法中直接返回了新建的DaggerAppComponent对象。下面,看看DaggerAppComponent的构造方法: - -``` -private DaggerAppComponent(Builder builder) { - initialize(builder); -} -``` - -在DaggerAppComponent的构造方法中调用了initialize方法,顾名思义,它就是真正初始化项目全局依赖配置的地方了,下面,来看看它内部的实现: - -``` -private void initialize(final Builder builder) { - // 1 - this.mainActivitySubcomponentBuilderProvider = - new Provider< - AbstractAllActivityModule_ContributesMainActivityInjector.MainActivitySubcomponent - .Builder>() { - @Override - public AbstractAllActivityModule_ContributesMainActivityInjector.MainActivitySubcomponent - .Builder - get() { - // 2 - return new MainActivitySubcomponentBuilder(); - } - }; - - // 一系列xxxActivitySubcomponentBuilderProvider的创建赋值代码块 - ... - -} -``` - -在注释1处,新建了一个mainActivit的子组件构造器实例提供者Provider。在注释2处,使用匿名内部类的方式重写了该Provider的get()方法,返回一个新创建好的MainActivitySubcomponentBuilder对象。很显然,它就是负责创建管理MAinActivity中所需依赖的Subcomponent建造者。接下来重点来分析下MainActivitySubcomponentBuilder这个类的作用。 - -``` -// 1 -private final class MainActivitySubcomponentBuilder - extends AbstractAllActivityModule_ContributesMainActivityInjector.MainActivitySubcomponent - .Builder { - private MainActivity seedInstance; - - @Override - public AbstractAllActivityModule_ContributesMainActivityInjector.MainActivitySubcomponent - build() { - if (seedInstance == null) { - throw new IllegalStateException(MainActivity.class.getCanonicalName() + " must be set"); - } - // 2 - return new MainActivitySubcomponentImpl(this); - } - - @Override - public void seedInstance(MainActivity arg0) { - // 3 - this.seedInstance = Preconditions.checkNotNull(arg0); - } -} -``` - -首先,在注释1处,MainActivitySubcomponentBuilder继承了AbstractAllActivityModule_ContributesMainActivityInjector内部的子组件MainActivitySubcomponent的内部的子组件建造者类Builder,如下所示: - -``` -@Subcomponent(modules = MainActivityModule.class) -public interface MainActivitySubcomponent extends AndroidInjector { - @Subcomponent.Builder - abstract class Builder extends - AndroidInjector.Builder {} -} -``` - -可以看到,这个子组件建造者Builder又继承了AndroidInjector的抽象内部类Builder,那么,这个AndroidInjector到底是什么呢? - -顾名思义,**AndroidInjector**是一个Android注射器,它**为每一个具体的子类型,即核心Android类型Activity和Fragment执行成员注入。** - -接下来分析下AndroidInjector的内部实现,源码如下所示: - -``` -public interface AndroidInjector { - - void inject(T instance); - - // 1 - interface Factory { - AndroidInjector create(T instance); - } - - // 2 - abstract class Builder implements AndroidInjector.Factory { - @Override - public final AndroidInjector create(T instance) { - seedInstance(instance); - return build(); - } - - @BindsInstance - public abstract void seedInstance(T instance); - - public abstract AndroidInjector build(); - } -} -``` - -在注释1处,使用了抽象工厂模式,用来创建一个具体的Activity或Fragment类型的AndroidInjector实例。注释2处,Builder实现了AndroidInjector.Factory,它是一种Subcomponent.Builder的通用实现模式,在重写的create()方法中,进行了实例保存seedInstance()和具体Android核心类型的构建。 - -接着,我们回到MainActivitySubcomponentBuilder类,可以看到,它实现了AndroidInjector.Builder的seedInstance()和build()方法。在注释3处首先播种了MainActivity的实例,然后 在注释2处新建了一个MainActivitySubcomponentImpl对象返回。我们看看MainActivitySubcomponentImpl这个类是如何将mPresenter依赖注入的,相关源码如下: - -``` -private final class MainActivitySubcomponentImpl - implements AbstractAllActivityModule_ContributesMainActivityInjector - .MainActivitySubcomponent { - - private MainPresenter getMainPresenter() { - // 2 - return MainPresenter_Factory.newMainPresenter( - DaggerAppComponent.this.provideDataManagerProvider.get()); - } - - @Override - public void inject(MainActivity arg0) { - // 1 - injectMainActivity(arg0); - } - - private MainActivity injectMainActivity(MainActivity instance) { - // 3 - BaseActivity_MembersInjector - .injectMPresenter(instance, getMainPresenter()); - return instance; - } -``` - -在注释1处,MainActivitySubcomponentImpl实现了AndroidInjector接口的inject()方法,**在injectMainActivity()首先调用getMainPresenter()方法从MainPresenter_Factory工厂类中新建了一个MainPresenter对象**。我们看看MainPresenter的newMainPresenter()方法: - -``` -public static MainPresenter newMainPresenter(DataManager dataManager) { - return new MainPresenter(dataManager); -} -``` - -这里直接新建了一个MainPresenter。然后我们回到MainActivitySubcomponentImpl类的注释3处,继续调用了**BaseActivity_MembersInjector的injectMPresenter()方法**,顾名思义,可以猜到,它是BaseActivity的成员注射器,继续看看injectMPresenter()内部: - -``` -public static void injectMPresenter( - BaseActivity instance, T mPresenter) { - instance.mPresenter = mPresenter; -} -``` - -可以看到,这里直接将需要的mPresenter实例赋值给了BaseActivity的mPresenter,当然,这里其实是指的BaseActivity的子类MainActivity,其它的xxxActivity的依赖管理机制都是如此。 - -### appComponent.inject(this)是如何将mAndroidInjector实例赋值给当前的Application的? - -我们继续查看appComponent的inject()方法: - -``` -@Override -public void inject(WanAndroidApp wanAndroidApp) { - injectWanAndroidApp(wanAndroidApp); -} -``` - -在inject()方法里调用了injectWanAndroidApp(),继续查看injectWanAndroidApp()方法: - -``` -private WanAndroidApp injectWanAndroidApp(WanAndroidApp instance) { - WanAndroidApp_MembersInjector.injectMAndroidInjector( - instance, - getDispatchingAndroidInjectorOfActivity()); - return instance; -} -``` - -首先,执行getDispatchingAndroidInjectorOfActivity()方法得到了一个Activity类型的DispatchingAndroidInjector对象,继续查看getDispatchingAndroidInjectorOfActivity()方法: - -``` -private DispatchingAndroidInjector getDispatchingAndroidInjectorOfActivity() { - return DispatchingAndroidInjector_Factory.newDispatchingAndroidInjector( - getMapOfClassOfAndProviderOfFactoryOf()); -} -``` - -在getDispatchingAndroidInjectorOfActivity()方法里面,首先调用了getMapOfClassOfAndProviderOfFactoryOf()方法,我们看到这个方法: - -``` -private Map, Provider>> - getMapOfClassOfAndProviderOfFactoryOf() { - return MapBuilder - ., Provider>> - newMapBuilder(8) - .put(MainActivity.class, (Provider) mainActivitySubcomponentBuilderProvider) - .put(SplashActivity.class, (Provider) splashActivitySubcomponentBuilderProvider) - .put(ArticleDetailActivity.class, - (Provider) articleDetailActivitySubcomponentBuilderProvider) - .put(KnowledgeHierarchyDetailActivity.class, - (Provider) knowledgeHierarchyDetailActivitySubcomponentBuilderProvider) - .put(LoginActivity.class, (Provider) loginActivitySubcomponentBuilderProvider) - .put(RegisterActivity.class, (Provider) registerActivitySubcomponentBuilderProvider) - .put(AboutUsActivity.class, (Provider) aboutUsActivitySubcomponentBuilderProvider) - .put(SearchListActivity.class, (Provider) searchListActivitySubcomponentBuilderProvider) - .build(); -} -``` - -可以看到,这里新建了一个建造者模式实现的MapBuilder,并且同时制定了固定容量为8,将项目下使用了AndroidInjection.inject(mActivity)方法的8个Activity对应的xxxActivitySubcomponentBuilderProvider保存起来。 - -我们再回到getDispatchingAndroidInjectorOfActivity()方法,这里将上面得到的Map容器传入了DispatchingAndroidInjector_Factory的newDispatchingAndroidInjector()方法中,这里应该就是新建DispatchingAndroidInjector的地方了。我们点进去看看: - -``` -public static DispatchingAndroidInjector newDispatchingAndroidInjector( - Map, Provider>> injectorFactories) { - return new DispatchingAndroidInjector(injectorFactories); -} -``` - -在这里,果然新建了一个DispatchingAndroidInjector对象。继续看看DispatchingAndroidInjector的构造方法: - -``` -@Inject -DispatchingAndroidInjector( - Map, Provider>> injectorFactories) { - this.injectorFactories = injectorFactories; -} -``` - -这里仅仅是将传进来的Map容器保存起来了。 - -我们再回到WanAndroidApp_MembersInjector的injectMAndroidInjector()方法,将上面得到的DispatchingAndroidInjector实例传入,继续查看injectMAndroidInjector()这个方法: - -``` -public static void injectMAndroidInjector( - WanAndroidApp instance, DispatchingAndroidInjector mAndroidInjector) { - instance.mAndroidInjector = mAndroidInjector; -} -``` - -可以看到,最后在WanAndroidApp_MembersInjector的injectMAndroidInjector()方法中,直接将新建好的DispatchingAndroidInjector实例赋值给了WanAndroidApp的mAndroidInjector。 - -### 在目标Activity下的AndroidInjection.inject(this)这句代码是如何将当前Activity对象纳入依赖分配总管DispatchingAndroidInjector囊中的呢? - -首先,我们看到AndroidInjection.inject(this)这个方法: - -``` -public static void inject(Activity activity) { - checkNotNull(activity, "activity"); - - // 1 - Application application = activity.getApplication(); - if (!(application instanceof HasActivityInjector)) { - throw new RuntimeException( - String.format( - "%s does not implement %s", - application.getClass().getCanonicalName(), - HasActivityInjector.class.getCanonicalName())); - } - - // 2 - AndroidInjector activityInjector = - ((HasActivityInjector) application).activityInjector(); - - checkNotNull(activityInjector, "%s.activityInjector() returned null", application.getClass()); - - // 3 - activityInjector.inject(activity); -``` - -} - -在注释1处,会先判断当前的application是否实现了HasActivityInjector这个接口,如果没有,则抛出RuntimeException。如果有,会继续在注释2处调用application的activityInjector()方法得到DispatchingAndroidInjector实例。最后,在注释3处,会将当前的activity实例传入activityInjector的inject()方法中。我们继续查看inject()方法: - -``` -@Override -public void inject(T instance) { - boolean wasInjected = maybeInject(instance); - if (!wasInjected) { - throw new IllegalArgumentException(errorMessageSuggestions(instance)); - } -} -``` - -**DispatchingAndroidInjector的inject()方法,它的作用就是给传入的instance实例执行成员注入**。具体在这个案例中,其实就是负责将创建好的Presenter实例赋值给BaseActivity对象 的mPresenter全局变量。在inject()方法中,又调用了maybeInject()方法,我们继续查看它: - -``` -@CanIgnoreReturnValue -public boolean maybeInject(T instance) { - // 1 - Provider> factoryProvider = - injectorFactories.get(instance.getClass()); - if (factoryProvider == null) { - return false; - } - - @SuppressWarnings("unchecked") - // 2 - AndroidInjector.Factory factory = (AndroidInjector.Factory) factoryProvider.get(); - try { - // 3 - AndroidInjector injector = - checkNotNull( - factory.create(instance), "%s.create(I) should not return null.", factory.getClass()); - // 4 - injector.inject(instance); - return true; - } catch (ClassCastException e) { - ... - } -} -``` - -在注释1处,我们从injectorFactories(前面得到的Map容器)中根据当前Activity实例拿到了factoryProvider对象,这里我们具体一点,看到MainActivity对应的factoryProvider,也就是我们研究的第一个问题中的mainActivitySubcomponentBuilderProvider: - -``` -private void initialize(final Builder builder) { - this.mainActivitySubcomponentBuilderProvider = - new Provider< - AbstractAllActivityModule_ContributesMainActivityInjector.MainActivitySubcomponent - .Builder>() { - @Override - public AbstractAllActivityModule_ContributesMainActivityInjector.MainActivitySubcomponent - .Builder - get() { - return new MainActivitySubcomponentBuilder(); - } - }; - - ... - -} -``` - -在maybeInject()方法的注释2处,调用了mainActivitySubcomponentBuilderProvider的get()方法得到了一个新建的MainActivitySubcomponentBuilder对象。在注释3处执行了它的create方法,create()方法的具体实现在AndroidInjector的内部类Builder中: - -``` -abstract class Builder implements AndroidInjector.Factory { - @Override - public final AndroidInjector create(T instance) { - seedInstance(instance); - return build(); - } -``` - -看到这里,我相信看过第一个问题的同学已经明白后面是怎么回事了。在create()方法中,我们首先MainActivitySubcomponentBuilder的seedInstance()将MainActivity实例注入,然后再调用它的build()方法新建了一个MainActivitySubcomponentImpl实例返回。 - -最后,在注释4处,执行了MainActivitySubcomponentImpl的inject()方法: - -``` -private final class MainActivitySubcomponentImpl - implements AbstractAllActivityModule_ContributesMainActivityInjector - .MainActivitySubcomponent { - - private MainPresenter getMainPresenter() { - // 2 - return MainPresenter_Factory.newMainPresenter( - DaggerAppComponent.this.provideDataManagerProvider.get()); - } - - @Override - public void inject(MainActivity arg0) { - // 1 - injectMainActivity(arg0); - } - - private MainActivity injectMainActivity(MainActivity instance) { - // 3 - BaseActivity_MembersInjector - .injectMPresenter(instance, getMainPresenter()); - return instance; - } -``` - -这里的逻辑已经在问题一的最后部分详细讲解了,最后,会在注释3处调用BaseActivity_MembersInjector的injectMPresenter()方法: - -``` -public static void injectMPresenter( - BaseActivity instance, T mPresenter) { - instance.mPresenter = mPresenter; -} -``` - -这样,就将mPresenter对象赋值给了当前Activity对象的mPresenter全局变量中了。至此,Dagger.Android的核心源码分析就结束了。 - - - -> 相比于ButterKnife,Dagger是一个**锋利的全局依赖注入管理框架**,它主要用来**管理对象的依赖关系和生命周期**,当项目越来越大时,类之间的调用层次会越来越深,并且有些类是Activity或Fragment,有些是单例,而且它们的生命周期不一致,所以创建所需对象时需要处理的各个对象的依赖关系和生命周期时的任务会很繁重。因此,使用Dagger会大大减轻这方面的工作量。虽然它的学习成本比较高,而且需要写一定的模板类,但是,**对于越大的项目来说,Dagger越值得被需要**。 - -# EventBus - -## 简单示例 - -### 首先,定义要传递的事件实体 - -``` -public class CollectEvent { ... } -``` - -### 准备订阅者:声明并注解你的订阅方法 - -``` -@Subscribe(threadMode = ThreadMode.MAIN) -public void onMessageEvent(CollectEvent event) { - LogHelper.d("OK"); -} -``` - -### 在2中,也就是订阅中所在的类中,注册和解注册你的订阅者 - -``` -@Override -public void onStart() { - super.onStart(); - EventBus.getDefault().register(this); -} - -@Override -public void onStop() { - super.onStop(); - EventBus.getDefault().unregister(this); -} -``` - -### 发送事件 - -``` -EventBus.getDefault().post(new CollectEvent()); -``` - -在正式讲解之前需要对一些基础性的概念进行详细的讲解。众所周知,EventBus没出现之前,那时候的开发者一般是使用Android四大组件中的广播进行组件间的消息传递,那么我们**为什么要使用事件总线机制来替代广播呢**? - -主要是因为: - -- 广播:耗时、容易被捕获(不安全)。 -- 事件总线:更节省资源、更高效,能将信息传递给原生以外的各种对象。 - -那么,话又说回来了,**事件总线又是什么呢?** - -如下图所示,事件总线机制通过记录对象、使用观察者模式来通知对象各种事件。(当然,你也可以发送基本数据类型如 int,String 等作为一个事件) - -![image](https://user-gold-cdn.xitu.io/2020/3/6/170ada0907b50b75?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -对于**事件总线EventBus**而言,它的**优缺点**又是如何?这里简单总结下: - -- 优点:开销小,代码更优雅、简洁,解耦发送者和接收者,可动态设置事件处理线程和优先级。 -- 缺点:每个事件必须自定义一个事件类,增加了维护成本。 - -EventBus是基于观察者模式扩展而来的,我们先了解一下观察者模式是什么? - -观察者模式又可称为**发布 - 订阅模式**,它定义了对象间的一种1对多的依赖关系,每当这个对象的状态改变时,其它的对象都会接收到通知并被自动更新。 - -观察者模式有以下角色: - -- 抽象被观察者:将所有已注册的观察者对象保存在一个集合中。 -- 具体被观察者:当内部状态发生变化时,将会通知所有已注册的观察者。 -- 抽象观察者:定义了一个更新接口,当被观察者状态改变时更新自己。 -- 具体观察者:实现抽象观察者的更新接口。 - -这里给出一个简单的示例来让大家更深一步理解观察者模式的思想: - -1、首先,创建抽象观察者 - -``` -public interface observer { - - public void update(String message); -} -``` - -2、接着,创建具体观察者 - -``` -public class WeXinUser implements observer { - private String name; - - public WeXinUser(String name) { - this.name = name; - } - - @Override - public void update(String message) { - ... - } -} -``` - -3、然后,创建抽象被观察者 - -``` -public interface observable { - - public void addWeXinUser(WeXinUser weXinUser); - - public void removeWeXinUser(WeXinUser weXinUser); - - public void notify(String message); -} -``` - -4、最后,创建具体被观察者 - -``` -public class Subscription implements observable { - private List mUserList = new ArrayList(); - - @Override - public void addWeXinUser(WeXinUser weXinUser) { - mUserList.add(weXinUser); - } - - @Override - public void removeWeXinUser(WeXinUser weXinUser) { - mUserList.remove(weXinUser); - } - - @Override - public void notify(String message) { - for(WeXinUser weXinUser : mUserList) { - weXinUser.update(message); - } - } -} -``` - -在具体使用时,我们便可以这样使用,如下所示: - -``` -Subscription subscription = new Subscription(); - -WeXinUser hongYang = new WeXinUser("HongYang"); -WeXinUser rengYuGang = new WeXinUser("RengYuGang"); -WeXinUser liuWangShu = new WeXinUser("LiuWangShu"); - -subscription.addWeiXinUser(hongYang); -subscription.addWeiXinUser(rengYuGang); -subscription.addWeiXinUser(liuWangShu); -subscription.notify("New article coming"); -``` - -在这里,hongYang、rengYuGang、liuWangShu等大神都订阅了我的微信公众号,每当我的公众号发表文章时(subscription.notify()),他们就会接收到最新的文章信息(weXinUser.update())。(ps:当然,这一切都是YY~) - -当然,EventBus的观察者模式和一般的观察者模式不同,它使用了**扩展的观察者模式对事件进行订阅和分发,其实这里的扩展就是指的使用了EventBus来作为中介者,抽离了许多职责**,如下是它的官方原理图: - -![image](https://user-gold-cdn.xitu.io/2020/3/6/170ada0907c082ed?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -在得知了EventBus的原理之后,我们注意到,每次我们在register之后,都必须进行一次unregister,这是为什么呢? - -**因为register是强引用,它会让对象无法得到内存回收,导致内存泄露。所以必须在unregister方法中释放对象所占的内存**。 - -有些同学可能之前使用的是EventBus2.x的版本,那么它又与EventBus3.x的版本有哪些区别呢? - -1. EventBus2.x使用的是**运行时注解,它采用了反射的方式对整个注册的类的所有方法进行扫描来完成注册,因而会对性能有一定影响**。 - -2. EventBus3.x使用的是**编译时注解,Java文件会编译成.class文件,再对class文件进行打包等一系列处理。在编译成.class文件时,EventBus会使用EventBusAnnotationProcessor注解处理器读取@Subscribe()注解并解析、处理其中的信息,然后生成Java类来保存所有订阅者的订阅信息。这样就创建出了对文件或类的索引关系,并将其编入到apk中**。 - -3. 从EventBus3.0开始**使用了对象池缓存减少了创建对象的开销**。 - -除了EventBus,其实现在比较流行的事件总线还有RxBus,那么,它与EventBus相比又如何呢? - -1. **RxJava的Observable有onError、onComplete等状态回调**。 - -2. **Rxjava使用组合而非嵌套的方式,避免了回调地狱**。 - -3. **Rxjava的线程调度设计的更加优秀,更简单易用**。 - -4. **Rxjava可使用多种操作符来进行链式调用来实现复杂的逻辑**。 - -5. **Rxjava的信息效率高于EventBus2.x,低于EventBus3.x**。 - -在了解了EventBus和RxBus的区别之后,那么,对待新项目的事件总线选型时,我们该如何考量? - -很简单,**如果项目中使用了RxJava,则使用RxBus,否则使用EventBus3.x**。 - -## 源码分析 - -接下来将按以下顺序来进行EventBus的源码分析: - -1. 订阅者:EventBus.getDefault().register(this); - -2. 发布者:EventBus.getDefault().post(new CollectEvent()); - -3. 订阅者:EventBus.getDefault().unregister(this)。 - -### EventBus.getDefault().register(this) - -首先,从获取EventBus实例的方法getDefault()开始分析: - -``` -public static EventBus getDefault() { - if (defaultInstance == null) { - synchronized (EventBus.class) { - if (defaultInstance == null) { - defaultInstance = new EventBus(); - } - } - } - return defaultInstance; -} -``` - -在getDefault()中使用了双重校验并加锁的单例模式来创建EventBus实例。 - -接着,看到EventBus的默认构造方法中做了什么: - -``` -private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder(); - -public EventBus() { - this(DEFAULT_BUILDER); -} -``` - -在EventBus的默认构造方法中又调用了它的另一个有参构造方法,将一个类型为EventBusBuilder的DEFAULT_BUILDER对象传递进去了。这里的EventBusBuilder很明显是一个EventBus的建造器,以便于EventBus能够添加自定义的参数和安装一个自定义的默认EventBus实例。 - -再看一下EventBusBuilder的构造方法: - -``` -public class EventBusBuilder { - - ... - - EventBusBuilder() { - } - - ... - -} -``` - -EventBusBuilder的构造方法中什么也没有做,那继续查看EventBus的这个有参构造方法: - -``` -private final Map, CopyOnWriteArrayList> subscriptionsByEventType; -private final Map>> typesBySubscriber; -private final Map, Object> stickyEvents; - -EventBus(EventBusBuilder builder) { - ... - - // 1 - subscriptionsByEventType = new HashMap<>(); - - // 2 - typesBySubscriber = new HashMap<>(); - - // 3 - stickyEvents = new ConcurrentHashMap<>(); - - // 4 - mainThreadSupport = builder.getMainThreadSupport(); - mainThreadPoster = mainThreadSupport != null ? mainThreadSupport.createPoster(this) : null; - backgroundPoster = new BackgroundPoster(this); - asyncPoster = new AsyncPoster(this); - - ... - - // 5 - subscriberMethodFinder = new SubscriberMethodFinder(builder.subscriberInfoIndexes, - builder.strictMethodVerification, builder.ignoreGeneratedIndex); - - // 从builder取中一些列订阅相关信息进行赋值 - ... - - // 6 - executorService = builder.executorService; -} -``` - -在注释1处,创建了一个subscriptionsByEventType对象,可以看到它是一个类型为HashMap的subscriptionsByEventType对象,并且其key为 Event 类型,value为 Subscription链表。这里的Subscription是一个订阅信息对象,它里面保存了两个重要的字段,一个是类型为 Object 的 subscriber,该字段即为注册的对象(在 Android 中时通常是 Activity对象);另一个是 类型为SubscriberMethod 的 subscriberMethod,它就是被@Subscribe注解的那个订阅方法,里面保存了一个重要的字段eventType,它是 Class 类型的,代表了 Event 的类型。在注释2处,新建了一个类型为 Map 的typesBySubscriber对象,它的key为subscriber对象,value为subscriber对象中所有的 Event 类型链表,日常使用中仅用于判断某个对象是否注册过。在注释3处新建了一个类型为ConcurrentHashMap的stickyEvents对象,它是专用于粘性事件处理的一个字段,key为事件的Class对象,value为当前的事件。可能有的同学不了解sticky event,这里解释下: - -- 我们都知道**普通事件是先注册,然后发送事件才能收到;而粘性事件,在发送事件之后再订阅该事件也能收到。并且,粘性事件会保存在内存中,每次进入都会去内存中查找获取最新的粘性事件,除非你手动解除注册**。 - -在注释4处,新建了三个不同类型的事件发送器,这里总结下: - -- mainThreadPoster:主线程事件发送器,通过它的mainThreadPoster.enqueue(subscription, event)方法可以将订阅信息和对应的事件进行入队,然后通过 handler 去发送一个消息,在 handler 的 handleMessage 中去执行方法。 -- backgroundPoster:后台事件发送器,通过它的enqueue() 将方法加入到后台的一个队列,最后通过线程池去执行,注意,它在 Executor的execute()方法 上添加了 synchronized关键字 并设立 了控制标记flag,保证任一时间只且仅能有一个任务会被线程池执行。 -- asyncPoster:实现逻辑类似于backgroundPoster,不同于backgroundPoster的保证任一时间只且仅能有一个任务会被线程池执行的特性,asyncPoster则是异步运行的,可以同时接收多个任务。 - -我们再回到注释5这行代码,这里新建了一个subscriberMethodFinder对象,这是从EventBus中抽离出的订阅方法查询的一个对象,在优秀的源码中,我们经常能看到**组合优于继承**的这种实现思想。在注释6处,从builder中取出了一个默认的线程池对象,它由**Executors的newCachedThreadPool()\**方法创建,它是一个\**有则用、无则创建、无数量上限**的线程池。 - -分析完这些核心的字段之后,后面的讲解就比较轻松了,接着查看EventBus的regist()方法: - -``` -public void register(Object subscriber) { - Class subscriberClass = subscriber.getClass(); - - // 1 - List subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass); - synchronized (this) { - for (SubscriberMethod subscriberMethod : subscriberMethods) { - // 2 - subscribe(subscriber, subscriberMethod); - } - } -} -``` - -在注释1处,根据当前注册类获取 subscriberMethods这个订阅方法列表 。在注释2处,使用了增强for循环令subsciber对象 对 subscriberMethods 中每个 SubscriberMethod 进行订阅。 - -接着查看SubscriberMethodFinder的findSubscriberMethods()方法: - -``` -List findSubscriberMethods(Class subscriberClass) { - // 1 - List subscriberMethods = METHOD_CACHE.get(subscriberClass); - if (subscriberMethods != null) { - return subscriberMethods; - } - - // 2 - if (ignoreGeneratedIndex) { - subscriberMethods = findUsingReflection(subscriberClass); - } else { - subscriberMethods = findUsingInfo(subscriberClass); - } - if (subscriberMethods.isEmpty()) { - throw new EventBusException("Subscriber " + subscriberClass - + " and its super classes have no public methods with the @Subscribe annotation"); - } else { - METHOD_CACHE.put(subscriberClass, subscriberMethods); - return subscriberMethods; - } -} -``` - -在注释1处,如果缓存中有subscriberClass对象对应 的订阅方法列表,则直接返回。注释2处,先详细说说这个**ignoreGeneratedIndex**字段, 它用来**判断是否使用生成的 APT 代码去优化寻找接收事件的过程,如果开启了的话,那么将会通过 subscriberInfoIndexes 来快速得到接收事件方法的相关信息**。如果我们没有在项目中接入 EventBus 的 APT,那么可以将 ignoreGeneratedIndex 字段设为 false 以提高性能。这里ignoreGeneratedIndex 默认为false,所以会执行findUsingInfo()方法,后面生成 subscriberMethods 成功的话会加入到缓存中,失败的话会 抛出异常。 - -接着查看SubscriberMethodFinder的findUsingInfo()方法: - -``` -private List findUsingInfo(Class subscriberClass) { - // 1 - FindState findState = prepareFindState(); - findState.initForSubscriber(subscriberClass); - // 2 - while (findState.clazz != null) { - findState.subscriberInfo = getSubscriberInfo(findState); - if (findState.subscriberInfo != null) { - SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods(); - for (SubscriberMethod subscriberMethod: array) { - if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) { - findState.subscriberMethods.add(subscriberMethod); - } - } - } else { - // 3 - findUsingReflectionInSingleClass(findState); - } - findState.moveToSuperclass(); - } - // 4 - return getMethodsAndRelease(findState); -} -``` - -在注释1处,调用了SubscriberMethodFinder的prepareFindState()方法创建了一个新的 FindState 类,来看看这个方法: - -``` -private static final FindState[] FIND_STATE_POOL = new FindState[POOL_SIZE]; -private FindState prepareFindState() { - // 1 - synchronized(FIND_STATE_POOL) { - for (int i = 0; i < POOL_SIZE; i++) { - FindState state = FIND_STATE_POOL[i]; - if (state != null) { - FIND_STATE_POOL[i] = null; - return state; - } - } - } - // 2 - return new FindState(); -} -``` - -在注释1处,会先从 FIND_STATE_POOL 即 FindState 池中取出可用的 FindState(这里的POOL_SIZE为4),如果没有的话,则通过注释2处的代码直接新建 一个新的 FindState 对象。 - -接着来分析下FindState这个类: - -``` -static class FindState { - .... - void initForSubscriber(Class subscriberClass) { - this.subscriberClass = clazz = subscriberClass; - skipSuperClasses = false; - subscriberInfo = null; - } - ... -} -``` - -它是 SubscriberMethodFinder 的内部类,这个方法主要做一个初始化、回收对象等工作。 - -接着回到SubscriberMethodFinder的注释2处的SubscriberMethodFinder()方法: - -``` -private SubscriberInfo getSubscriberInfo(FindState findState) { - if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) { - SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo(); - if (findState.clazz == superclassInfo.getSubscriberClass()) { - return superclassInfo; - } - } - if (subscriberInfoIndexes != null) { - for (SubscriberInfoIndex index: subscriberInfoIndexes) { - SubscriberInfo info = index.getSubscriberInfo(findState.clazz); - if (info != null) { - return info; - } - } - } - return null; -} -``` - -在前面初始化的时候,findState的subscriberInfo和subscriberInfoIndexes 这两个字段为空,所以这里直接返回 null。 - -接着查看注释3处的findUsingReflectionInSingleClass()方法: - -``` -private void findUsingReflectionInSingleClass(FindState findState) { - Method[] methods; - try { - // This is faster than getMethods, especially when subscribers are fat classes like Activities - methods = findState.clazz.getDeclaredMethods(); - } catch (Throwable th) { - methods = findState.clazz.getMethods(); - findState.skipSuperClasses = true; - } - for (Method method: methods) { - int modifiers = method.getModifiers(); - if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) { - Class [] parameterTypes = method.getParameterTypes(); - if (parameterTypes.length == 1) { - Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class); - if (subscribeAnnotation != null) { - // 重点 - Class eventType = parameterTypes[0]; - if (findState.checkAdd(method, eventType)) { - ThreadMode threadMode = subscribeAnnotation.threadMode(); - findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode, subscribeAnnotation.priority(), subscribeAnnotation.sticky())); - } - } - } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) { - String methodName = method.getDeclaringClass().getName() + "." + method.getName(); - throw new EventBusException("@Subscribe method " + methodName + "must have exactly 1 parameter but has " + parameterTypes.length); - } - } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) { - String methodName = method.getDeclaringClass().getName() + "." + method.getName(); - throw new EventBusException(methodName + " is a illegal @Subscribe method: must be public, non-static, and non-abstract"); - } - } -} -``` - -这个方法很长,大概做的事情是: - -1. **通过反射的方式获取订阅者类中的所有声明方法,然后在这些方法里面寻找以 @Subscribe作为注解的方法进行处理**。 - -2. **在经过经过一轮检查,看看 findState.subscriberMethods是否存在,如果没有,将方法名,threadMode,优先级,是否为 sticky 方法等信息封装到 SubscriberMethod 对象中,最后添加到 subscriberMethods 列表中**。 - -最后,继续查看注释4处的getMethodsAndRelease()方法: - -``` -private List getMethodsAndRelease(FindState findState) { - // 1 - List subscriberMethods = new ArrayList<>(findState.subscriberMethods); - // 2 - findState.recycle(); - // 3 - synchronized(FIND_STATE_POOL) { - for (int i = 0; i < POOL_SIZE; i++) { - if (FIND_STATE_POOL[i] == null) { - FIND_STATE_POOL[i] = findState; - break; - } - } - } - // 4 - return subscriberMethods; -} -``` - -在这里,首先在注释1处,从findState中取出了保存的subscriberMethods。在注释2处,将findState里的保存的所有对象进行回收。在注释3处,把findState存储在 FindState 池中方便下一次使用,以提高性能。最后,在注释4处,返回subscriberMethods。接着,**在EventBus的 register() 方法的最后会调用 subscribe 方法**: - -``` -public void register(Object subscriber) { - Class subscriberClass = subscriber.getClass(); - List subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass); - synchronized (this) { - for (SubscriberMethod subscriberMethod : subscriberMethods) { - subscribe(subscriber, subscriberMethod); - } - } -} -``` - -继续看看这个subscribe()方法做的事情: - -``` -private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) { - Class eventType = subscriberMethod.eventType; - Subscription newSubscription = new Subscription(subscriber, subscriberMethod); - - // 1 - CopyOnWriteArrayList subscriptions = subscriptionsByEventType.get(eventType); - if (subscriptions == null) { - subscriptions = new CopyOnWriteArrayList <> (); - subscriptionsByEventType.put(eventType, subscriptions); - } else { - if (subscriptions.contains(newSubscription)) { - throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event " + eventType); - } - } - int size = subscriptions.size(); - - // 2 - for (int i = 0; i <= size; i++) { - if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) { - subscriptions.add(i, newSubscription); - break; - } - } - - // 3 - List> subscribedEvents = typesBySubscriber.get(subscriber); - if (subscribedEvents == null) { - subscribedEvents = new ArrayList<>(); - typesBySubscriber.put(subscriber, subscribedEvents); - } - subscribedEvents.add(eventType); - // 4 - if (subscriberMethod.sticky) { - if (eventInheritance) { - Set, Object>> entries = stickyEvents.entrySet(); - for (Map.Entry, Object> entry : entries) { - Class candidateEventType = entry.getKey(); - if(eventType.isAssignableFrom(candidateEventType)) { - Object stickyEvent = entry.getValue(); - checkPostStickyEventToSubscription(newSubscription, stickyEvent); - } - } - } else { - Object stickyEvent = stickyEvents.get(eventType); - checkPostStickyEventToSubscription(newSubscription, stickyEvent); - } - } -} -``` - -首先,在注释1处,会根据 subscriberMethod的eventType,在 subscriptionsByEventType 去查找一个 CopyOnWriteArrayList ,如果没有则创建一个新的 CopyOnWriteArrayList,然后将这个 CopyOnWriteArrayList 放入 subscriptionsByEventType 中。在注释2处,**添加 newSubscription对象,它是一个 Subscription 类,里面包含着 subscriber 和 subscriberMethod 等信息,并且这里有一个优先级的判断,说明它是按照优先级添加的。优先级越高,会插到在当前 List 靠前面的位置**。在注释3处,对typesBySubscriber 进行添加,这主要是在EventBus的isRegister()方法中去使用的,目的是用来判断这个 Subscriber对象 是否已被注册过。最后,在注释4处,会判断是否是 sticky事件。如果是sticky事件的话,会调用 checkPostStickyEventToSubscription() 方法。 - -接着查看这个checkPostStickyEventToSubscription()方法: - -``` -private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) { - if (stickyEvent != null) { - postToSubscription(newSubscription, stickyEvent, isMainThread()); - } -} -``` - -可以看到最终是**调用了postToSubscription()这个方法来进行粘性事件的发送**,对于粘性事件的处理,最后再分析,接下来看看事件是如何post的。 - -### EventBus.getDefault().post(new CollectEvent()) - -``` -public void post(Object event) { - // 1 - PostingThreadState postingState = currentPostingThreadState.get(); - List eventQueue = postingState.eventQueue; - eventQueue.add(event); - // 2 - if (!postingState.isPosting) { - postingState.isMainThread = isMainThread(); - postingState.isPosting = true; - if (postingState.canceled) { - throw new EventBusException("Internal error. Abort state was not reset"); - } - try { - while (!eventQueue.isEmpty()) { - postSingleEvent(eventQueue.remove(0), postingState); - } - } finally { - postingState.isPosting = false; - postingState.isMainThread = false; - } - } -} -``` - -注释1处,这里的currentPostingThreadState 是一个 ThreadLocal 类型的对象,里面存储了 PostingThreadState,而 PostingThreadState 中包含了一个 eventQueue 和其他一些标志位,相关的源码如下: - -``` -private final ThreadLocal currentPostingThreadState = new ThreadLocal () { -@Override -protected PostingThreadState initialValue() { - return new PostingThreadState(); -} -}; - -final static class PostingThreadState { - final List eventQueue = new ArrayList<>(); - boolean isPosting; - boolean isMainThread; - Subscription subscription; - Object event; - boolean canceled; -} -``` - -接着把传入的 event,保存到了当前线程中的一个变量 PostingThreadState 的 eventQueue 中。在注释2处,最后调用了 postSingleEvent() 方法,我们继续查看这个方法: - -``` -private void postSingleEvent(Object event, PostingThreadState postingState) throws Error { - Class eventClass = event.getClass(); - boolean subscriptionFound = false; - // 1 - if (eventInheritance) { - // 2 - List> eventTypes = lookupAllEventTypes(eventClass); - int countTypes = eventTypes.size(); - for (int h = 0; h < countTypes; h++) { - Class clazz = eventTypes.get(h); - subscriptionFound |= - // 3 - postSingleEventForEventType(event, postingState, clazz); - } - } else { - subscriptionFound = postSingleEventForEventType(event, postingState, eventClass); - } - if (!subscriptionFound) { - ... - } -} -``` - -首先,在注释1处,首先取出 Event 的 class 类型,接着**会对 eventInheritance 标志位 判断,它默认为true,如果设为 true 的话,它会在发射事件的时候判断是否需要发射父类事件,设为 false,能够提高一些性能**。接着,在注释2处,会调用lookupAllEventTypes() 方法,它的作用就是取出 Event 及其父类和接口的 class 列表,当然重复取的话会影响性能,所以它也做了一个 eventTypesCache 的缓存,这样就不用重复调用 getSuperclass() 方法。最后,在注释3处会调用postSingleEventForEventType()方法,看下这个方法: - -``` -private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class eventClass) { - CopyOnWriteArrayList subscriptions; - synchronized(this) { - subscriptions = subscriptionsByEventType.get(eventClass); - } - if (subscriptions != null && !subscriptions.isEmpty()) { - for (Subscription subscription: subscriptions) { - postingState.event = event; - postingState.subscription = subscription; - boolean aborted = false; - try { - postToSubscription(subscription, event, postingState.isMainThread); - aborted = postingState.canceled; - } finally { - postingState.event = null; - postingState.subscription = null; - postingState.canceled = false; - } - if (aborted) { - break; - } - } - return true; - } - return false; -} -``` - -可以看到,这里直接根据 Event 类型从 subscriptionsByEventType 中取出对应的 subscriptions对象,最后调用了 postToSubscription() 方法。 - -这个时候再看看这个postToSubscription()方法: - -``` -private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) { - switch (subscription.subscriberMethod.threadMode) { - case POSTING: - invokeSubscriber(subscription, event); - break; - case MAIN: - if (isMainThread) { - invokeSubscriber(subscription, event); - } else { - mainThreadPoster.enqueue(subscription, event); - } - break; - case MAIN_ORDERED: - if (mainThreadPoster != null) { - mainThreadPoster.enqueue(subscription, event); - } else { - invokeSubscriber(subscription, event); - } - break; - case BACKGROUND: - if (isMainThread) { - backgroundPoster.enqueue(subscription, event); - } else { - invokeSubscriber(subscription, event); - } - break; - case ASYNC: - asyncPoster.enqueue(subscription, event); - break; - default: - throw new IllegalStateException("Unknow thread mode: " + subscription.subscriberMethod.threadMode); - } -} -``` - -从上面可以看出,这里通过threadMode 来判断在哪个线程中去执行方法: - -1. POSTING:执行 invokeSubscriber() 方法,内部**直接采用反射调用**。 - -2. MAIN:**首先去判断当前是否在 UI 线程,如果是的话则直接反射调用,否则调用mainThreadPoster的enqueue()方法,即把当前的方法加入到队列之中,然后通过 handler 去发送一个消息,在 handler 的 handleMessage 中去执行方法**。 - -3. MAIN_ORDERED:**与MAIN类似,不过是确保是顺序执行的**。 - -4. BACKGROUND:**判断当前是否在 UI 线程,如果不是的话则直接反射调用,是的话通过backgroundPoster的enqueue()方法 将方法加入到后台的一个队列,最后通过线程池去执行。注意,backgroundPoster在 Executor的execute()方法 上添加了 synchronized关键字 并设立 了控制标记flag,保证任一时间只且仅能有一个任务会被线程池执行**。 - -5. ASYNC:**逻辑实现类似于BACKGROUND,将任务加入到后台的一个队列,最终由Eventbus 中的一个线程池去调用,这里的线程池与 BACKGROUND 逻辑中的线程池用的是同一个,即使用Executors的newCachedThreadPool()方法创建的线程池,它是一个有则用、无则创建、无数量上限的线程池。不同于backgroundPoster的保证任一时间只且仅能有一个任务会被线程池执行的特性,这里asyncPoster则是异步运行的,可以同时接收多个任务**。 - -分析完EventBus的post()方法值,接着看看它的unregister()。 - -### EventBus.getDefault().unregister(this) - -它的核心源码如下所示: - -``` -public synchronized void unregister(Object subscriber) { - List> subscribedTypes = typesBySubscriber.get(subscriber); - if (subscribedTypes != null) { - for (Class eventType : subscribedTypes) { - //1 - unsubscribeByEventType(subscriber, eventType); - } - // 2 - typesBySubscriber.remove(subscriber); - } -} -``` - -首先,在注释1处,**unsubscribeByEventType() 方法中对 subscriptionsByEventType 移除了该 subscriber 的所有订阅信息**。最后,在注释2处,**移除了注册对象和其对应的所有 Event 事件链表**。 - -最后,再来分析下EventBus中对粘性事件的处理。 - -### EventBus.getDefault.postSticky(new CollectEvent()) - -如果想要发射 sticky 事件需要通过 EventBus的postSticky() 方法,内部源码如下所示: - -``` -public void postSticky(Object event) { - synchronized (stickyEvents) { - // 1 - stickyEvents.put(event.getClass(), event); - } - // 2 - post(event); -} -``` - -在注释1处,先将该事件放入 stickyEvents 中,接着在注释2处使用post()发送事件。前面我们在分析register()方法的最后部分时,其中有关粘性事件的源码如下: - -``` -if (subscriberMethod.sticky) { - Object stickyEvent = stickyEvents.get(eventType); - if (stickyEvent != null) { - postToSubscription(newSubscription, stickyEvent, isMainThread()); - } -} -``` - -可以看到,在这里**会判断当前事件是否是 sticky 事件,如果是,则从 stickyEvents 中拿出该事件并执行 postToSubscription() 方法**。 - - - -> EventBus 的源码在Android主流三方库源码分析系列中可以说是除了ButterKnife之外,算是比较简单的了。但是,它其中的一些思想和设计是值得借鉴的。比如**它使用 FindState 复用池来复用 FindState 对象,在各处使用了 synchronized 关键字进行代码块同步的一些优化操作**。其中上面分析了这么多,**EventBus最核心的逻辑就是利用了 subscriptionsByEventType 这个重要的列表,将订阅对象,即接收事件的方法存储在这个列表,发布事件的时候在列表中查询出相对应的方法并执行**。 diff --git "a/Docs/Android\346\211\251\345\261\225\347\237\245\350\257\206\347\202\271.md" "b/Docs/Android\346\211\251\345\261\225\347\237\245\350\257\206\347\202\271.md" deleted file mode 100644 index 9baa87e..0000000 --- "a/Docs/Android\346\211\251\345\261\225\347\237\245\350\257\206\347\202\271.md" +++ /dev/null @@ -1,994 +0,0 @@ -- [ART](#art) - - [ART 功能](#art-%e5%8a%9f%e8%83%bd) - - [预先 (AOT) 编译](#%e9%a2%84%e5%85%88-aot-%e7%bc%96%e8%af%91) - - [垃圾回收优化](#%e5%9e%83%e5%9c%be%e5%9b%9e%e6%94%b6%e4%bc%98%e5%8c%96) - - [开发和调试方面的优化](#%e5%bc%80%e5%8f%91%e5%92%8c%e8%b0%83%e8%af%95%e6%96%b9%e9%9d%a2%e7%9a%84%e4%bc%98%e5%8c%96) - - [ART GC](#art-gc) -- [Hook](#hook) - - [基本流程](#%e5%9f%ba%e6%9c%ac%e6%b5%81%e7%a8%8b) - - [使用示例](#%e4%bd%bf%e7%94%a8%e7%a4%ba%e4%be%8b) -- [Proguard](#proguard) - - [规则](#%E8%A7%84%E5%88%99) - - [公共模板](#%e5%85%ac%e5%85%b1%e6%a8%a1%e6%9d%bf) - - [常用的自定义混淆规则](#%e5%b8%b8%e7%94%a8%e7%9a%84%e8%87%aa%e5%ae%9a%e4%b9%89%e6%b7%b7%e6%b7%86%e8%a7%84%e5%88%99) - - [aar中增加独立的混淆配置](#aar%e4%b8%ad%e5%a2%9e%e5%8a%a0%e7%8b%ac%e7%ab%8b%e7%9a%84%e6%b7%b7%e6%b7%86%e9%85%8d%e7%bd%ae) - - [检查混淆和追踪异常](#%e6%a3%80%e6%9f%a5%e6%b7%b7%e6%b7%86%e5%92%8c%e8%bf%bd%e8%b8%aa%e5%bc%82%e5%b8%b8) -- [架构](#%e6%9e%b6%e6%9e%84) - - [MVC](#mvc) - - [MVP](#mvp) - - [MVVM](#mvvm) -- [Jetpack](#jetpack) - - [架构](#%e6%9e%b6%e6%9e%84-1) - - [使用示例](#%e4%bd%bf%e7%94%a8%e7%a4%ba%e4%be%8b-1) -- [NDK 开发](#ndk-%e5%bc%80%e5%8f%91) - - [JNI 基础](#jni-%e5%9f%ba%e7%a1%80) - - [数据类型](#%e6%95%b0%e6%8d%ae%e7%b1%bb%e5%9e%8b) - - [String 字符串函数操作](#string-%e5%ad%97%e7%ac%a6%e4%b8%b2%e5%87%bd%e6%95%b0%e6%93%8d%e4%bd%9c) - - [常用 JNI 访问 Java 对象方法](#%e5%b8%b8%e7%94%a8-jni-%e8%ae%bf%e9%97%ae-java-%e5%af%b9%e8%b1%a1%e6%96%b9%e6%b3%95) - - [NDK 开发](#ndk-%e5%bc%80%e5%8f%91-1) - - [基础开发流程](#%e5%9f%ba%e7%a1%80%e5%bc%80%e5%8f%91%e6%b5%81%e7%a8%8b) - - [System.loadLibrary()](#systemloadlibrary) - - [CMake 构建 NDK 项目](#cmake-%e6%9e%84%e5%bb%ba-ndk-%e9%a1%b9%e7%9b%ae) - - [常用的 Android NDK 原生 API](#%e5%b8%b8%e7%94%a8%e7%9a%84-android-ndk-%e5%8e%9f%e7%94%9f-api) -- [类加载器](#%e7%b1%bb%e5%8a%a0%e8%bd%bd%e5%99%a8) - - [双亲委托模式](#%e5%8f%8c%e4%ba%b2%e5%a7%94%e6%89%98%e6%a8%a1%e5%bc%8f) - - [DexPathList](#dexpathlist) -# ART -ART 代表 Android Runtime,其处理应用程序执行的方式完全不同于 Dalvik,Dalvik 是依靠一个 Just-In-Time (JIT) 编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运 行。ART 则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫 Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。 - -## ART 功能 -### 预先 (AOT) 编译 -ART 引入了预先编译机制,可提高应用的性能。ART 还具有比 Dalvik 更严格的安装时验证。在安装时,ART 使用设备自带的 dex2oat 工具来编译应用。该实用工具接受 DEX 文件作为输入,并为目标设备生成经过编译的应用可执行文件。该工具应能够顺利编译所有有效的 DEX 文件。 - -### 垃圾回收优化 -垃圾回收 (GC) 可能有损于应用性能,从而导致显示不稳定、界面响应速度缓慢以及其他问题。ART 通过以下几种方式对垃圾回收做了优化: -- 只有一次(而非两次)GC 暂停 -- 在 GC 保持暂停状态期间并行处理 -- 在清理最近分配的短时对象这种特殊情况中,回收器的总 GC 时间更短 -- 优化了垃圾回收的工效,能够更加及时地进行并行垃圾回收,这使得 GC_FOR_ALLOC 事件在典型用例中极为罕见 -- 压缩 GC 以减少后台内存使用和碎片 - -### 开发和调试方面的优化 -- 支持采样分析器 - -一直以来,开发者都使用 Traceview 工具(用于跟踪应用执行情况)作为分析器。虽然 Traceview 可提供有用的信息,但每次方法调用产生的开销会导致 Dalvik 分析结果出现偏差,而且使用该工具明显会影响运行时性能 - -ART 添加了对没有这些限制的专用采样分析器的支持,因而可更准确地了解应用执行情况,而不会明显减慢速度。KitKat 版本为 Dalvik 的 Traceview 添加了采样支持。 - - -- 支持更多调试功能 - -ART 支持许多新的调试选项,特别是与监控和垃圾回收相关的功能。例如,查看堆栈跟踪中保留了哪些锁,然后跳转到持有锁的线程;询问指定类的当前活动的实例数、请求查看实例,以及查看使对象保持有效状态的参考;过滤特定实例的事件(如断点)等。 - -- 优化了异常和崩溃报告中的诊断详细信息 - -当发生运行时异常时,ART 会为您提供尽可能多的上下文和详细信息。ART 会提供 ``java.lang.ClassCastException``、``java.lang.ClassNotFoundException`` 和 ``java.lang.NullPointerException`` 的更多异常详细信息(较高版本的 Dalvik 会提供 ``java.lang.ArrayIndexOutOfBoundsException`` 和 ``java.lang.ArrayStoreException`` 的更多异常详细信息,这些信息现在包括数组大小和越界偏移量;ART 也提供这类信息)。 - -## ART GC -ART 有多个不同的 GC 方案,这些方案包括运行不同垃圾回收器。默认方案是 CMS(并发标记清除)方案,主要使用粘性 CMS 和部分 CMS。粘性 CMS 是 ART 的不移动分代垃圾回收器。它仅扫描堆中自上次 GC 后修改的部分,并且只能回收自上次 GC 后分配的对象。除 CMS 方案外,当应用将进程状态更改为察觉不到卡顿的进程状态(例如,后台或缓存)时,ART 将执行堆压缩。 - -除了新的垃圾回收器之外,ART 还引入了一种基于位图的新内存分配程序,称为 RosAlloc(插槽运行分配器)。此新分配器具有分片锁,当分配规模较小时可添加线程的本地缓冲区,因而性能优于 DlMalloc。 - -与 Dalvik 相比,ART CMS 垃圾回收计划在很多方面都有一定的改善: - -- 与 Dalvik 相比,暂停次数从 2 次减少到 1 次。Dalvik 的第一次暂停主要是为了进行根标记,即在 ART 中进行并发标记,让线程标记自己的根,然后马上恢复运行。 - -- 与 Dalvik 类似,ART GC 在清除过程开始之前也会暂停 1 次。两者在这方面的主要差异在于:在此暂停期间,某些 Dalvik 环节在 ART 中并发进行。这些环节包括 java.lang.ref.Reference 处理、系统弱清除(例如,jni 弱全局等)、重新标记非线程根和卡片预清理。在 ART 暂停期间仍进行的阶段包括扫描脏卡片以及重新标记线程根,这些操作有助于缩短暂停时间。 - -- 相对于 Dalvik,ART GC 改进的最后一个方面是粘性 CMS 回收器增加了 GC 吞吐量。不同于普通的分代 GC,粘性 CMS 不移动。系统会将年轻对象保存在一个分配堆栈(基本上是 java.lang.Object 数组)中,而非为其设置一个专属区域。这样可以避免移动所需的对象以维持低暂停次数,但缺点是容易在堆栈中加入大量复杂对象图像而使堆栈变长。 - -ART GC 与 Dalvik 的另一个主要区别在于 ART GC 引入了移动垃圾回收器。使用移动 GC 的目的在于通过堆压缩来减少后台应用使用的内存。目前,触发堆压缩的事件是 ActivityManager 进程状态的改变。当应用转到后台运行时,它会通知 ART 已进入不再“感知”卡顿的进程状态。此时 ART 会进行一些操作(例如,压缩和监视器压缩),从而导致应用线程长时间暂停。目前正在使用的两个移动 GC 是同构空间压缩和半空间压缩。 - -- 半空间压缩将对象在两个紧密排列的碰撞指针空间之间进行移动。这种移动 GC 适用于小内存设备,因为它可以比同构空间压缩稍微多节省一点内存。额外节省出的空间主要来自紧密排列的对象,这样可以避免 RosAlloc/DlMalloc 分配器占用开销。由于 CMS 仍在前台使用,且不能从碰撞指针空间中进行收集,因此当应用在前台使用时,半空间还要再进行一次转换。这种情况并不理想,因为它可能引起较长时间的暂停。 - -- 同构空间压缩通过将对象从一个 RosAlloc 空间复制到另一个 RosAlloc 空间来实现。这有助于通过减少堆碎片来减少内存使用量。这是目前非低内存设备的默认压缩模式。相比半空间压缩,同构空间压缩的主要优势在于应用从后台切换到前台时无需进行堆转换。 - -# Hook -## 基本流程 -1、根据需求确定 要 hook 的对象 -2、寻找要hook的对象的持有者,拿到要 hook 的对象 -3、定义“要 hook 的对象”的代理类,并且创建该类的对象 -4、使用上一步创建出来的对象,替换掉要 hook 的对象 - -## 使用示例 -```java -/** -* hook的核心代码 -* 这个方法的唯一目的:用自己的点击事件,替换掉 View 原来的点击事件 -* -* @param view hook的范围仅限于这个view -*/ -@SuppressLint({"DiscouragedPrivateApi", "PrivateApi"}) -public static void hook(Context context, final View view) {// - try { - // 反射执行View类的getListenerInfo()方法,拿到v的mListenerInfo对象,这个对象就是点击事件的持有者 - Method method = View.class.getDeclaredMethod("getListenerInfo"); - method.setAccessible(true);//由于getListenerInfo()方法并不是public的,所以要加这个代码来保证访问权限 - Object mListenerInfo = method.invoke(view);//这里拿到的就是mListenerInfo对象,也就是点击事件的持有者 - - // 要从这里面拿到当前的点击事件对象 - Class listenerInfoClz = Class.forName("android.view.View$ListenerInfo");// 这是内部类的表示方法 - Field field = listenerInfoClz.getDeclaredField("mOnClickListener"); - final View.OnClickListener onClickListenerInstance = (View.OnClickListener) field.get(mListenerInfo);//取得真实的mOnClickListener对象 - - // 2. 创建我们自己的点击事件代理类 - // 方式1:自己创建代理类 - // ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance); - // 方式2:由于View.OnClickListener是一个接口,所以可以直接用动态代理模式 - // Proxy.newProxyInstance的3个参数依次分别是: - // 本地的类加载器; - // 代理类的对象所继承的接口(用Class数组表示,支持多个接口) - // 代理类的实际逻辑,封装在new出来的InvocationHandler内 - Object proxyOnClickListener = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() { - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - Log.d("HookSetOnClickListener", "点击事件被hook到了");//加入自己的逻辑 - return method.invoke(onClickListenerInstance, args);//执行被代理的对象的逻辑 - } - }); - // 3. 用我们自己的点击事件代理类,设置到"持有者"中 - field.set(mListenerInfo, proxyOnClickListener); - } catch (Exception e) { - e.printStackTrace(); - } -} - -// 自定义代理类 -static class ProxyOnClickListener implements View.OnClickListener { - View.OnClickListener oriLis; - - public ProxyOnClickListener(View.OnClickListener oriLis) { - this.oriLis = oriLis; - } - - @Override - public void onClick(View v) { - Log.d("HookSetOnClickListener", "点击事件被hook到了"); - if (oriLis != null) { - oriLis.onClick(v); - } - } -} -``` - -# Proguard -Proguard 具有以下三个功能: -- 压缩(Shrink): 检测和删除没有使用的类,字段,方法和特性 -- 优化(Optimize) : 分析和优化Java字节码 -- 混淆(Obfuscate): 使用简短的无意义的名称,对类,字段和方法进行重命名 - -## 规则 -- 关键字 - - | 关键字 | 描述 -|--|-- -| keep | 保留类和类中的成员,防止被混淆或移除 -| keepnames | 保留类和类中的成员,防止被混淆,成员没有被引用会被移除 -| keepclassmembers | 只保留类中的成员,防止被混淆或移除 -| keepclassmembernames | 只保留类中的成员,防止被混淆,成员没有引用会被移除 -| keepclasseswithmembers | 保留类和类中的成员,防止被混淆或移除,保留指明的成员 -| keepclasseswithmembernames | 保留类和类中的成员,防止被混淆,保留指明的成员,成员没有引用会被移除 - - - 通配符 - - | 通配符 | 描述 -|--|-- -| \ | 匹配类中的所有字段 -| \ | 匹配类中所有的方法 -| \ | 匹配类中所有的构造函数 -| * | 匹配任意长度字符,不包含包名分隔符(.) -| ** | 匹配任意长度字符,包含包名分隔符(.) -| *** | 匹配任意参数类型 - - - 指定混淆时可使用字典 -``` --applymapping filename 指定重用一个已经写好了的map文件作为新旧元素名的映射。 --obfuscationdictionary filename 指定一个文本文件用来生成混淆后的名字。 --classobfuscationdictionary filename 指定一个混淆类名的字典 --packageobfuscationdictionary filename 指定一个混淆包名的字典 --overloadaggressively 混淆的时候大量使用重载,多个方法名使用同一个混淆名(慎用) -``` - -## 公共模板 -``` -############################################# -# -# 对于一些基本指令的添加 -# -############################################# -# 代码混淆压缩比,在 0~7 之间,默认为 5,一般不做修改 --optimizationpasses 5 - -# 混合时不使用大小写混合,混合后的类名为小写 --dontusemixedcaseclassnames - -# 指定不去忽略非公共库的类 --dontskipnonpubliclibraryclasses - -# 这句话能够使我们的项目混淆后产生映射文件 -# 包含有类名->混淆后类名的映射关系 --verbose - -# 指定不去忽略非公共库的类成员 --dontskipnonpubliclibraryclassmembers - -# 不做预校验,preverify 是 proguard 的四个步骤之一,Android 不需要 preverify,去掉这一步能够加快混淆速度。 --dontpreverify - -# 保留 Annotation 不混淆 --keepattributes *Annotation*,InnerClasses - -# 避免混淆泛型 --keepattributes Signature - -# 抛出异常时保留代码行号 --keepattributes SourceFile,LineNumberTable - -# 指定混淆是采用的算法,后面的参数是一个过滤器 -# 这个过滤器是谷歌推荐的算法,一般不做更改 --optimizations !code/simplification/cast,!field/*,!class/merging/* - - -############################################# -# -# Android开发中一些需要保留的公共部分 -# -############################################# - -# 保留我们使用的四大组件,自定义的 Application 等等这些类不被混淆 -# 因为这些子类都有可能被外部调用 --keep public class * extends android.app.Activity --keep public class * extends android.app.Appliction --keep public class * extends android.app.Service --keep public class * extends android.content.BroadcastReceiver --keep public class * extends android.content.ContentProvider --keep public class * extends android.app.backup.BackupAgentHelper --keep public class * extends android.preference.Preference --keep public class * extends android.view.View --keep public class com.android.vending.licensing.ILicensingService - - -# 保留 support 下的所有类及其内部类 --keep class android.support.** { *; } - -# 保留继承的 --keep public class * extends android.support.v4.** --keep public class * extends android.support.v7.** --keep public class * extends android.support.annotation.** - -# 保留 R 下面的资源 --keep class **.R$* { *; } - -# 保留本地 native 方法不被混淆 --keepclasseswithmembernames class * { - native ; -} - -# 保留在 Activity 中的方法参数是view的方法, -# 这样以来我们在 layout 中写的 onClick 就不会被影响 --keepclassmembers class * extends android.app.Activity { - public void *(android.view.View); -} - -# 保留枚举类不被混淆 --keepclassmembers enum * { - public static **[] values(); - public static ** valueOf(java.lang.String); -} - -# 保留我们自定义控件(继承自 View)不被混淆 --keep public class * extends android.view.View { - *** get*(); - void set*(***); - public (android.content.Context); - public (android.content.Context, android.util.AttributeSet); - public (android.content.Context, android.util.AttributeSet, int); -} - -# 保留 Parcelable 序列化类不被混淆 --keep class * implements android.os.Parcelable { - public static final android.os.Parcelable$Creator *; -} - -# 保留 Serializable 序列化的类不被混淆 --keepnames class * implements java.io.Serializable --keepclassmembers class * implements java.io.Serializable { - static final long serialVersionUID; - private static final java.io.ObjectStreamField[] serialPersistentFields; - !static !transient ; - !private ; - !private ; - private void writeObject(java.io.ObjectOutputStream); - private void readObject(java.io.ObjectInputStream); - java.lang.Object writeReplace(); - java.lang.Object readResolve(); -} - -# 对于带有回调函数的 onXXEvent、**On*Listener 的,不能被混淆 --keepclassmembers class * { - void *(**On*Event); - void *(**On*Listener); -} - -# webView 处理,项目中没有使用到 webView 忽略即可 --keepclassmembers class fqcn.of.javascript.interface.for.webview { - public *; -} --keepclassmembers class * extends android.webkit.webViewClient { - public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap); - public boolean *(android.webkit.WebView, java.lang.String); -} --keepclassmembers class * extends android.webkit.webViewClient { - public void *(android.webkit.webView, java.lang.String); -} - -# js --keepattributes JavascriptInterface --keep class android.webkit.JavascriptInterface { *; } --keepclassmembers class * { - @android.webkit.JavascriptInterface ; -} - -# @Keep --keep,allowobfuscation @interface android.support.annotation.Keep --keep @android.support.annotation.Keep class * --keepclassmembers class * { - @android.support.annotation.Keep *; -} -``` - -## 常用的自定义混淆规则 -```xml -# 通配符*,匹配任意长度字符,但不含包名分隔符(.) -# 通配符**,匹配任意长度字符,并且包含包名分隔符(.) - -# 不混淆某个类 --keep public class com.jasonwu.demo.Test { *; } - -# 不混淆某个包所有的类 --keep class com.jasonwu.demo.test.** { *; } - -# 不混淆某个类的子类 --keep public class * com.jasonwu.demo.Test { *; } - -# 不混淆所有类名中包含了 ``model`` 的类及其成员 --keep public class **.*model*.** {*;} - -# 不混淆某个接口的实现 --keep class * implements com.jasonwu.demo.TestInterface { *; } - -# 不混淆某个类的构造方法 --keepclassmembers class com.jasonwu.demo.Test { - public (); -} - -# 不混淆某个类的特定的方法 --keepclassmembers class com.jasonwu.demo.Test { - public void test(java.lang.String); -} -``` - - -## aar中增加独立的混淆配置 -``build.gralde`` -```gradle -android { - ··· - defaultConfig { - ··· - consumerProguardFile 'proguard-rules.pro' - } - ··· -} -``` - -## 检查混淆和追踪异常 -开启 Proguard 功能,则每次构建时 ProGuard 都会输出下列文件: - -- dump.txt -说明 APK 中所有类文件的内部结构。 - -- mapping.txt -提供原始与混淆过的类、方法和字段名称之间的转换。 - -- seeds.txt -列出未进行混淆的类和成员。 - -- usage.txt -列出从 APK 移除的代码。 - -这些文件保存在 /build/outputs/mapping/release/ 中。我们可以查看 seeds.txt 里面是否是我们需要保留的,以及 usage.txt 里查看是否有误删除的代码。 mapping.txt 文件很重要,由于我们的部分代码是经过重命名的,如果该部分出现 bug,对应的异常堆栈信息里的类或成员也是经过重命名的,难以定位问题。我们可以用 retrace 脚本(在 Windows 上为 retrace.bat;在 Mac/Linux 上为 retrace.sh)。它位于 /tools/proguard/ 目录中。该脚本利用 mapping.txt 文件和你的异常堆栈文件生成没有经过混淆的异常堆栈文件,这样就可以看清是哪里出问题了。使用 retrace 工具的语法如下: - -```shell -retrace.bat|retrace.sh [-verbose] mapping.txt [] -``` - -# 架构 -## MVC -![](https://mmbiz.qpic.cn/mmbiz_png/zKFJDM5V3Wy5xbLTp6JMMdouZiavFxyYCwLhGyLdicyLzgUDKFTZVt1OgU6iaSx2IUwnygzmQzW7Renaa8hmQ62cQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) - -在 Android 中,三者的关系如下: - -![](https://mmbiz.qpic.cn/mmbiz_png/zKFJDM5V3Wy5xbLTp6JMMdouZiavFxyYCicNvEVMO9vDgukUR29Z1DCacZJwmmH1EEb7gUOZmDxolWexP01O8jfg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) - -由于在 Android 中 xml 布局的功能性太弱,所以 Activity 承担了绝大部分的工作,所以在 Android 中 mvc 更像: - -![](https://mmbiz.qpic.cn/mmbiz_png/zKFJDM5V3Wy5xbLTp6JMMdouZiavFxyYCOq89MLQX4UM3dgBTQfU72desHb1XbOWRQZINnXOCCdZCuicUiaTHhtEg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) - -总结: -- 具有一定的分层,model 解耦,controller 和 view 并没有解耦 -- controller 和 view 在 Android 中无法做到彻底分离,Controller 变得臃肿不堪 -- 易于理解、开发速度快、可维护性高 - -## MVP -![](https://mmbiz.qpic.cn/mmbiz_png/zKFJDM5V3Wy5xbLTp6JMMdouZiavFxyYCLVgibsuVQFguBI8FBdZibLNfpvbpd6njkdGWdyR2UL6TzMOhKHFqLC0Q/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) - -通过引入接口 BaseView,让相应的视图组件如 Activity,Fragment去实现 BaseView,把业务逻辑放在 presenter 层中,弱化 Model 只有跟 view 相关的操作都由 View 层去完成。 - -总结: -- 彻底解决了 MVC 中 View 和 Controller 傻傻分不清楚的问题 -- 但是随着业务逻辑的增加,一个页面可能会非常复杂,UI 的改变是非常多,会有非常多的 case,这样就会造成 View 的接口会很庞大 -- 更容易单元测试 - -## MVVM -![](https://mmbiz.qpic.cn/mmbiz_png/zKFJDM5V3Wy5xbLTp6JMMdouZiavFxyYCMygIDD6xo5djkq6Y3jZo53sT2A4kKNaz8JEVRwmUnTmcAwJm0pZVWg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) - -在 MVP 中 View 和 Presenter 要相互持有,方便调用对方,而在 MVP 中 View 和 ViewModel 通过 Binding 进行关联,他们之前的关联处理通过 DataBinding 完成。 - -总结: -- 很好的解决了 MVC 和 MVP 的问题 -- 视图状态较多,ViewModel 的构建和维护的成本都会比较高 -- 但是由于数据和视图的双向绑定,导致出现问题时不太好定位来源 - -# Jetpack -## 架构 -![](https://developer.android.google.cn/topic/libraries/architecture/images/final-architecture.png) - -## 使用示例 -``build.gradle`` -```groovy -android { - ··· - dataBinding { - enabled = true - } -} -dependencies { - ··· - implementation "androidx.fragment:fragment-ktx:$rootProject.fragmentVersion" - implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.lifecycleVersion" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.lifecycleVersion" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.lifecycleVersion" -} -``` - -``fragment_plant_detail.xml`` -```xml - - - - - - - - - - - - -``` - - -``PlantDetailFragment.kt`` -```kotlin -class PlantDetailFragment : Fragment() { - - private val args: PlantDetailFragmentArgs by navArgs() - private lateinit var shareText: String - - private val plantDetailViewModel: PlantDetailViewModel by viewModels { - InjectorUtils.providePlantDetailViewModelFactory(requireActivity(), args.plantId) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = DataBindingUtil.inflate( - inflater, R.layout.fragment_plant_detail, container, false).apply { - viewModel = plantDetailViewModel - lifecycleOwner = this@PlantDetailFragment - } - - plantDetailViewModel.plant.observe(this) { plant -> - // 更新相关 UI - } - - return binding.root - } -} -``` - -``Plant.kt`` -```kotlin -data class Plant ( - val name: String -) -``` - -``PlantDetailViewModel.kt`` -```kotlin -class PlantDetailViewModel( - plantRepository: PlantRepository, - private val plantId: String -) : ViewModel() { - - val plant: LiveData - - override fun onCleared() { - super.onCleared() - viewModelScope.cancel() - } - - init { - plant = plantRepository.getPlant(plantId) - } -} -``` - -``PlantDetailViewModelFactory.kt`` -```kotlin -class PlantDetailViewModelFactory( - private val plantRepository: PlantRepository, - private val plantId: String -) : ViewModelProvider.NewInstanceFactory() { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return PlantDetailViewModel(plantRepository, plantId) as T - } -} -``` - -``InjectorUtils.kt`` -```kotlin -object InjectorUtils { - private fun getPlantRepository(context: Context): PlantRepository { - ··· - } - - fun providePlantDetailViewModelFactory( - context: Context, - plantId: String - ): PlantDetailViewModelFactory { - return PlantDetailViewModelFactory(getPlantRepository(context), plantId) - } -} -``` - -# NDK 开发 -> NDK 全称是 Native Development Kit,是一组可以让你在 Android 应用中编写实现 C/C++ 的工具,可以在项目用自己写源代码构建,也可以利用现有的预构建库。 - -使用 NDK 的使用目的有: -- 从设备获取更好的性能以用于计算密集型应用,例如游戏或物理模拟 -- 重复使用自己或其他开发者的 C/C++ 库,便利于跨平台。 -- NDK 集成了譬如 OpenSL、Vulkan 等 API 规范的特定实现,以实现在 java 层无法做到的功能如提升音频性能等 -- 增加反编译难度 - -## JNI 基础 -### 数据类型 -- 基本数据类型 - -| Java 类型 | Native 类型 | 符号属性 | 字长 -|--|--|--|-- -| boolean | jboolean | 无符号 | 8位 -| byte | jbyte | 无符号 | 8位 -| char | jchar | 无符号 | 16位 -| short | jshort | 有符号 | 16位 -| int | jnit | 有符号 | 32位 -| long | jlong | 有符号 | 64位 -| float | jfloat | 有符号 | 32位 -| double | jdouble | 有符号 | 64位 - -- 引用数据类型 - -| Java 引用类型 | Native 类型 | Java 引用类型 | Native 类型 -|--|--|--|-- -| All objects | jobject | char[] | jcharArray -| java.lang.Class | jclass | short[] | jshortArray -| java.lang.String | jstring | int[] | jintArray -| Object[] | jobjectArray | long[] | jlongArray -| boolean[] | jbooleanArray | float[] | jfloatArray -| byte[] | jbyteArray | double[] | jdoubleArray -| java.lang.Throwable | jthrowable - -### String 字符串函数操作 -| JNI 函数 | 描述 -|--|-- -| GetStringChars / ReleaseStringChars | 获得或释放一个指向 Unicode 编码的字符串的指针(指 C/C++ 字符串) -| GetStringUTFChars / ReleaseStringUTFChars | 获得或释放一个指向 UTF-8 编码的字符串的指针(指 C/C++ 字符串) -| GetStringLength | 返回 Unicode 编码的字符串的长度 -| getStringUTFLength | 返回 UTF-8 编码的字符串的长度 -| NewString | 将 Unicode 编码的 C/C++ 字符串转换为 Java 字符串 -| NewStringUTF | 将 UTF-8 编码的 C/C++ 字符串转换为 Java 字符串 -| GetStringCritical / ReleaseStringCritical | 获得或释放一个指向字符串内容的指针(指 Java 字符串) -| GetStringRegion | 获取或者设置 Unicode 编码的字符串的指定范围的内容 -| GetStringUTFRegion | 获取或者设置 UTF-8 编码的字符串的指定范围的内容 - -### 常用 JNI 访问 Java 对象方法 -``MyJob.java`` -```java -package com.example.myjniproject; - -public class MyJob { - - public static String JOB_STRING = "my_job"; - private int jobId; - - public MyJob(int jobId) { - this.jobId = jobId; - } - - public int getJobId() { - return jobId; - } -} -``` -``native-lib.cpp`` -```c++ -#include - -extern "C" -JNIEXPORT jint JNICALL -Java_com_example_myjniproject_MainActivity_getJobId(JNIEnv *env, jobject thiz, jobject job) { - - // 根据实力获取 class 对象 - jclass jobClz = env->GetObjectClass(job); - // 根据类名获取 class 对象 - jclass jobClz = env->FindClass("com/example/myjniproject/MyJob"); - - // 获取属性 id - jfieldID fieldId = env->GetFieldID(jobClz, "jobId", "I"); - // 获取静态属性 id - jfieldID sFieldId = env->GetStaticFieldID(jobClz, "JOB_STRING", "Ljava/lang/String;"); - - // 获取方法 id - jmethodID methodId = env->GetMethodID(jobClz, "getJobId", "()I"); - // 获取构造方法 id - jmethodID initMethodId = env->GetMethodID(jobClz, "", "(I)V"); - - // 根据对象属性 id 获取该属性值 - jint id = env->GetIntField(job, fieldId); - // 根据对象方法 id 调用该方法 - jint id = env->CallIntMethod(job, methodId); - - // 创建新的对象 - jobject newJob = env->NewObject(jobClz, initMethodId, 10); - - return id; -} -``` - -## NDK 开发 -### 基础开发流程 -- 在 java 中声明 native 方法 -```java -public class MainActivity extends AppCompatActivity { - - // Used to load the 'native-lib' library on application startup. - static { - System.loadLibrary("native-lib"); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - Log.d("MainActivity", stringFromJNI()); - } - - private native String stringFromJNI(); -} -``` - -- 在 ``app/src/main`` 目录下新建 cpp 目录,新建相关 cpp 文件,实现相关方法(AS 可用快捷键快速生成) - -``native-lib.cpp`` -``` -#include - -extern "C" JNIEXPORT jstring JNICALL -Java_com_example_myjniproject_MainActivity_stringFromJNI( - JNIEnv *env, - jobject /* this */) { - std::string hello = "Hello from C++"; - return env->NewStringUTF(hello.c_str()); -} -``` - ->- 函数名的格式遵循遵循如下规则:Java_包名_类名_方法名。 ->- extern "C" 指定采用 C 语言的命名风格来编译,否则由于 C 与 C++ 风格不同,导致链接时无法找到具体的函数 ->- JNIEnv*:表示一个指向 JNI 环境的指针,可以通过他来访问 JNI 提供的接口方法 ->- jobject:表示 java 对象中的 this ->- JNIEXPORT 和 JNICALL:JNI 所定义的宏,可以在 jni.h 头文件中查找到 - -- 通过 CMake 或者 ndk-build 构建动态库 - -### System.loadLibrary() -``java/lang/System.java``: -```java -@CallerSensitive -public static void load(String filename) { - Runtime.getRuntime().load0(Reflection.getCallerClass(), filename); -} -``` - -- 调用 ``Runtime`` 相关 native 方法 - -``java/lang/Runtime.java``: -```java -private static native String nativeLoad(String filename, ClassLoader loader, Class caller); -``` - -- native 方法的实现如下: - -``dalvik/vm/native/java_lang_Runtime.cpp``: -```cpp -static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args, - JValue* pResult) -{ - ··· - bool success; - - assert(fileNameObj != NULL); - // 将 Java 的 library path String 转换到 native 的 String - fileName = dvmCreateCstrFromString(fileNameObj); - - success = dvmLoadNativeCode(fileName, classLoader, &reason); - if (!success) { - const char* msg = (reason != NULL) ? reason : "unknown failure"; - result = dvmCreateStringFromCstr(msg); - dvmReleaseTrackedAlloc((Object*) result, NULL); - } - ··· -} -``` - -- ``dvmLoadNativeCode`` 函数实现如下: - -``dalvik/vm/Native.cpp`` -```cpp -bool dvmLoadNativeCode(const char* pathName, Object* classLoader, - char** detail) -{ - SharedLib* pEntry; - void* handle; - ··· - *detail = NULL; - - // 如果已经加载过了,则直接返回 true - pEntry = findSharedLibEntry(pathName); - if (pEntry != NULL) { - if (pEntry->classLoader != classLoader) { - ··· - return false; - } - ··· - if (!checkOnLoadResult(pEntry)) - return false; - return true; - } - - Thread* self = dvmThreadSelf(); - ThreadStatus oldStatus = dvmChangeStatus(self, THREAD_VMWAIT); - // 把.so mmap 到进程空间,并把 func 等相关信息填充到 soinfo 中 - handle = dlopen(pathName, RTLD_LAZY); - dvmChangeStatus(self, oldStatus); - ··· - // 创建一个新的 entry - SharedLib* pNewEntry; - pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib)); - pNewEntry->pathName = strdup(pathName); - pNewEntry->handle = handle; - pNewEntry->classLoader = classLoader; - dvmInitMutex(&pNewEntry->onLoadLock); - pthread_cond_init(&pNewEntry->onLoadCond, NULL); - pNewEntry->onLoadThreadId = self->threadId; - - // 尝试添加到列表中 - SharedLib* pActualEntry = addSharedLibEntry(pNewEntry); - - if (pNewEntry != pActualEntry) { - ··· - freeSharedLibEntry(pNewEntry); - return checkOnLoadResult(pActualEntry); - } else { - ··· - bool result = true; - void* vonLoad; - int version; - // 调用该 so 库的 JNI_OnLoad 方法 - vonLoad = dlsym(handle, "JNI_OnLoad"); - if (vonLoad == NULL) { - ··· - } else { - // 调用 JNI_Onload 方法,重写类加载器。 - OnLoadFunc func = (OnLoadFunc)vonLoad; - Object* prevOverride = self->classLoaderOverride; - - self->classLoaderOverride = classLoader; - oldStatus = dvmChangeStatus(self, THREAD_NATIVE); - ··· - version = (*func)(gDvmJni.jniVm, NULL); - dvmChangeStatus(self, oldStatus); - self->classLoaderOverride = prevOverride; - - if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 && - version != JNI_VERSION_1_6) - { - ··· - result = false; - } else { - ··· - } - } - - if (result) - pNewEntry->onLoadResult = kOnLoadOkay; - else - pNewEntry->onLoadResult = kOnLoadFailed; - - pNewEntry->onLoadThreadId = 0; - - // 释放锁资源 - dvmLockMutex(&pNewEntry->onLoadLock); - pthread_cond_broadcast(&pNewEntry->onLoadCond); - dvmUnlockMutex(&pNewEntry->onLoadLock); - return result; - } -} -``` - - - -## CMake 构建 NDK 项目 -> CMake 是一个开源的跨平台工具系列,旨在构建,测试和打包软件,从 Android Studio 2.2 开始,Android Sudio 默认地使用 CMake 与 Gradle 搭配使用来构建原生库。 - -启动方式只需要在 ``app/build.gradle`` 中添加相关: -```groovy -android { - ··· - defaultConfig { - ··· - externalNativeBuild { - cmake { - cppFlags "" - } - } - - ndk { - abiFilters 'arm64-v8a', 'armeabi-v7a' - } - } - ··· - externalNativeBuild { - cmake { - path "CMakeLists.txt" - } - } -} -``` - -然后在对应目录新建一个 ``CMakeLists.txt`` 文件: -```txt -# 定义了所需 CMake 的最低版本 -cmake_minimum_required(VERSION 3.4.1) - -# add_library() 命令用来添加库 -# native-lib 对应着生成的库的名字 -# SHARED 代表为分享库 -# src/main/cpp/native-lib.cpp 则是指明了源文件的路径。 -add_library( # Sets the name of the library. - native-lib - - # Sets the library as a shared library. - SHARED - - # Provides a relative path to your source file(s). - src/main/cpp/native-lib.cpp) - -# find_library 命令添加到 CMake 构建脚本中以定位 NDK 库,并将其路径存储为一个变量。 -# 可以使用此变量在构建脚本的其他部分引用 NDK 库 -find_library( # Sets the name of the path variable. - log-lib - - # Specifies the name of the NDK library that - # you want CMake to locate. - log) - -# 预构建的 NDK 库已经存在于 Android 平台上,因此,无需再构建或将其打包到 APK 中。 -# 由于 NDK 库已经是 CMake 搜索路径的一部分,只需要向 CMake 提供希望使用的库的名称,并将其关联到自己的原生库中 - -# 要将预构建库关联到自己的原生库 -target_link_libraries( # Specifies the target library. - native-lib - - # Links the target library to the log library - # included in the NDK. - ${log-lib}) -··· -``` -- [CMake 命令详细信息文档](https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html) - -## 常用的 Android NDK 原生 API -| 支持 NDK 的 API 级别 | 关键原生 API | 包括 -|--|--|-- -| 3 | Java 原生接口 | #include -| 3 | Android 日志记录 API | #include -| 5 | OpenGL ES 2.0 | #include
#include -| 8 | Android 位图 API | #include -| 9 | OpenSL ES | #include
#include
#include
#include -| 9 | 原生应用 API | #include
#include
#include
··· -| 18 | OpenGL ES 3.0 | #include
#include -| 21 | 原生媒体 API | #include
#include
··· -| 24 | 原生相机 API | #include
#include
··· -| ··· - -# 类加载器 -![](https://img-blog.csdn.net/20161021101447117?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center) - -## 双亲委托模式 -某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。 - -因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子 ClassLoader 再加载一次。如果不使用这种委托模式,那我们就可以随时使用自定义的类来动态替代一些核心的类,存在非常大的安全隐患。 - -## DexPathList -DexClassLoader 重载了 ``findClass`` 方法,在加载类时会调用其内部的 DexPathList 去加载。DexPathList 是在构造 DexClassLoader 时生成的,其内部包含了 DexFile。 - -``DexPathList.java`` -```java -··· -public Class findClass(String name) { - for (Element element : dexElements) { - DexFile dex = element.dexFile; - if (dex != null) { - Class clazz = dex.loadClassBinaryName(name, definingContext); - if (clazz != null) { - return clazz; - } - } - } - return null; -} -··· -``` - diff --git "a/Docs/Android\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" "b/Docs/Android\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" deleted file mode 100644 index bfcc937..0000000 --- "a/Docs/Android\347\237\245\350\257\206\347\202\271\346\261\207\346\200\273.md" +++ /dev/null @@ -1,2726 +0,0 @@ -- [Activity](#activity) - - [生命周期](#生命周期) - - [启动模式](#启动模式) - - [启动过程](#启动过程) -- [Fragment](#fragment) - - [特点](#特点) - - [生命周期](#生命周期-1) - - [与Activity通信](#与activity通信) -- [Service](#service) - - [启动过程](#启动过程-1) - - [绑定过程](#绑定过程) - - [生命周期](#生命周期-2) - - [启用前台服务](#启用前台服务) -- [BroadcastReceiver](#broadcastreceiver) - - [注册过程](#注册过程) -- [ContentProvider](#contentprovider) - - [基本使用](#基本使用) -- [数据存储](#数据存储) -- [View](#view) - - [MeasureSpec](#measurespec) - - [MotionEvent](#motionevent) - - [VelocityTracker](#velocitytracker) - - [GestureDetector](#gesturedetector) - - [Scroller](#scroller) - - [View 的滑动](#view-的滑动) - - [View 的事件分发](#view-的事件分发) - - [在 Activity 中获取某个 View 的宽高](#在-activity-中获取某个-view-的宽高) - - [Draw 的基本流程](#draw-的基本流程) - - [自定义 View](#自定义-view) -- [进程](#进程) - - [进程生命周期](#进程生命周期) - - [多进程](#多进程) - - [进程存活](#进程存活) - - [OOM_ADJ](#oom_adj) - - [进程被杀情况](#进程被杀情况) - - [进程保活方案](#进程保活方案) -- [Parcelable 接口](#parcelable-接口) - - [使用示例](#使用示例) - - [方法说明](#方法说明) - - [Parcelable 与 Serializable 对比](#parcelable-与-serializable-对比) -- [IPC](#ipc) - - [IPC方式](#ipc方式) - - [Binder](#binder) - - [流程](#流程) - - [AIDL 通信](#aidl-通信) - - [Messenger](#messenger) -- [Window / WindowManager](#window--windowmanager) - - [Window 概念与分类](#window-概念与分类) - - [Window 的内部机制](#window-的内部机制) - - [Window 的创建过程](#window-的创建过程) - - [Activity 的 Window 创建过程](#activity-的-window-创建过程) - - [Dialog 的 Window 创建过程](#dialog-的-window-创建过程) - - [Toast 的 Window 创建过程](#toast-的-window-创建过程) -- [Bitmap](#bitmap) - - [配置信息与压缩方式](#配置信息与压缩方式) - - [常用操作](#常用操作) - - [裁剪、缩放、旋转、移动](#裁剪缩放旋转移动) - - [保存与释放](#保存与释放) - - [图片压缩](#图片压缩) - - [BitmapFactory](#bitmapfactory) - - [Bitmap创建流程](#bitmap创建流程) - - [Option类](#option类) - - [基本使用](#基本使用-1) - - [内存回收](#内存回收) -- [屏幕适配](#屏幕适配) - - [单位](#单位) - - [头条适配方案](#头条适配方案) - - [刘海屏适配](#刘海屏适配) -- [Context](#context) -- [SharedPreferences](#sharedpreferences) - - [获取方式](#获取方式) - - [getPreferences](#getpreferences) - - [getDefaultSharedPreferences](#getdefaultsharedpreferences) - - [getSharedPreferences](#getsharedpreferences) - - [架构](#架构) - - [apply / commit](#apply--commit) - - [注意](#注意) -- [消息机制](#消息机制) - - [Handler 机制](#handler-机制) - - [工作原理](#工作原理) - - [ThreadLocal](#threadlocal) - - [MessageQueue](#messagequeue) - - [Looper](#looper) - - [Handler](#handler) -- [线程异步](#线程异步) - - [AsyncTask](#asynctask) - - [基本使用](#基本使用-2) - - [工作原理](#工作原理-1) - - [HandlerThread](#handlerthread) - - [IntentService](#intentservice) - - [线程池](#线程池) -- [RecyclerView 优化](#recyclerview-优化) -- [Webview](#webview) - - [基本使用](#基本使用-3) - - [WebView](#webview-1) - - [WebSettings](#websettings) - - [WebViewClient](#webviewclient) - - [WebChromeClient](#webchromeclient) - - [Webview 加载优化](#webview-加载优化) - - [内存泄漏](#内存泄漏) -# Activity -## 生命周期 -![](http://gityuan.com/images/lifecycle/activity.png) - -- Activity A 启动另一个Activity B,回调如下: -Activity A 的onPause() → Activity B的onCreate() → onStart() → onResume() → Activity A的onStop();如果B是透明主题又或则是个DialogActivity,则不会回调A的onStop; - -- 使用onSaveInstanceState()保存简单,轻量级的UI状态 -```java -lateinit var textView: TextView -var gameState: String? = null - -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - gameState = savedInstanceState?.getString(GAME_STATE_KEY) - setContentView(R.layout.activity_main) - textView = findViewById(R.id.text_view) -} - -override fun onRestoreInstanceState(savedInstanceState: Bundle?) { - textView.text = savedInstanceState?.getString(TEXT_VIEW_KEY) -} - -override fun onSaveInstanceState(outState: Bundle?) { - outState?.run { - putString(GAME_STATE_KEY, gameState) - putString(TEXT_VIEW_KEY, textView.text.toString()) - } - super.onSaveInstanceState(outState) -} -``` -## 启动模式 -| LaunchMode | 说明 -|----------|-----| -| standard | 系统在启动它的任务中创建 activity 的新实例 | -| singleTop | 如果activity的实例已存在于当前任务的顶部,则系统通过调用其onNewIntent(),否则会创建新实例 | -| singleTask | 系统创建新 task 并在 task 的根目录下实例化 activity。但如果 activity 的实例已存在于单独的任务中,则调用其 onNewIntent() 方法,其上面的实例会被移除栈。一次只能存在一个 activity 实例 | -| singleInstance | 相同 singleTask,activity始终是其task的唯一成员; 任何由此开始的activity 都在一个单独的 task 中打开 | -  - - -## 启动过程 -![](https://img-blog.csdn.net/20180427173504903) - -``ActivityThread.java`` -```java -private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { - ... - ActivityInfo aInfo = r.activityInfo; - if (r.packageInfo == null) { - //step 1: 创建LoadedApk对象 - r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo, - Context.CONTEXT_INCLUDE_CODE); - } - ... //component初始化过程 - - java.lang.ClassLoader cl = r.packageInfo.getClassLoader(); - //step 2: 创建Activity对象 - Activity activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent); - ... - - //step 3: 创建Application对象 - Application app = r.packageInfo.makeApplication(false, mInstrumentation); - - if (activity != null) { - //step 4: 创建ContextImpl对象 - Context appContext = createBaseContextForActivity(r, activity); - CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager()); - Configuration config = new Configuration(mCompatConfiguration); - //step5: 将Application/ContextImpl都attach到Activity对象 - activity.attach(appContext, this, getInstrumentation(), r.token, - r.ident, app, r.intent, r.activityInfo, title, r.parent, - r.embeddedID, r.lastNonConfigurationInstances, config, - r.referrer, r.voiceInteractor); - - ... - int theme = r.activityInfo.getThemeResource(); - if (theme != 0) { - activity.setTheme(theme); - } - - activity.mCalled = false; - if (r.isPersistable()) { - //step 6: 执行回调onCreate - mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState); - } else { - mInstrumentation.callActivityOnCreate(activity, r.state); - } - - r.activity = activity; - r.stopped = true; - if (!r.activity.mFinished) { - activity.performStart(); //执行回调onStart - r.stopped = false; - } - if (!r.activity.mFinished) { - //执行回调onRestoreInstanceState - if (r.isPersistable()) { - if (r.state != null || r.persistentState != null) { - mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state, - r.persistentState); - } - } else if (r.state != null) { - mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state); - } - } - ... - r.paused = true; - mActivities.put(r.token, r); - } - - return activity; -} - -``` - -# Fragment -## 特点 -- Fragment 解决 Activity 间的切换不流畅,轻量切换 -- 可以从 startActivityForResult 中接收到返回结果,但是View不能 -- 只能在 Activity 保存其状态(用户离开 Activity)之前使用 commit() 提交事务。如果您试图在该时间点后提交,则会引发异常。 这是因为如需恢复 Activity,则提交后的状态可能会丢失。 对于丢失提交无关紧要的情况,请使用 commitAllowingStateLoss()。 - -## 生命周期 -![](https://developer.android.google.cn/images/fragment_lifecycle.png)![](https://developer.android.google.cn/images/activity_fragment_lifecycle.png) - -## 与Activity通信 -执行此操作的一个好方法是,在片段内定义一个回调接口,并要求宿主 Activity 实现它。 -```java -public static class FragmentA extends ListFragment { - ... - // Container Activity must implement this interface - public interface OnArticleSelectedListener { - public void onArticleSelected(Uri articleUri); - } - ... -} - -public static class FragmentA extends ListFragment { - OnArticleSelectedListener mListener; - ... - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - try { - mListener = (OnArticleSelectedListener) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString()); - } - } - ... -} -``` - -# Service -Service 分为两种工作状态,一种是启动状态,主要用于执行后台计算;另一种是绑定状态,主要用于其他组件和 Service 的交互。 - -## 启动过程 -![](http://gityuan.com/images/android-service/am/Seq_start_service.png) - -``ActivityThread.java`` -```java -@UnsupportedAppUsage -private void handleCreateService(CreateServiceData data) { - ··· - LoadedApk packageInfo = getPackageInfoNoCheck( - data.info.applicationInfo, data.compatInfo); - Service service = null; - try { - java.lang.ClassLoader cl = packageInfo.getClassLoader(); - service = packageInfo.getAppFactory() - .instantiateService(cl, data.info.name, data.intent); - } - ··· - - try { - if (localLOGV) Slog.v(TAG, "Creating service " + data.info.name); - - ContextImpl context = ContextImpl.createAppContext(this, packageInfo); - context.setOuterContext(service); - - Application app = packageInfo.makeApplication(false, mInstrumentation); - service.attach(context, this, data.info.name, data.token, app, - ActivityManager.getService()); - service.onCreate(); - mServices.put(data.token, service); - try { - ActivityManager.getService().serviceDoneExecuting( - data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - ··· -} -``` - -## 绑定过程 -![](http://gityuan.com/images/ams/bind_service.jpg) - -``ActivityThread.java`` -```java -private void handleBindService(BindServiceData data) { - Service s = mServices.get(data.token); - ··· - if (s != null) { - try { - data.intent.setExtrasClassLoader(s.getClassLoader()); - data.intent.prepareToEnterProcess(); - try { - if (!data.rebind) { - IBinder binder = s.onBind(data.intent); - ActivityManager.getService().publishService( - data.token, data.intent, binder); - } else { - s.onRebind(data.intent); - ActivityManager.getService().serviceDoneExecuting( - data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0); - } - } catch (RemoteException ex) { - throw ex.rethrowFromSystemServer(); - } - } - ··· - } -} -``` - -## 生命周期 -![](https://upload-images.jianshu.io/upload_images/944365-cf5c1a9d2dddaaca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/456/format/webp) -| 值 | 说明 | -|-----|-----| -| START_NOT_STICKY | 如果系统在 onStartCommand() 返回后终止服务,则除非有挂起 Intent 要传递,否则系统不会重建服务。这是最安全的选项,可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务 | -| START_STICKY | 如果系统在 onStartCommand() 返回后终止服务,则会重建服务并调用 onStartCommand(),但不会重新传递最后一个 Intent。相反,除非有挂起 Intent 要启动服务(在这种情况下,将传递这些 Intent ),否则系统会通过空 Intent 调用 onStartCommand()。这适用于不执行命令、但无限期运行并等待作业的媒体播放器(或类似服务 | -| START_REDELIVER_INTENT | 如果系统在 onStartCommand() 返回后终止服务,则会重建服务,并通过传递给服务的最后一个 Intent 调用 onStartCommand()。任何挂起 Intent 均依次传递。这适用于主动执行应该立即恢复的作业(例如下载文件)的服务 | - -## 启用前台服务 -```java - -``` -```java -Notification notification = new Notification(icon, text, System.currentTimeMillis()); -Intent notificationIntent = new Intent(this, ExampleActivity.class); -PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); -notification.setLatestEventInfo(this, title, mmessage, pendingIntent); -startForeground(ONGOING_NOTIFICATION_ID, notification); -``` - -# BroadcastReceiver -target 26 之后,无法在 AndroidManifest 显示声明大部分广播,除了一部分必要的广播,如: -- ACTION_BOOT_COMPLETED -- ACTION_TIME_SET -- ACTION_LOCALE_CHANGED -```java -LocalBroadcastManager.getInstance(MainActivity.this).registerReceiver(receiver, filter); -``` - -## 注册过程 -![](http://gityuan.com/images/ams/send_broadcast.jpg) - - -# ContentProvider -ContentProvider 管理对结构化数据集的访问。它们封装数据,并提供用于定义数据安全性的机制。 内容提供程序是连接一个进程中的数据与另一个进程中运行的代码的标准界面。 - -ContentProvider 无法被用户感知,对于一个 ContentProvider 组件来说,它的内部需要实现增删该查这四种操作,它的内部维持着一份数据集合,这个数据集合既可以是数据库实现,也可以是其他任何类型,如 List 和 Map,内部的 insert、delete、update、query 方法需要处理好线程同步,因为这几个方法是在 Binder 线程池中被调用的。 - -ContentProvider 通过 Binder 向其他组件乃至其他应用提供数据。当 ContentProvider 所在的进程启动时,ContentProvider 会同时启动并发布到 AMS 中,需要注意的是,这个时候 ContentProvider 的 onCreate 要先于 Application 的 onCreate 而执行。 - -## 基本使用 -```java -// Queries the user dictionary and returns results -mCursor = getContentResolver().query( - UserDictionary.Words.CONTENT_URI, // The content URI of the words table - mProjection, // The columns to return for each row - mSelectionClause // Selection criteria - mSelectionArgs, // Selection criteria - mSortOrder); // The sort order for the returned rows -``` - -```java -public class Installer extends ContentProvider { - - @Override - public boolean onCreate() { - return true; - } - - @Nullable - @Override - public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { - return null; - } - - @Nullable - @Override - public String getType(@NonNull Uri uri) { - return null; - } - - @Nullable - @Override - public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { - return null; - } - - @Override - public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { - return 0; - } - - @Override - public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { - return 0; - } -} -``` - -> ContentProvider 和 sql 在实现上有什么区别? ->- ContentProvider 屏蔽了数据存储的细节,内部实现透明化,用户只需关心 uri 即可(是否匹配) ->- ContentProvider 能实现不同 app 的数据共享,sql 只能是自己程序才能访问 ->- Contentprovider 还能增删本地的文件,xml等信息 - -# 数据存储 -| 存储方式 | 说明 | -|-----|-----| -| SharedPreferences | 在键值对中存储私有原始数据 | -| 内部存储 | 在设备内存中存储私有数据 | -| 外部存储 | 在共享的外部存储中存储公共数据 | -| SQLite 数据库 | 在私有数据库中存储结构化数据 | - -# View -![](https://user-gold-cdn.xitu.io/2019/6/12/16b4a8a388f3a91a?imageslim) -ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View 的三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联 - -View 的整个绘制流程可以分为以下三个阶段: -- measure: 判断是否需要重新计算 View 的大小,需要的话则计算 -- layout: 判断是否需要重新计算 View 的位置,需要的话则计算 -- draw: 判断是否需要重新绘制 View,需要的话则重绘制 - -![](https://img-blog.csdn.net/20180510164327114) - -## MeasureSpec -MeasureSpec表示的是一个32位的整形值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec 是 View 类的一个静态内部类,用来说明应该如何测量这个 View - -| Mode | 说明 | -|-----|-----| -| UNSPECIFIED | 不指定测量模式, 父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少用到。 | -| EXACTLY | 精确测量模式,视图宽高指定为 match_parent 或具体数值时生效,表示父视图已经决定了子视图的精确大小,这种模式下 View 的测量值就是 SpecSize 的值| -| AT_MOST | 最大值测量模式,当视图的宽高指定为 wrap_content 时生效,此时子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸 | - -对于 DecorView 而言,它的MeasureSpec 由窗口尺寸和其自身的 LayoutParams 共同决定;对于普通的 View,它的 MeasureSpec 由父视图的 MeasureSpec 和其自身的 LayoutParams 共同决定 - -| childLayoutParams/parentSpecMode | EXACTLY | AT_MOST -|--|--|-- -| dp/px | EXACTLY(childSize) | EXACTLY(childSize) -| match_parent | EXACTLY(childSize) | AT_MOST(parentSize) -| wrap_content | AT_MOST(parentSize) | AT_MOST(parentSize) - - -直接继承 View 的控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,因为 View 在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,在这种模式下,它的宽/高等于父容器当前剩余的空间大小,就相当于使用 match_parent。这解决方式如下: -```java -protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - // 在 wrap_content 的情况下指定内部宽/高(mWidth 和 mHeight`) - if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { - setMeasuredDimension(mWidth, mHeight); - } else if (widthSpecMode == MeasureSpec.AT_MOST) { - setMeasureDimension(mWidth, heightSpecSize); - } else if (heightSpecMode == MeasureSpec.AT_MOST) { - setMeasureDimension(widthSpecSize, mHeight); - } -} -``` - -## MotionEvent -| 事件 | 说明 -|-----|------ -| ACTION_DOWN | 手指刚接触到屏幕 -| ACTION_MOVE | 手指在屏幕上移动 -| ACTION_UP | 手机从屏幕上松开的一瞬间 -| ACTION_CANCEL | 触摸事件取消 - -点击屏幕后松开,事件序列为 DOWN -> UP,点击屏幕滑动松开,事件序列为 DOWN -> MOVE -> ...> MOVE -> UP。 - -``getX/getY`` 返回相对于当前View左上角的坐标,`getRawX/getRawY` 返回相对于屏幕左上角的坐标 - -TouchSlop是系统所能识别出的被认为滑动的最小距离,不同设备值可能不相同,可通过 ``ViewConfiguration.get(getContext()).getScaledTouchSlop()`` 获取。 - -## VelocityTracker -**VelocityTracker** 可用于追踪手指在滑动中的速度: -```java -view.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - VelocityTracker velocityTracker = VelocityTracker.obtain(); - velocityTracker.addMovement(event); - velocityTracker.computeCurrentVelocity(1000); - int xVelocity = (int) velocityTracker.getXVelocity(); - int yVelocity = (int) velocityTracker.getYVelocity(); - velocityTracker.clear(); - velocityTracker.recycle(); - return false; - } -}); -``` - -## GestureDetector -**GestureDetector** 辅助检测用户的单击、滑动、长按、双击等行为: -```java -final GestureDetector mGestureDetector = new GestureDetector(this, new GestureDetector.OnGestureListener() { - @Override - public boolean onDown(MotionEvent e) { return false; } - - @Override - public void onShowPress(MotionEvent e) { } - - @Override - public boolean onSingleTapUp(MotionEvent e) { return false; } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; } - - @Override - public void onLongPress(MotionEvent e) { } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; } -}); -mGestureDetector.setOnDoubleTapListener(new OnDoubleTapListener() { - @Override - public boolean onSingleTapConfirmed(MotionEvent e) { return false; } - - @Override - public boolean onDoubleTap(MotionEvent e) { return false; } - - @Override - public boolean onDoubleTapEvent(MotionEvent e) { return false; } -}); -// 解决长按屏幕后无法拖动的问题 -mGestureDetector.setIsLongpressEnabled(false); -imageView.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - return mGestureDetector.onTouchEvent(event); - } -}); -``` -如果是监听滑动相关,建议在 ``onTouchEvent`` 中实现,如果要监听双击,那么就使用 ``GestureDectector``。 - -## Scroller -弹性滑动对象,用于实现 View 的弹性滑动,**Scroller** 本身无法让 View 弹性滑动,需要和 View 的 ``computeScroll`` 方法配合使用。``startScroll`` 方法是无法让 View 滑动的,``invalidate`` 会导致 View 重绘,重绘后会在 ``draw`` 方法中又会去调用 ``computeScroll`` 方法,``computeScroll`` 方法又会去向 Scroller 获取当前的 scrollX 和 scrollY,然后通过 ``scrollTo`` 方法实现滑动,接着又调用 ``postInvalidate`` 方法如此反复。 -```java -Scroller mScroller = new Scroller(mContext); - -private void smoothScrollTo(int destX) { - int scrollX = getScrollX(); - int delta = destX - scrollX; - // 1000ms 内滑向 destX,效果就是慢慢滑动 - mScroller.startScroll(scrollX, 0 , delta, 0, 1000); - invalidate(); -} - -@Override -public void computeScroll() { - if (mScroller.computeScrollOffset()) { - scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); - postInvalidate(); - } -} -``` - -## View 的滑动 -- ``scrollTo/scrollBy`` -适合对 View 内容的滑动。``scrollBy`` 实际上也是调用了 ``scrollTo`` 方法: -```java -public void scrollTo(int x, int y) { - if (mScrollX != x || mScrollY != y) { - int oldX = mScrollX; - int oldY = mScrollY; - mScrollX = x; - mScrollY = y; - invalidateParentCaches(); - onScrollChanged(mScrollX, mScrollY, oldX, oldY); - if (!awakenScrollBars()) { - postInvalidateOnAnimation(); - } - } -} - -public void scrollBy(int x, int y) { - scrollTo(mScrollX + x, mScrollY + y); -} -``` -mScrollX的值等于 View 的左边缘和 View 内容左边缘在水平方向的距离,mScrollY的值等于 View 上边缘和 View 内容上边缘在竖直方向的距离。``scrollTo`` 和 ``scrollBy`` 只能改变 View 内容的位置而不能改变 View 在布局中的位置。 - -- 使用动画 -操作简单,主要适用于没有交互的 View 和实现复杂的动画效果。 -- 改变布局参数 -操作稍微复杂,适用于有交互的 View. -```java -ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); -params.width += 100; -params.leftMargin += 100; -view.requestLayout(); -//或者 view.setLayoutParams(params); -``` - -## View 的事件分发 -点击事件达到顶级 View(一般是一个 ViewGroup),会调用 ViewGroup 的 dispatchTouchEvent 方法,如果顶级 ViewGroup 拦截事件即 onInterceptTouchEvent 返回 true,则事件由 ViewGroup 处理,这时如果 ViewGroup 的 mOnTouchListener 被设置,则 onTouch 会被调用,否则 onTouchEvent 会被调用。也就是说如果都提供的话,onTouch 会屏蔽掉 onTouchEvent。在 onTouchEvent 中,如果设置了 mOnClickListenser,则 onClick 会被调用。如果顶级 ViewGroup 不拦截事件,则事件会传递给它所在的点击事件链上的子 View,这时子 View 的 dispatchTouchEvent 会被调用。如此循环。 - -![](https://user-gold-cdn.xitu.io/2019/7/19/16c08654e36be140?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -![](https://user-gold-cdn.xitu.io/2019/7/19/16c086493dc70018?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -- ViewGroup 默认不拦截任何事件。ViewGroup 的 onInterceptTouchEvent 方法默认返回 false。 - -- View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,onTouchEvent 方法就会被调用。 - -- View 在可点击状态下,onTouchEvent 默认会消耗事件。 - -- ACTION_DOWN 被拦截了,onInterceptTouchEvent 方法执行一次后,就会留下记号(mFirstTouchTarget == null)那么往后的 ACTION_MOVE 和 ACTION_UP 都会拦截。` - - -## 在 Activity 中获取某个 View 的宽高 -- Activity/View#onWindowFocusChanged -``` -// 此时View已经初始化完毕 -// 当Activity的窗口得到焦点和失去焦点时均会被调用一次 -// 如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用 -public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - if (hasFocus) { - int width = view.getMeasureWidth(); - int height = view.getMeasuredHeight(); - } -} -``` -- view.post(runnable) -``` -// 通过post可以将一个runnable投递到消息队列的尾部,// 然后等待Looper调用次runnable的时候,View也已经初 -// 始化好了 -protected void onStart() { - super.onStart(); - view.post(new Runnable() { - - @Override - public void run() { - int width = view.getMeasuredWidth(); - int height = view.getMeasuredHeight(); - } - }); -} -``` -- ViewTreeObserver -```java -// 当View树的状态发生改变或者View树内部的View的可见// 性发生改变时,onGlobalLayout方法将被回调 -protected void onStart() { - super.onStart(); - - ViewTreeObserver observer = view.getViewTreeObserver(); - observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { - - @SuppressWarnings("deprecation") - @Override - public void onGlobalLayout() { - view.getViewTreeObserver().removeGlobalOnLayoutListener(this); - int width = view.getMeasuredWidth(); - int height = view.getMeasuredHeight(); - } - }); -} -``` - -## Draw 的基本流程 -```java -// 绘制基本上可以分为六个步骤 -public void draw(Canvas canvas) { - ... - // 步骤一:绘制View的背景 - drawBackground(canvas); - ... - // 步骤二:如果需要的话,保持canvas的图层,为fading做准备 - saveCount = canvas.getSaveCount(); - ... - canvas.saveLayer(left, top, right, top + length, null, flags); - ... - // 步骤三:绘制View的内容 - onDraw(canvas); - ... - // 步骤四:绘制View的子View - dispatchDraw(canvas); - ... - // 步骤五:如果需要的话,绘制View的fading边缘并恢复图层 - canvas.drawRect(left, top, right, top + length, p); - ... - canvas.restoreToCount(saveCount); - ... - // 步骤六:绘制View的装饰(例如滚动条等等) - onDrawForeground(canvas) -} -``` - -## 自定义 View -- 继承 View 重写 ``onDraw`` 方法 - -主要用于实现一些不规则的效果,静态或者动态地显示一些不规则的图形,即重写 ``onDraw`` 方法。采用这种方式需要自己支持 wrap_content,并且 padding 也需要自己处理。 - -- 继承 ViewGroup 派生特殊的 Layout - -主要用于实现自定义布局,采用这种方式需要合适地处理 ViewGroup 的测量、布局两个过程,并同时处理子元素的测量和布局过程。 - -- 继承特定的 View - -用于扩张某种已有的View的功能 - -- 继承特定的 ViewGroup - -用于扩张某种已有的ViewGroup的功能 - -# 进程 -进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 - -当某个应用组件启动且该应用没有运行其他任何组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件在相同的进程和线程(称为“主”线程)中运行。 - -各类组件元素的清单文件条目````、````、```` 和 ````—均支持 android:process 属性,此属性可以指定该组件应在哪个进程运行。 - -## 进程生命周期 -**1、前台进程** -- 托管用户正在交互的 Activity(已调用 Activity 的 ``onResume()`` 方法) -- 托管某个 Service,后者绑定到用户正在交互的 Activity -- 托管正在“前台”运行的 Service(服务已调用 ``startForeground()``) -- 托管正执行一个生命周期回调的 Service(``onCreate()``、``onStart()`` 或 ``onDestroy()``) -- 托管正执行其 ``onReceive()`` 方法的 BroadcastReceiver - -**2、可见进程** -- 托管不在前台、但仍对用户可见的 Activity(已调用其 ``onPause()`` 方法)。例如,如果 re前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。 -- 托管绑定到可见(或前台)Activity 的 Service - -**3、服务进程** -- 正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。 - -**4、后台进程** -- 包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 ``onStop()`` 方法)。通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。 - -**5、空进程** -- 不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。\ - -## 多进程 -如果注册的四大组件中的任意一个组件时用到了多进程,运行该组件时,都会创建一个新的 Application 对象。对于多进程重复创建 Application 这种情况,只需要在该类中对当前进程加以判断即可。 -```java -public class MyApplication extends Application { - - @Override - public void onCreate() { - Log.d("MyApplication", getProcessName(android.os.Process.myPid())); - super.onCreate(); - } - - /** - * 根据进程 ID 获取进程名 - * @param pid 进程id - * @return 进程名 - */ - public String getProcessName(int pid){ - ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE); - List processInfoList = am.getRunningAppProcesses(); - if (processInfoList == null) { - return null; - } - for (ActivityManager.RunningAppProcessInfo processInfo : processInfoList) { - if (processInfo.pid == pid) { - return processInfo.processName; - } - } - return null; - } -} -``` - ->一般来说,使用多进程会造成以下几个方面的问题: ->- 静态成员和单例模式完全失效 ->- 线程同步机制完全失效 ->- SharedPreferences 的可靠性下降 ->- Application 会多次创建 - -## 进程存活 -### OOM_ADJ -| ADJ级别 | 取值 | 解释 -|-----|-----|------ -| UNKNOWN_ADJ | 16 | 一般指将要会缓存进程,无法获取确定值 -| CACHED_APP_MAX_ADJ | 15 | 不可见进程的adj最大值 -| CACHED_APP_MIN_ADJ | 9 | 不可见进程的adj最小值 -| SERVICE_B_AD | 8 | B List 中的 Service(较老的、使用可能性更小) -| PREVIOUS_APP_ADJ | 7 | 上一个App的进程(往往通过按返回键) -| HOME_APP_ADJ | 6 | Home进程 -| SERVICE_ADJ | 5 | 服务进程(Service process) -| HEAVY_WEIGHT_APP_ADJ | 4 | 后台的重量级进程,system/rootdir/init.rc 文件中设置 -| BACKUP_APP_ADJ | 3 | 备份进程 -| PERCEPTIBLE_APP_ADJ | 2 | 可感知进程,比如后台音乐播放 -| VISIBLE_APP_ADJ | 1 | 可见进程(Visible process) -| FOREGROUND_APP_ADJ | 0 | 前台进程(Foreground process) -| PERSISTENT_SERVICE_ADJ | -11 | 关联着系统或persistent进程 -| PERSISTENT_PROC_ADJ | -12 | 系统 persistent 进程,比如telephony -| SYSTEM_ADJ | -16 | 系统进程 -| NATIVE_ADJ | -17 | native进程(不被系统管理) - -### 进程被杀情况 -![](https://pic3.zhimg.com/80/18b6bfb1bf54433619a7122c3a8e606e_hd.png) - -### 进程保活方案 -- 开启一个像素的 Activity -- 使用前台服务 -- 多进程相互唤醒 -- JobSheduler 唤醒 -- 粘性服务 & 与系统服务捆绑 - -# Parcelable 接口 -只要实现了 Parcelable 接口,一个类的对象就可以实现序列化并可以通过 Intent 和 Binder 传递。 - -## 使用示例 -```java -import android.os.Parcel; -import android.os.Parcelable; - -public class User implements Parcelable { - - private int userId; - - protected User(Parcel in) { - userId = in.readInt(); - } - - public static final Creator CREATOR = new Creator() { - @Override - public User createFromParcel(Parcel in) { - return new User(in); - } - - @Override - public User[] newArray(int size) { - return new User[size]; - } - }; - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(userId); - } - - public int getUserId() { - return userId; - } -} -``` - -## 方法说明 -Parcel 内部包装了可序列化的数据,可以在 Binder 中自由传输。序列化功能由 ``writeToParcel`` 方法完成,最终是通过 Parcel 中的一系列 write 方法完成。反序列化功能由 CREATOR 来完成,通过 Parcel 的一系列 read 方法来完成反序列化过程。 - -| 方法 | 功能 -|--|-- -| createFromParcel(Parcel in) | 从序列化后的对象中创建原始对象 -| newArray(int size) | 创建指定长度的原始对象数组 -| User(Parcel in) | 从序列化后的对象中创建原始对象 -| writeToParcel(Parcel dest, int flags) | 将当前对象写入序列化结构中,其中 flags 标识有两种值:0 或者 1。为 1 时标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况都为 0 -| describeContents | 返回当前对象的内容描述。如果含有文件描述符,返回 1,否则返回 0,几乎所有情况都返回 0 | - -## Parcelable 与 Serializable 对比 -- Serializable 使用 I/O 读写存储在硬盘上,而 Parcelable 是直接在内存中读写 -- Serializable 会使用反射,序列化和反序列化过程需要大量 I/O 操作, Parcelable 自已实现封送和解封(marshalled &unmarshalled)操作不需要用反射,数据也存放在 Native 内存中,效率要快很多 - -# IPC -IPC 即 Inter-Process Communication (进程间通信)。Android 基于 Linux,而 Linux 出于安全考虑,不同进程间不能之间操作对方的数据,这叫做“进程隔离”。 -> 在 Linux 系统中,虚拟内存机制为每个进程分配了线性连续的内存空间,操作系统将这种虚拟内存空间映射到物理内存空间,每个进程有自己的虚拟内存空间,进而不能操作其他进程的内存空间,只有操作系统才有权限操作物理内存空间。 进程隔离保证了每个进程的内存安全。 - -## IPC方式 -| 名称 | 优点 | 缺点 | 适用场景 -|----|-----|----|---- -| Bundle | 简单易用 | 只能传输 Bundle 支持的数据类型 | 四大组件间的进程间通信 -| 文件共享 | 简单易用 | 不适合高并发场景,并且无法做到进程间即时通信|无并发访问情形,交换简单的数据实时性不高的场景 -| AIDL |功能强大,支持一对多并发通信,支持实时通信 | 使用稍复杂,需要处理好线程同步 | 一对多通信且有 RPC 需求 -| Messenger | 功能一般,支持一对多串行通信,支持实时通信|不能很处理高并发清醒,不支持 RPC,数据通过 Message 进行传输,因此只能传输 Bundle 支持的数据类型|低并发的一对多即时通信,无RPC需求,或者无需返回结果的RPC需求 -| ContentProvider | 在数据源访问方面功能强大,支持一对多并发数据共享,可通过 Call 方法扩展其他操作|可以理解为受约束的 AIDL,主要提供数据源的 CRUD 操作 | 一对多的进程间数据共享 -| Socket | 可以通过网络传输字节流,支持一对多并发实时通信 | 实现细节稍微有点烦琐,不支持直接的RPC | 网络数据交换 - -## Binder -Binder 是 Android 中的一个类,实现了 IBinder 接口。从 IPC 角度来说,Binder 是 Android 中的一种扩进程通信方方式。从 Android 应用层来说,Binder 是客户端和服务器端进行通信的媒介,当 bindService 的时候,服务端会返回一个包含了服务端业务调用的 Binder 对象。 - -Binder 相较于传统 IPC 来说更适合于Android系统,具体原因的包括如下三点: -- Binder 本身是 C/S 架构的,这一点更符合 Android 系统的架构 -- 性能上更有优势:管道,消息队列,Socket 的通讯都需要两次数据拷贝,而 Binder 只需要一次。要知道,对于系统底层的 IPC 形式,少一次数据拷贝,对整体性能的影响是非常之大的 -- 安全性更好:传统 IPC 形式,无法得到对方的身份标识(UID/GID),而在使用 Binder IPC 时,这些身份标示是跟随调用过程而自动传递的。Server 端很容易就可以知道 Client 端的身份,非常便于做安全检查 - -示例: - -- **新建AIDL接口文件** - -``RemoteService.aidl`` -```java -package com.example.mystudyapplication3; - -interface IRemoteService { - - int getUserId(); - -} -``` - -系统会自动生成 ``IRemoteService.java``: - -```java -/* - * This file is auto-generated. DO NOT MODIFY. - */ -package com.example.mystudyapplication3; -// Declare any non-default types here with import statements -//import com.example.mystudyapplication3.IUserBean; - -public interface IRemoteService extends android.os.IInterface { - /** - * Local-side IPC implementation stub class. - */ - public static abstract class Stub extends android.os.Binder implements com.example.mystudyapplication3.IRemoteService { - private static final java.lang.String DESCRIPTOR = "com.example.mystudyapplication3.IRemoteService"; - - /** - * Construct the stub at attach it to the interface. - */ - public Stub() { - this.attachInterface(this, DESCRIPTOR); - } - - /** - * Cast an IBinder object into an com.example.mystudyapplication3.IRemoteService interface, - * generating a proxy if needed. - */ - public static com.example.mystudyapplication3.IRemoteService asInterface(android.os.IBinder obj) { - if ((obj == null)) { - return null; - } - android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); - if (((iin != null) && (iin instanceof com.example.mystudyapplication3.IRemoteService))) { - return ((com.example.mystudyapplication3.IRemoteService) iin); - } - return new com.example.mystudyapplication3.IRemoteService.Stub.Proxy(obj); - } - - @Override - public android.os.IBinder asBinder() { - return this; - } - - @Override - public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { - java.lang.String descriptor = DESCRIPTOR; - switch (code) { - case INTERFACE_TRANSACTION: { - reply.writeString(descriptor); - return true; - } - case TRANSACTION_getUserId: { - data.enforceInterface(descriptor); - int _result = this.getUserId(); - reply.writeNoException(); - reply.writeInt(_result); - return true; - } - default: { - return super.onTransact(code, data, reply, flags); - } - } - } - - private static class Proxy implements com.example.mystudyapplication3.IRemoteService { - private android.os.IBinder mRemote; - - Proxy(android.os.IBinder remote) { - mRemote = remote; - } - - @Override - public android.os.IBinder asBinder() { - return mRemote; - } - - public java.lang.String getInterfaceDescriptor() { - return DESCRIPTOR; - } - - @Override - public int getUserId() throws android.os.RemoteException { - android.os.Parcel _data = android.os.Parcel.obtain(); - android.os.Parcel _reply = android.os.Parcel.obtain(); - int _result; - try { - _data.writeInterfaceToken(DESCRIPTOR); - mRemote.transact(Stub.TRANSACTION_getUserId, _data, _reply, 0); - _reply.readException(); - _result = _reply.readInt(); - } finally { - _reply.recycle(); - _data.recycle(); - } - return _result; - } - } - - static final int TRANSACTION_getUserId = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); - } - - public int getUserId() throws android.os.RemoteException; -} -``` - -| 方法 | 含义 -|--|-- -| DESCRIPTOR | Binder 的唯一标识,一般用当前的 Binder 的类名表示 -| asInterface(IBinder obj) | 将服务端的 Binder 对象成客户端所需的 AIDL 接口类型对象,这种转换过程是区分进程的,如果位于同一进程,返回的就是 Stub 对象本身,否则返回的是系统封装后的 Stub.proxy 对象。 -| asBinder | 用于返回当前 Binder 对象 -| onTransact | 运行在服务端中的 Binder 线程池中,远程请求会通过系统底层封装后交由此方法来处理 - -| 定向 tag | 含义 -|--|-- -| in | 数据只能由客户端流向服务端,服务端将会收到客户端对象的完整数据,客户端对象不会因为服务端对传参的修改而发生变动。 -| out | 数据只能由服务端流向客户端,服务端将会收到客户端对象,该对象不为空,但是它里面的字段为空,但是在服务端对该对象作任何修改之后客户端的传参对象都会同步改动。 -| inout | 服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。 - -### 流程 -![](http://gityuan.com/images/binder/binder_start_service/binder_ipc_arch.jpg) - -## AIDL 通信 -Android Interface Definition Language - -使用示例: -- **新建AIDL接口文件** -```java -// RemoteService.aidl -package com.example.mystudyapplication3; - -interface IRemoteService { - - int getUserId(); - -} -``` -- **创建远程服务** -```java -public class RemoteService extends Service { - - private int mId = -1; - - private Binder binder = new IRemoteService.Stub() { - - @Override - public int getUserId() throws RemoteException { - return mId; - } - }; - - @Nullable - @Override - public IBinder onBind(Intent intent) { - mId = 1256; - return binder; - } -} -``` -- **声明远程服务** -```java - -``` -- **绑定远程服务** -```java -public class MainActivity extends AppCompatActivity { - - public static final String TAG = "wzq"; - - IRemoteService iRemoteService; - private ServiceConnection mConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - iRemoteService = IRemoteService.Stub.asInterface(service); - try { - Log.d(TAG, String.valueOf(iRemoteService.getUserId())); - } catch (RemoteException e) { - e.printStackTrace(); - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - iRemoteService = null; - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - bindService(new Intent(MainActivity.this, RemoteService.class), mConnection, Context.BIND_AUTO_CREATE); - } -} -``` - -## Messenger -Messenger可以在不同进程中传递 Message 对象,在Message中放入我们需要传递的数据,就可以轻松地实现数据的进程间传递了。Messenger 是一种轻量级的 IPC 方案,底层实现是 AIDL。 - -# Window / WindowManager -## Window 概念与分类 -Window 是一个抽象类,它的具体实现是 PhoneWindow。WindowManager 是外界访问 Window 的入口,Window 的具体实现位于 WindowManagerService 中,WindowManager 和 WindowManagerService 的交互是一个 IPC 过程。Android 中所有的视图都是通过 Window 来呈现,因此 Window 实际是 View 的直接管理者。 - -| Window 类型 | 说明 | 层级 -|--|--|-- -| Application Window | 对应着一个 Activity | 1~99 -| Sub Window | 不能单独存在,只能附属在父 Window 中,如 Dialog 等 | 1000~1999 -| System Window | 需要权限声明,如 Toast 和 系统状态栏等 | 2000~2999 - -## Window 的内部机制 -Window 是一个抽象的概念,每一个 Window 对应着一个 View 和一个 ViewRootImpl。Window 实际是不存在的,它是以 View 的形式存在。对 Window 的访问必须通过 WindowManager,WindowManager 的实现类是 WindowManagerImpl: - -``WindowManagerImpl.java`` -```java -@Override -public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { - applyDefaultToken(params); - mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow); -} - -@Override -public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { - applyDefaultToken(params); - mGlobal.updateViewLayout(view, params); -} - -@Override -public void removeView(View view) { - mGlobal.removeView(view, false); -} -``` - -WindowManagerImpl 没有直接实现 Window 的三大操作,而是全部交给 WindowManagerGlobal 处理,WindowManagerGlobal 以工厂的形式向外提供自己的实例: - -``WindowManagerGlobal.java`` -```java -// 添加 -public void addView(View view, ViewGroup.LayoutParams params, - Display display, Window parentWindow) { - ··· - // 子 Window 的话需要调整一些布局参数 - final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params; - if (parentWindow != null) { - parentWindow.adjustLayoutParamsForSubWindow(wparams); - } else { - ··· - } - ViewRootImpl root; - View panelParentView = null; - synchronized (mLock) { - // 新建一个 ViewRootImpl,并通过其 setView 来更新界面完成 Window 的添加过程 - ··· - root = new ViewRootImpl(view.getContext(), display); - view.setLayoutParams(wparams); - mViews.add(view); - mRoots.add(root); - mParams.add(wparams); - // do this last because it fires off messages to start doing things - try { - root.setView(view, wparams, panelParentView); - } catch (RuntimeException e) { - // BadTokenException or InvalidDisplayException, clean up. - if (index >= 0) { - removeViewLocked(index, true); - } - throw e; - } - } -} - -// 删除 -@UnsupportedAppUsage -public void removeView(View view, boolean immediate) { - ··· - synchronized (mLock) { - int index = findViewLocked(view, true); - View curView = mRoots.get(index).getView(); - removeViewLocked(index, immediate); - ··· - } -} - -private void removeViewLocked(int index, boolean immediate) { - ViewRootImpl root = mRoots.get(index); - View view = root.getView(); - if (view != null) { - InputMethodManager imm = InputMethodManager.getInstance(); - if (imm != null) { - imm.windowDismissed(mViews.get(index).getWindowToken()); - } - } - boolean deferred = root.die(immediate); - if (view != null) { - view.assignParent(null); - if (deferred) { - mDyingViews.add(view); - } - } -} - -// 更新 -public void updateViewLayout(View view, ViewGroup.LayoutParams params) { - ··· - final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; - view.setLayoutParams(wparams); - synchronized (mLock) { - int index = findViewLocked(view, true); - ViewRootImpl root = mRoots.get(index); - mParams.remove(index); - mParams.add(index, wparams); - root.setLayoutParams(wparams, false); - } -} -``` - -在 ViewRootImpl 中最终会通过 WindowSession 来完成 Window 的添加、更新、删除工作,mWindowSession 的类型是 IWindowSession,是一个 Binder 对象,真正地实现类是 Session,是一个 IPC 过程。 - -## Window 的创建过程 -### Activity 的 Window 创建过程 -在 Activity 的创建过程中,最终会由 ActivityThread 的 performLaunchActivity() 来完成整个启动过程,该方法内部会通过类加载器创建 Activity 的实例对象,并调用 attach 方法关联一系列上下文环境变量。在 Activity 的 attach 方法里,系统会创建所属的 Window 对象并设置回调接口,然后在 Activity 的 setContentView 方法中将视图附属在 Window 上: - -``Activity.java`` -```java -final void attach(Context context, ActivityThread aThread, - Instrumentation instr, IBinder token, int ident, - Application application, Intent intent, ActivityInfo info, - CharSequence title, Activity parent, String id, - NonConfigurationInstances lastNonConfigurationInstances, - Configuration config, String referrer, IVoiceInteractor voiceInteractor, - Window window, ActivityConfigCallback activityConfigCallback) { - attachBaseContext(context); - - mFragments.attachHost(null /*parent*/); - - mWindow = new PhoneWindow(this, window, activityConfigCallback); - mWindow.setWindowControllerCallback(this); - mWindow.setCallback(this); - mWindow.setOnWindowDismissedCallback(this); - mWindow.getLayoutInflater().setPrivateFactory(this); - if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) { - mWindow.setSoftInputMode(info.softInputMode); - } - if (info.uiOptions != 0) { - mWindow.setUiOptions(info.uiOptions); - } - ··· -} -··· - -public void setContentView(@LayoutRes int layoutResID) { - getWindow().setContentView(layoutResID); - initWindowDecorActionBar(); -} - -``` - -``PhoneWindow.java`` -```java -@Override -public void setContentView(int layoutResID) { - if (mContentParent == null) { // 如果没有 DecorView,就创建 - installDecor(); - } else { - mContentParent.removeAllViews(); - } - mLayoutInflater.inflate(layoutResID, mContentParent); - final Callback cb = getCallback(); - if (cb != null && !isDestroyed()) { - // 回调 Activity 的 onContentChanged 方法通知 Activity 视图已经发生改变 - cb.onContentChanged(); - } -} -``` - -这个时候 DecorView 还没有被 WindowManager 正式添加。在 ActivityThread 的 handleResumeActivity 方法中,首先会调用 Activity 的 onResume 方法,接着调用 Activity 的 makeVisible(),完成 DecorView 的添加和显示过程: - -``Activity.java`` -```java -void makeVisible() { - if (!mWindowAdded) { - ViewManager wm = getWindowManager(); - wm.addView(mDecor, getWindow().getAttributes()); - mWindowAdded = true; - } - mDecor.setVisibility(View.VISIBLE); -} -``` - -### Dialog 的 Window 创建过程 -Dialog 的 Window 的创建过程和 Activity 类似,创建同样是通过 PolicyManager 的 makeNewWindow 方法完成的,创建后的对象实际就是 PhoneWindow。当 Dialog 被关闭时,会通过 WindowManager 来移除 DecorView:mWindowManager.removeViewImmediate(mDecor)。 - -``Dialog.java`` -```java -Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) { - ··· - mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - - final Window w = new PhoneWindow(mContext); - mWindow = w; - w.setCallback(this); - w.setOnWindowDismissedCallback(this); - w.setOnWindowSwipeDismissedCallback(() -> { - if (mCancelable) { - cancel(); - } - }); - w.setWindowManager(mWindowManager, null, null); - w.setGravity(Gravity.CENTER); - - mListenersHandler = new ListenersHandler(this); -} -``` - -普通 Dialog 必须采用 Activity 的 Context,采用 Application 的 Context 就会报错,是因为应用 token 所导致,应用 token 一般只有 Activity 拥有。系统 Window 比较特殊,不需要 token。 - -### Toast 的 Window 创建过程 -Toast 属于系统 Window ,由于其具有定时取消功能,所以系统采用了 Handler。Toast 的内部有两类 IPC 过程,第一类是 Toast 访问 NotificationManagerService,第二类是 NotificationManagerService 回调 Toast 里的 TN 接口。 - -Toast 内部的视图由两种方式,一种是系统默认的样式,另一种是 setView 指定一个自定义 View,它们都对应 Toast 的一个内部成员 mNextView。 - -``Toast.java`` -```java -public void show() { - if (mNextView == null) { - throw new RuntimeException("setView must have been called"); - } - - INotificationManager service = getService(); - String pkg = mContext.getOpPackageName(); - TN tn = mTN; - tn.mNextView = mNextView; - - try { - service.enqueueToast(pkg, tn, mDuration); - } catch (RemoteException e) { - // Empty - } -} -··· - -public void cancel() { - mTN.cancel(); -} - -``` - -``NotificationManagerService.java`` -```java -private void showNextToastLocked() { - ToastRecord record = mToastQueue.get(0); - while (record != null) { - if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback); - try { - record.callback.show(); - scheduleTimeoutLocked(record, false); - return; - } catch (RemoteException e) { - Slog.w(TAG, "Object died trying to show notification " + record.callback - + " in package " + record.pkg); - // remove it from the list and let the process die - int index = mToastQueue.indexOf(record); - if (index >= 0) { - mToastQueue.remove(index); - } - keepProcessAliveLocked(record.pid); - if (mToastQueue.size() > 0) { - record = mToastQueue.get(0); - } else { - record = null; - } - } - } -} - -··· -private void scheduleTimeoutLocked(ToastRecord r, boolean immediate) -{ - Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r); - long delay = immediate ? 0 : (r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY); - mHandler.removeCallbacksAndMessages(r); - mHandler.sendMessageDelayed(m, delay); -} -``` - -# Bitmap -![](https://upload-images.jianshu.io/upload_images/2618044-cd996dd172cce293.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000/format/webp) - -## 配置信息与压缩方式 -**Bitmap 中有两个内部枚举类:** -- Config 是用来设置颜色配置信息 -- CompressFormat 是用来设置压缩方式 - -| Config | 单位像素所占字节数 | 解析 -|-------|-------|------ -| Bitmap.Config.ALPHA_8 | 1 | 颜色信息只由透明度组成,占8位 -| Bitmap.Config.ARGB_4444 | 2 |颜色信息由rgba四部分组成,每个部分都占4位,总共占16位 -| Bitmap.Config.ARGB_8888 | 4 |颜色信息由rgba四部分组成,每个部分都占8位,总共占32位。是Bitmap默认的颜色配置信息,也是最占空间的一种配置 -| Bitmap.Config.RGB_565 | 2 | 颜色信息由rgb三部分组成,R占5位,G占6位,B占5位,总共占16位 -| RGBA_F16 | 8 | Android 8.0 新增(更丰富的色彩表现HDR) -| HARDWARE | Special | Android 8.0 新增 (Bitmap直接存储在graphic memory) - -> 通常我们优化 Bitmap 时,当需要做性能优化或者防止 OOM,我们通常会使用 Bitmap.Config.RGB_565 这个配置,因为 Bitmap.Config.ALPHA_8 只有透明度,显示一般图片没有意义,Bitmap.Config.ARGB_4444 显示图片不清楚, Bitmap.Config.ARGB_8888 占用内存最多。 - -| CompressFormat | 解析 -|-------|------- -| Bitmap.CompressFormat.JPEG | 表示以 JPEG 压缩算法进行图像压缩,压缩后的格式可以是 ``.jpg`` 或者 ``.jpeg``,是一种有损压缩 | -| Bitmap.CompressFormat.PNG | 颜色信息由 rgba 四部分组成,每个部分都占 4 位,总共占 16 位 | -| Bitmap.Config.ARGB_8888 | 颜色信息由 rgba 四部分组成,每个部分都占 8 位,总共占 32 位。是 Bitmap 默认的颜色配置信息,也是最占空间的一种配置 -| Bitmap.Config.RGB_565 | 颜色信息由 rgb 三部分组成,R 占 5 位,G 占 6 位,B 占 5 位,总共占 16 位 - -## 常用操作 -### 裁剪、缩放、旋转、移动 -```java -Matrix matrix = new Matrix(); -// 缩放 -matrix.postScale(0.8f, 0.9f); -// 左旋,参数为正则向右旋 -matrix.postRotate(-45); -// 平移, 在上一次修改的基础上进行再次修改 set 每次操作都是最新的 会覆盖上次的操作 -matrix.postTranslate(100, 80); -// 裁剪并执行以上操作 -Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true); -```` -> 虽然Matrix还可以调用postSkew方法进行倾斜操作,但是却不可以在此时创建Bitmap时使用。 - -### Bitmap与Drawable转换 -```java -// Drawable -> Bitmap -public static Bitmap drawableToBitmap(Drawable drawable) { - Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(); - drawable.draw(canvas); - return bitmap; -} - -// Bitmap -> Drawable -public static Drawable bitmapToDrawable(Resources resources, Bitmap bm) { - Drawable drawable = new BitmapDrawable(resources, bm); - return drawable; -} -``` - -### 保存与释放 -```java -Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test); -File file = new File(getFilesDir(),"test.jpg"); -if(file.exists()){ - file.delete(); -} -try { - FileOutputStream outputStream=new FileOutputStream(file); - bitmap.compress(Bitmap.CompressFormat.JPEG,90,outputStream); - outputStream.flush(); - outputStream.close(); -} catch (FileNotFoundException e) { - e.printStackTrace(); -} catch (IOException e) { - e.printStackTrace(); -} -//释放bitmap的资源,这是一个不可逆转的操作 -bitmap.recycle(); -``` - -### 图片压缩 -```java -public static Bitmap compressImage(Bitmap image) { - if (image == null) { - return null; - } - ByteArrayOutputStream baos = null; - try { - baos = new ByteArrayOutputStream(); - image.compress(Bitmap.CompressFormat.JPEG, 100, baos); - byte[] bytes = baos.toByteArray(); - ByteArrayInputStream isBm = new ByteArrayInputStream(bytes); - Bitmap bitmap = BitmapFactory.decodeStream(isBm); - return bitmap; - } catch (OutOfMemoryError e) { - e.printStackTrace(); - } finally { - try { - if (baos != null) { - baos.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - return null; -} -``` - -## BitmapFactory -### Bitmap创建流程 -![](https://upload-images.jianshu.io/upload_images/2618044-9c2046ca5054da05.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000/format/webp) - - -### Option类 -| 常用方法 | 说明 -|-----|------ -| boolean inJustDecodeBounds | 如果设置为true,不获取图片,不分配内存,但会返回图片的高度宽度信息 -| int inSampleSize | 图片缩放的倍数 -| int outWidth | 获取图片的宽度值 -| int outHeight | 获取图片的高度值 -| int inDensity | 用于位图的像素压缩比 -| int inTargetDensity | 用于目标位图的像素压缩比(要生成的位图) -| byte[] inTempStorage | 创建临时文件,将图片存储 -| boolean inScaled | 设置为true时进行图片压缩,从inDensity到inTargetDensity -| boolean inDither | 如果为true,解码器尝试抖动解码 -| Bitmap.Config inPreferredConfig | 设置解码器这个值是设置色彩模式,默认值是ARGB_8888,在这个模式下,一个像素点占用4bytes空间,一般对透明度不做要求的话,一般采用RGB_565模式,这个模式下一个像素点占用2bytes -| String outMimeType | 设置解码图像 -| boolean inPurgeable | 当存储Pixel的内存空间在系统内存不足时是否可以被回收 -| boolean inInputShareable | inPurgeable为true情况下才生效,是否可以共享一个InputStream -| boolean inPreferQualityOverSpeed | 为true则优先保证Bitmap质量其次是解码速度 -| boolean inMutable | 配置Bitmap是否可以更改,比如:在Bitmap上隔几个像素加一条线段 -| int inScreenDensity | 当前屏幕的像素密度 - -### 基本使用 -```java -try { - FileInputStream fis = new FileInputStream(filePath); - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - // 设置inJustDecodeBounds为true后,再使用decodeFile()等方法,并不会真正的分配空间,即解码出来的Bitmap为null,但是可计算出原始图片的宽度和高度,即options.outWidth和options.outHeight - BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options); - float srcWidth = options.outWidth; - float srcHeight = options.outHeight; - int inSampleSize = 1; - - if (srcHeight > height || srcWidth > width) { - if (srcWidth > srcHeight) { - inSampleSize = Math.round(srcHeight / height); - } else { - inSampleSize = Math.round(srcWidth / width); - } - } - - options.inJustDecodeBounds = false; - options.inSampleSize = inSampleSize; - - return BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options); -} catch (Exception e) { - e.printStackTrace(); -} -``` - -## 内存回收 -```java -if(bitmap != null && !bitmap.isRecycled()){ - // 回收并且置为null - bitmap.recycle(); - bitmap = null; -} -``` -Bitmap 类的构造方法都是私有的,所以开发者不能直接 new 出一个 Bitmap 对象,只能通过 BitmapFactory 类的各种静态方法来实例化一个 Bitmap。仔细查看 BitmapFactory 的源代码可以看到,生成 Bitmap 对象最终都是通过 JNI 调用方式实现的。所以,加载 Bitmap 到内存里以后,是包含两部分内存区域的。简单的说,一部分是Java 部分的,一部分是 C 部分的。这个 Bitmap 对象是由 Java 部分分配的,不用的时候系统就会自动回收了,但是那个对应的 C 可用的内存区域,虚拟机是不能直接回收的,这个只能调用底层的功能释放。所以需要调用 recycle() 方法来释放 C 部分的内存。从 Bitmap 类的源代码也可以看到,recycle() 方法里也的确是调用了 JNI 方法了的。 - -# 屏幕适配 -## 单位 -- dpi -每英寸像素数(dot per inch) - -- dp -密度无关像素 - 一种基于屏幕物理密度的抽象单元。 这些单位相对于 160 dpi 的屏幕,因此一个 dp 是 160 dpi 屏幕上的一个 px。 dp 与像素的比率将随着屏幕密度而变化,但不一定成正比。为不同设备的 UI 元素的实际大小提供了一致性。 - -- sp -与比例无关的像素 - 这与 dp 单位类似,但它也可以通过用户的字体大小首选项进行缩放。建议在指定字体大小时使用此单位,以便根据屏幕密度和用户偏好调整它们。 -``` -dpi = px / inch - -density = dpi / 160 - -dp = px / density -``` -## 头条适配方案 -```java -private static void setCustomDensity(@NonNull Activity activity, @NonNull final Application application) { - final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics(); - if (sNoncompatDensity == 0) { - sNoncompatDensity = appDisplayMetrics.density; - sNoncompatScaledDensity = appDisplayMetrics.scaledDensity; - // 监听字体切换 - application.registerComponentCallbacks(new ComponentCallbacks() { - @Override - public void onConfigurationChanged(Configuration newConfig) { - if (newConfig != null && newConfig.fontScale > 0) { - sNoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity; - } - } - - @Override - public void onLowMemory() { - - } - }); - } - - // 适配后的dpi将统一为360dpi - final float targetDensity = appDisplayMetrics.widthPixels / 360; - final float targetScaledDensity = targetDensity * (sNoncompatScaledDensity / sNoncompatDensity); - final int targetDensityDpi = (int)(160 * targetDensity); - - appDisplayMetrics.density = targetDensity; - appDisplayMetrics.scaledDensity = targetScaledDensity; - appDisplayMetrics.densityDpi = targetDensityDpi; - - final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics(); - activityDisplayMetrics.density = targetDensity; - activityDisplayMetrics.scaledDensity = targetScaledDensity; - activityDisplayMetrics.densityDpi = targetDensityDpi -} -``` - -## 刘海屏适配 -- Android P 刘海屏适配方案 - -Android P 支持最新的全面屏以及为摄像头和扬声器预留空间的凹口屏幕。通过全新的 DisplayCutout 类,可以确定非功能区域的位置和形状,这些区域不应显示内容。要确定这些凹口屏幕区域是否存在及其位置,使用 getDisplayCutout() 函数。 - -| DisplayCutout 类方法 | 说明 -|--|-- -| getBoundingRects() | 返回Rects的列表,每个Rects都是显示屏上非功能区域的边界矩形 -| getSafeInsetLeft () | 返回安全区域距离屏幕左边的距离,单位是px -| getSafeInsetRight () | 返回安全区域距离屏幕右边的距离,单位是px -| getSafeInsetTop () | 返回安全区域距离屏幕顶部的距离,单位是px -| getSafeInsetBottom() | 返回安全区域距离屏幕底部的距离,单位是px - -Android P 中 WindowManager.LayoutParams 新增了一个布局参数属性 layoutInDisplayCutoutMode: - -| 模式 | 模式说明 -|--|-- -| LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT | 只有当DisplayCutout完全包含在系统栏中时,才允许窗口延伸到DisplayCutout区域。 否则,窗口布局不与DisplayCutout区域重叠。 -| LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER | 该窗口决不允许与DisplayCutout区域重叠。 -| LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES | 该窗口始终允许延伸到屏幕短边上的DisplayCutout区域。 - -- Android P 之前的刘海屏适配 - -不同厂商的刘海屏适配方案不尽相同,需分别查阅各自的开发者文档。 - -# Context -Context 本身是一个抽象类,是对一系列系统服务接口的封装,包括:内部资源、包、类加载、I/O操作、权限、主线程、IPC 和组件启动等操作的管理。ContextImpl, Activity, Service, Application 这些都是 Context 的直接或间接子类, 关系如下: - -![](http://gityuan.com/images/context/context.jpg) - -ContextWrapper是代理Context的实现,简单地将其所有调用委托给另一个Context(mBase)。 - -Application、Activity、Service通过``attach() ``调用父类ContextWrapper的``attachBaseContext()``, 从而设置父类成员变量 mBase 为 ContextImpl 对象, ContextWrapper 的核心工作都是交给 mBase(ContextImpl) 来完成,这样可以子类化 Context 以修改行为而无需更改原始 Context。 - -# SharedPreferences -SharedPreferences 采用key-value(键值对)形式, 主要用于轻量级的数据存储, 尤其适合保存应用的配置参数, 但不建议使用 SharedPreferences 来存储大规模的数据, 可能会降低性能. - -SharedPreferences采用xml文件格式来保存数据, 该文件所在目录位于 ``/data/data//shared_prefs``,如: -```xml - - - https://github.com/JasonWu1111/Android-Review - -``` - -从Android N开始, 创建的 SP 文件模式, 不允许 ``MODE_WORLD_READABLE`` 和 ``MODE_WORLD_WRITEABLE`` 模块, 否则会直接抛出异常 SecurityException。 ``MODE_MULTI_PROCESS`` 这种多进程的方式也是 Google 不推荐的方式, 后续同样会不再支持。 - -当设置 MODE_MULTI_PROCESS 模式, 则每次 getSharedPreferences 过程, 会检查 SP 文件上次修改时间和文件大小, 一旦所有修改则会重新从磁盘加载文件。 - -## 获取方式 -### getPreferences -Activity.getPreferences(mode): 以当前 Activity 的类名作为 SP 的文件名. 即 xxxActivity.xml -``Activity.java`` -```java -public SharedPreferences getPreferences(int mode) { - return getSharedPreferences(getLocalClassName(), mode); -} -``` - -### getDefaultSharedPreferences -PreferenceManager.getDefaultSharedPreferences(Context): 以包名加上 _preferences 作为文件名, 以 MODE_PRIVATE 模式创建 SP 文件. 即 packgeName_preferences.xml. -```java -public static SharedPreferences getDefaultSharedPreferences(Context context) { - return context.getSharedPreferences(getDefaultSharedPreferencesName(context), - getDefaultSharedPreferencesMode()); -} -``` - -### getSharedPreferences -直接调用 Context.getSharedPreferences(name, mode),所有的方法最终都是调用到如下方法: -```java -class ContextImpl extends Context { - private ArrayMap mSharedPrefsPaths; - - public SharedPreferences getSharedPreferences(String name, int mode) { - File file; - synchronized (ContextImpl.class) { - if (mSharedPrefsPaths == null) { - mSharedPrefsPaths = new ArrayMap<>(); - } - //先从mSharedPrefsPaths查询是否存在相应文件 - file = mSharedPrefsPaths.get(name); - if (file == null) { - //如果文件不存在, 则创建新的文件 - file = getSharedPreferencesPath(name); - mSharedPrefsPaths.put(name, file); - } - } - - return getSharedPreferences(file, mode); - } -} -``` - -## 架构 -![](http://gityuan.com/images/sp/shared_preference.jpg) - -SharedPreferences 与 Editor 只是两个接口. SharedPreferencesImpl 和 EditorImpl 分别实现了对应接口。另外, ContextImpl 记录着 SharedPreferences 的重要数据。 - -``putxxx()`` 操作把数据写入到EditorImpl.mModified; - -``apply()/commit()`` 操作先调用 commitToMemory(), 将数据同步到 SharedPreferencesImpl 的 mMap, 并保存到 MemoryCommitResult 的 mapToWriteToDisk,再调用 enqueueDiskWrite(), 写入到磁盘文件; 先之前把原有数据保存到 .bak 为后缀的文件,用于在写磁盘的过程出现任何异常可恢复数据; - -``getxxx()`` 操作从 SharedPreferencesImpl.mMap 读取数据. - -## apply / commit -- apply 没有返回值, commit 有返回值能知道修改是否提交成功 -- apply 是将修改提交到内存,再异步提交到磁盘文件,而 commit 是同步的提交到磁盘文件 -- 多并发的提交 commit 时,需等待正在处理的 commit 数据更新到磁盘文件后才会继续往下执行,从而降低效率; 而 apply 只是原子更新到内存,后调用 apply 函数会直接覆盖前面内存数据,从一定程度上提高很多效率。 - -## 注意 -- 强烈建议不要在 sp 里面存储特别大的 key/value,有助于减少卡顿 / anr -- 不要高频地使用 apply,尽可能地批量提交 -- 不要使用 MODE_MULTI_PROCESS -- 高频写操作的 key 与高频读操作的 key 可以适当地拆分文件,由于减少同步锁竞争 -- 不要连续多次 edit(),应该获取一次获取 edit(),然后多次执行 putxxx(),减少内存波动 - -# 消息机制 -## Handler 机制 -Handler 有两个主要用途:(1)安排 Message 和 runnables 在将来的某个时刻执行; (2)将要在不同于自己的线程上执行的操作排入队列。(在多个线程并发更新UI的同时保证线程安全。) - -Android 规定访问 UI 只能在主线程中进行,因为 Android 的 UI 控件不是线程安全的,多线程并发访问会导致 UI 控件处于不可预期的状态。为什么系统不对 UI 控件的访问加上锁机制?缺点有两个:加锁会让 UI 访问的逻辑变得复杂;其次锁机制会降低 UI 访问的效率。如果子线程访问 UI,那么程序就会抛出异常。ViewRootImpl 对UI操作做了验证,这个验证工作是由 ViewRootImpl的 ``checkThread`` 方法完成: - -``ViewRootImpl.java`` -```java -void checkThread() { - if (mThread != Thread.currentThread()) { - throw new CalledFromWrongThreadException( - "Only the original thread that created a view hierarchy can touch its views."); - } -} -``` - -- Message:Handler 接收和处理的消息对象 -- MessageQueue:Message 的队列,先进先出,每一个线程最多可以拥有一个 -- Looper:消息泵,是 MessageQueue 的管理者,会不断从 MessageQueue 中取出消息,并将消息分给对应的 Handler 处理,每个线程只有一个 Looper。 - -Handler 创建的时候会采用当前线程的 Looper 来构造消息循环系统,需要注意的是,线程默认是没有 Looper 的,直接使用 Handler 会报错,如果需要使用 Handler 就必须为线程创建 Looper,因为默认的 UI 主线程,也就是 ActivityThread,ActivityThread 被创建的时候就会初始化 Looper,这也是在主线程中默认可以使用 Handler 的原因。 - -## 工作原理 -### ThreadLocal -ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,其他线程则无法获取。Looper、ActivityThread 以及 AMS 中都用到了 ThreadLocal。当不同线程访问同一个ThreadLocal 的 get方法,ThreadLocal 内部会从各自的线程中取出一个数组,然后再从数组中根据当前 ThreadLcoal 的索引去查找对应的value值。 -``ThreadLocal.java`` -```java -public void set(T value) { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - if (map != null) - map.set(this, value); - else - createMap(t, value); -} - -··· -public T get() { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - if (map != null) { - ThreadLocalMap.Entry e = map.getEntry(this); - if (e != null) { - @SuppressWarnings("unchecked") - T result = (T)e.value; - return result; - } - } - return setInitialValue(); -} -``` - -### MessageQueue -MessageQueue主要包含两个操作:插入和读取。读取操作本身会伴随着删除操作,插入和读取对应的方法分别是 ``enqueueMessage`` 和 ``next``。MessageQueue 内部实现并不是用的队列,实际上通过一个单链表的数据结构来维护消息列表。next 方法是一个无限循环的方法,如果消息队列中没有消息,那么 next 方法会一直阻塞。当有新消息到来时,next 方法会放回这条消息并将其从单链表中移除。 - -``MessageQueue.java`` -```java -boolean enqueueMessage(Message msg, long when) { - ··· - synchronized (this) { - ··· - msg.markInUse(); - msg.when = when; - Message p = mMessages; - boolean needWake; - if (p == null || when == 0 || when < p.when) { - // New head, wake up the event queue if blocked. - msg.next = p; - mMessages = msg; - needWake = mBlocked; - } else { - // Inserted within the middle of the queue. Usually we don't have to wake - // up the event queue unless there is a barrier at the head of the queue - // and the message is the earliest asynchronous message in the queue. - needWake = mBlocked && p.target == null && msg.isAsynchronous(); - Message prev; - for (;;) { - prev = p; - p = p.next; - if (p == null || when < p.when) { - break; - } - if (needWake && p.isAsynchronous()) { - needWake = false; - } - } - msg.next = p; // invariant: p == prev.next - prev.next = msg; - } - - // We can assume mPtr != 0 because mQuitting is false. - if (needWake) { - nativeWake(mPtr); - } - } - return true; -} -··· -Message next() { - // Return here if the message loop has already quit and been disposed. - // This can happen if the application tries to restart a looper after quit - // which is not supported. - ··· - for (;;) { - ··· - synchronized (this) { - // Try to retrieve the next message. Return if found. - final long now = SystemClock.uptimeMillis(); - Message prevMsg = null; - Message msg = mMessages; - if (msg != null && msg.target == null) { - // Stalled by a barrier. Find the next asynchronous message in the queue. - do { - prevMsg = msg; - msg = msg.next; - } while (msg != null && !msg.isAsynchronous()); - } - if (msg != null) { - if (now < msg.when) { - // Next message is not ready. Set a timeout to wake up when it is ready. - nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); - } else { - // Got a message. - mBlocked = false; - if (prevMsg != null) { - prevMsg.next = msg.next; - } else { - mMessages = msg.next; - } - msg.next = null; - if (DEBUG) Log.v(TAG, "Returning message: " + msg); - msg.markInUse(); - return msg; - } - } else { - // No more messages. - nextPollTimeoutMillis = -1; - } - ··· - } - - // Run the idle handlers. - // We only ever reach this code block during the first iteration. - for (int i = 0; i < pendingIdleHandlerCount; i++) { - final IdleHandler idler = mPendingIdleHandlers[i]; - mPendingIdleHandlers[i] = null; // release the reference to the handler - - boolean keep = false; - try { - keep = idler.queueIdle(); - } catch (Throwable t) { - Log.wtf(TAG, "IdleHandler threw exception", t); - } - - if (!keep) { - synchronized (this) { - mIdleHandlers.remove(idler); - } - } - } - - // Reset the idle handler count to 0 so we do not run them again. - pendingIdleHandlerCount = 0; - - // While calling an idle handler, a new message could have been delivered - // so go back and look again for a pending message without waiting. - nextPollTimeoutMillis = 0; - } -} -``` - -### Looper -Looper 会不停地从 MessageQueue 中 查看是否有新消息,如果有新消息就会立刻处理,否则会一直阻塞。 -``Looper.java`` -```java -private Looper(boolean quitAllowed) { - mQueue = new MessageQueue(quitAllowed); - mThread = Thread.currentThread(); -} -``` - -可通过 Looper.prepare() 为当前线程创建一个 Looper: -```java -new Thread("Thread#2") { - @Override - public void run() { - Looper.prepare(); - Handler handler = new Handler(); - Looper.loop(); - } -}.start(); -``` - -除了 prepare 方法外,Looper 还提供了 ``prepareMainLooper`` 方法,主要是给 ActivityThread 创建 Looper 使用,本质也是通过 prepare 方法实现的。由于主线程的 Looper 比较特殊,所以 Looper 提供了一个 getMainLooper 方法来获取主线程的 Looper。 - -Looper 提供了 ``quit`` 和 ``quitSafely`` 来退出一个 Looper,二者的区别是:``quit`` 会直接退出 Looper,而 ``quitSafly`` 只是设定一个退出标记,然后把消息队列中的已有消息处理完毕后才安全地退出。Looper 退出后,通过 Handler 发送的消息会失败,这个时候 Handler 的 send 方法会返回 false。因此在不需要的时候应终止 Looper。 - -``Looper.java`` -```java -public static void loop() { - final Looper me = myLooper(); - if (me == null) { - throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); - } - final MessageQueue queue = me.mQueue; - ··· - for (;;) { - Message msg = queue.next(); // might block - if (msg == null) { - // No message indicates that the message queue is quitting. - return; - } - ··· - try { - msg.target.dispatchMessage(msg); - dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; - } finally { - if (traceTag != 0) { - Trace.traceEnd(traceTag); - } - } - ··· - msg.recycleUnchecked(); - } -} -``` -loop 方法是一个死循环,唯一跳出循环的方式是 MessageQueue 的 next 方法返回了null。当 Looper 的 quit 方法被调用时,Looper就会调用 MessageQueue 的 quit 或者 qutiSafely 方法来通知消息队列退出,当消息队列被标记为退出状态时,它的 next 方法就会返回 null。loop 方法会调用 MessageQueue 的 next 方法来获取新消息,而 next 是一个阻塞操作,当没有消息时,next 会一直阻塞,导致 loop 方法一直阻塞。Looper 处理这条消息: msg.target.dispatchMessage(msg),这里的 msg.target 是发送这条消息的 Handler 对象。 - -### Handler -Handler 的工作主要包含消息的发送和接收的过程。消息的发送可以通过 post/send 的一系列方法实现,post 最终也是通过send来实现的。 - -![](https://img-blog.csdnimg.cn/20181220142659447) - -# 线程异步 -线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 - -应用启动时,系统会为应用创建一个名为“主线程”的执行线程( UI 线程)。 此线程非常重要,因为它负责将事件分派给相应的用户界面小部件,其中包括绘图事件。 此外,它也是应用与 Android UI 工具包组件(来自 ``android.widget`` 和 ``android.view`` 软件包的组件)进行交互的线程。 - -系统不会为每个组件实例创建单独的线程。运行于同一进程的所有组件均在 UI 线程中实例化,并且对每个组件的系统调用均由该线程进行分派。 因此,响应系统回调的方法(例如,报告用户操作的 onKeyDown() 或生命周期回调方法)始终在进程的 UI 线程中运行。 - -Android 的单线程模式必须遵守两条规则: -- 不要阻塞 UI 线程 -- 不要在 UI 线程之外访问 Android UI 工具包 - -为解决此问题,Android 提供了几种途径来从其他线程访问 UI 线程: -- ``Activity.runOnUiThread(Runnable)`` -- ``View.post(Runnable)`` -- ``View.postDelayed(Runnable, long)`` - -## AsyncTask -AsyncTask 封装了 Thread 和 Handler,并不适合特别耗时的后台任务,对于特别耗时的任务来说,建议使用线程池。 - -### 基本使用 -| 方法 | 说明 -|--|-- -| onPreExecute() | 异步任务执行前调用,用于做一些准备工作 -| doInBackground(Params...params) | 用于执行异步任务,此方法中可以通过 publishProgress 方法来更新任务的进度,publishProgress 会调用 onProgressUpdate 方法 -| onProgressUpdate | 在主线程中执行,后台任务的执行进度发生改变时调用 -| onPostExecute | 在主线程中执行,在异步任务执行之后 - -```java -import android.os.AsyncTask; - -public class DownloadTask extends AsyncTask { - - @Override - protected void onPreExecute() { - super.onPreExecute(); - } - - @Override - protected Boolean doInBackground(String... strings) { - return null; - } - - @Override - protected void onProgressUpdate(Integer... values) { - super.onProgressUpdate(values); - } - - @Override - protected void onPostExecute(Boolean aBoolean) { - super.onPostExecute(aBoolean); - } -} -``` - -- 异步任务的实例必须在 UI 线程中创建,即 AsyncTask 对象必须在UI线程中创建。 -- execute(Params... params)方法必须在UI线程中调用。 -- 不要手动调用 onPreExecute(),doInBackground(),onProgressUpdate(),onPostExecute() 这几个方法。 -- 不能在 doInBackground() 中更改UI组件的信息。 -- 一个任务实例只能执行一次,如果执行第二次将会抛出异常。 -- execute() 方法会让同一个进程中的 AsyncTask 串行执行,如果需要并行,可以调用 executeOnExcutor 方法。 - -### 工作原理 -``AsyncTask.java`` -```java -@MainThread -public final AsyncTask execute(Params... params) { - return executeOnExecutor(sDefaultExecutor, params); -} - -@MainThread -public final AsyncTask executeOnExecutor(Executor exec, - Params... params) { - if (mStatus != Status.PENDING) { - switch (mStatus) { - case RUNNING: - throw new IllegalStateException("Cannot execute task:" - + " the task is already running."); - case FINISHED: - throw new IllegalStateException("Cannot execute task:" - + " the task has already been executed " - + "(a task can be executed only once)"); - } - } - - mStatus = Status.RUNNING; - - onPreExecute(); - - mWorker.mParams = params; - exec.execute(mFuture); - - return this; -} -``` -sDefaultExecutor 是一个串行的线程池,一个进程中的所有的 AsyncTask 全部在该线程池中执行。AysncTask 中有两个线程池(SerialExecutor 和 THREAD_POOL_EXECUTOR)和一个 Handler(InternalHandler),其中线程池 SerialExecutor 用于任务的排队,THREAD_POOL_EXECUTOR 用于真正地执行任务,InternalHandler 用于将执行环境从线程池切换到主线程。 - -``AsyncTask.java`` -```java -private static Handler getMainHandler() { - synchronized (AsyncTask.class) { - if (sHandler == null) { - sHandler = new InternalHandler(Looper.getMainLooper()); - } - return sHandler; - } -} - -private static class InternalHandler extends Handler { - public InternalHandler(Looper looper) { - super(looper); - } - - @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) - @Override - public void handleMessage(Message msg) { - AsyncTaskResult result = (AsyncTaskResult) msg.obj; - switch (msg.what) { - case MESSAGE_POST_RESULT: - // There is only one result - result.mTask.finish(result.mData[0]); - break; - case MESSAGE_POST_PROGRESS: - result.mTask.onProgressUpdate(result.mData); - break; - } - } -} - - -private Result postResult(Result result) { - @SuppressWarnings("unchecked") - Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT, - new AsyncTaskResult(this, result)); - message.sendToTarget(); - return result; -} -``` - -## HandlerThread -HandlerThread 集成了 Thread,却和普通的 Thread 有显著的不同。普通的 Thread 主要用于在 run 方法中执行一个耗时任务,而 HandlerThread 在内部创建了消息队列,外界需要通过 Handler 的消息方式通知 HanderThread 执行一个具体的任务。 - -``HandlerThread.java`` -```java -@Override -public void run() { - mTid = Process.myTid(); - Looper.prepare(); - synchronized (this) { - mLooper = Looper.myLooper(); - notifyAll(); - } - Process.setThreadPriority(mPriority); - onLooperPrepared(); - Looper.loop(); - mTid = -1; -} -``` - -## IntentService -IntentService 可用于执行后台耗时的任务,当任务执行后会自动停止,由于其是 Service 的原因,它的优先级比单纯的线程要高,所以 IntentService 适合执行一些高优先级的后台任务。在实现上,IntentService 封装了 HandlerThread 和 Handler。 - -``IntentService.java`` -```java -@Override -public void onCreate() { - // TODO: It would be nice to have an option to hold a partial wakelock - // during processing, and to have a static startService(Context, Intent) - // method that would launch the service & hand off a wakelock. - - super.onCreate(); - HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); - thread.start(); - - mServiceLooper = thread.getLooper(); - mServiceHandler = new ServiceHandler(mServiceLooper); -} -``` - -IntentService 第一次启动时,会在 onCreatea 方法中创建一个 HandlerThread,然后使用的 Looper 来构造一个 Handler 对象 mServiceHandler,这样通过 mServiceHandler 发送的消息最终都会在 HandlerThread 中执行。每次启动 IntentService,它的 onStartCommand 方法就会调用一次,onStartCommand 中处理每个后台任务的 Intent,onStartCommand 调用了 onStart 方法: - -``IntentService.java`` -```java -private final class ServiceHandler extends Handler { - public ServiceHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - onHandleIntent((Intent)msg.obj); - stopSelf(msg.arg1); - } -} - -··· - -@Override -public void onStart(@Nullable Intent intent, int startId) { - Message msg = mServiceHandler.obtainMessage(); - msg.arg1 = startId; - msg.obj = intent; - mServiceHandler.sendMessage(msg); -} -``` - -可以看出,IntentService 仅仅是通过 mServiceHandler 发送了一个消息,这个消息会在 HandlerThread 中被处理。mServiceHandler 收到消息后,会将 Intent 对象传递给 onHandlerIntent 方法中处理,执行结束后,通过 stopSelf(int startId) 来尝试停止服务。(stopSelf() 会立即停止服务,而 stopSelf(int startId) 则会等待所有的消息都处理完毕后才终止服务)。 - -## 线程池 -线程池的优点有以下: -- 重用线程池中的线程,避免因为线程的创建和销毁带来性能开销。 -- 能有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。 -- 能够对线程进行管理,并提供定时执行以及定间隔循环执行等功能。 - -java 中,ThreadPoolExecutor 是线程池的真正实现: - -``ThreadPoolExecutor.java`` -```java -/** - * Creates a new {@code ThreadPoolExecutor} with the given initial - * parameters. - * - * @param corePoolSize 核心线程数 - * @param maximumPoolSize 最大线程数 - * @param keepAliveTime 非核心线程闲置的超时时长 - * @param unit 用于指定 keepAliveTime 参数的时间单位 - * @param 任务队列,通过线程池的 execute 方法提交的 Runnable 对象会存储在这个参数中 - * @param threadFactory 线程工厂,用于创建新线程 - * @param handler 任务队列已满或者是无法成功执行任务时调用 - */ -public ThreadPoolExecutor(int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - ThreadFactory threadFactory, - RejectedExecutionHandler handler) { - ··· -} -``` - -| 类型 | 创建方法 | 说明 -|--|--|-- -| FixedThreadPool | Executors.newFixedThreadPool(int nThreads) | 一种线程数量固定的线程池,只有核心线程并且不会被回收,没有超时机制 -| CachedThreadPool | Executors.newCachedThreadPool() | 一种线程数量不定的线程池,只有非核心线程,当线程都处于活动状态时,会创建新线程来处理新任务,否则会利用空闲的线程,超时时长为60s -| ScheduledThreadPool | Executors.newScheduledThreadPool(int corePoolSize) | 核心线程数是固定的,非核心线程数没有限制,非核心线程闲置时立刻回收,主要用于执行定时任务和固定周期的重复任务 -| SingleThreadExecutor | Executors.newSingleThreadExecutor() | 只有一个核心线程,确保所有任务在同一线程中按顺序执行 - -# RecyclerView 优化 -- 数据处理和视图加载分离:数据的处理逻辑尽可能放在异步处理,onBindViewHolder 方法中只处理数据填充到视图中。 - -- 数据优化:分页拉取远端数据,对拉取下来的远端数据进行缓存,提升二次加载速度;对于新增或者删除数据通过 DiffUtil 来进行局部刷新数据,而不是一味地全局刷新数据。 - -示例 -```java -public class AdapterDiffCallback extends DiffUtil.Callback { - - private List mOldList; - private List mNewList; - - public AdapterDiffCallback(List oldList, List newList) { - mOldList = oldList; - mNewList = newList; - DiffUtil.DiffResult - } - - @Override - public int getOldListSize() { - return mOldList.size(); - } - - @Override - public int getNewListSize() { - return mNewList.size(); - } - - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - return mOldList.get(oldItemPosition).getClass().equals(mNewList.get(newItemPosition).getClass()); - } - - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition)); - } -} -``` -```java -DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new AdapterDiffCallback(oldList, newList)); -diffResult.dispatchUpdatesTo(mAdapter); -``` - -- 布局优化:减少布局层级,简化 ItemView - -- 升级 RecycleView 版本到 25.1.0 及以上使用 Prefetch 功能 - -- 通过重写 RecyclerView.onViewRecycled(holder) 来回收资源 - -- 如果 Item 高度是固定的话,可以使用 RecyclerView.setHasFixedSize(true); 来避免 requestLayout 浪费资源 - -- 对 ItemView 设置监听器,不要对每个 Item 都调用 addXxListener,应该大家公用一个 XxListener,根据 ID 来进行不同的操作,优化了对象的频繁创建带来的资源消耗 - -- 如果多个 RecycledView 的 Adapter 是一样的,比如嵌套的 RecyclerView 中存在一样的 Adapter,可以通过设置 RecyclerView.setRecycledViewPool(pool),来共用一个 RecycledViewPool。 - -# Webview -## 基本使用 -### WebView -```java -// 获取当前页面的URL -public String getUrl(); -// 获取当前页面的原始URL(重定向后可能当前url不同) -// 就是http headers的Referer参数,loadUrl时为null -public String getOriginalUrl(); -// 获取当前页面的标题 -public String getTitle(); -// 获取当前页面的favicon -public Bitmap getFavicon(); -// 获取当前页面的加载进度 -public int getProgress(); - -// 通知WebView内核网络状态 -// 用于设置JS属性`window.navigator.isOnline`和产生HTML5事件`online/offline` -public void setNetworkAvailable(boolean networkUp) - -// 设置初始缩放比例 -public void setInitialScale(int scaleInPercent); - -``` - -### WebSettings -```java -WebSettings settings = web.getSettings(); - -// 存储(storage) -// 启用HTML5 DOM storage API,默认值 false -settings.setDomStorageEnabled(true); -// 启用Web SQL Database API,这个设置会影响同一进程内的所有WebView,默认值 false -// 此API已不推荐使用,参考:https://www.w3.org/TR/webdatabase/ -settings.setDatabaseEnabled(true); -// 启用Application Caches API,必需设置有效的缓存路径才能生效,默认值 false -// 此API已废弃,参考:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Using_the_application_cache -settings.setAppCacheEnabled(true); -settings.setAppCachePath(context.getCacheDir().getAbsolutePath()); - -// 定位(location) -settings.setGeolocationEnabled(true); - -// 是否保存表单数据 -settings.setSaveFormData(true); -// 是否当webview调用requestFocus时为页面的某个元素设置焦点,默认值 true -settings.setNeedInitialFocus(true); - -// 是否支持viewport属性,默认值 false -// 页面通过``自适应手机屏幕 -settings.setUseWideViewPort(true); -// 是否使用overview mode加载页面,默认值 false -// 当页面宽度大于WebView宽度时,缩小使页面宽度等于WebView宽度 -settings.setLoadWithOverviewMode(true); -// 布局算法 -settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL); - -// 是否支持Javascript,默认值false -settings.setJavaScriptEnabled(true); -// 是否支持多窗口,默认值false -settings.setSupportMultipleWindows(false); -// 是否可用Javascript(window.open)打开窗口,默认值 false -settings.setJavaScriptCanOpenWindowsAutomatically(false); - -// 资源访问 -settings.setAllowContentAccess(true); // 是否可访问Content Provider的资源,默认值 true -settings.setAllowFileAccess(true); // 是否可访问本地文件,默认值 true -// 是否允许通过file url加载的Javascript读取本地文件,默认值 false -settings.setAllowFileAccessFromFileURLs(false); -// 是否允许通过file url加载的Javascript读取全部资源(包括文件,http,https),默认值 false -settings.setAllowUniversalAccessFromFileURLs(false); - -// 资源加载 -settings.setLoadsImagesAutomatically(true); // 是否自动加载图片 -settings.setBlockNetworkImage(false); // 禁止加载网络图片 -settings.setBlockNetworkLoads(false); // 禁止加载所有网络资源 - -// 缩放(zoom) -settings.setSupportZoom(true); // 是否支持缩放 -settings.setBuiltInZoomControls(false); // 是否使用内置缩放机制 -settings.setDisplayZoomControls(true); // 是否显示内置缩放控件 - -// 默认文本编码,默认值 "UTF-8" -settings.setDefaultTextEncodingName("UTF-8"); -settings.setDefaultFontSize(16); // 默认文字尺寸,默认值16,取值范围1-72 -settings.setDefaultFixedFontSize(16); // 默认等宽字体尺寸,默认值16 -settings.setMinimumFontSize(8); // 最小文字尺寸,默认值 8 -settings.setMinimumLogicalFontSize(8); // 最小文字逻辑尺寸,默认值 8 -settings.setTextZoom(100); // 文字缩放百分比,默认值 100 - -// 字体 -settings.setStandardFontFamily("sans-serif"); // 标准字体,默认值 "sans-serif" -settings.setSerifFontFamily("serif"); // 衬线字体,默认值 "serif" -settings.setSansSerifFontFamily("sans-serif"); // 无衬线字体,默认值 "sans-serif" -settings.setFixedFontFamily("monospace"); // 等宽字体,默认值 "monospace" -settings.setCursiveFontFamily("cursive"); // 手写体(草书),默认值 "cursive" -settings.setFantasyFontFamily("fantasy"); // 幻想体,默认值 "fantasy" - - -if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - // 用户是否需要通过手势播放媒体(不会自动播放),默认值 true - settings.setMediaPlaybackRequiresUserGesture(true); -} -if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // 5.0以上允许加载http和https混合的页面(5.0以下默认允许,5.0+默认禁止) - settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); -} -if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // 是否在离开屏幕时光栅化(会增加内存消耗),默认值 false - settings.setOffscreenPreRaster(false); -} - -if (isNetworkConnected(context)) { - // 根据cache-control决定是否从网络上取数据 - settings.setCacheMode(WebSettings.LOAD_DEFAULT); -} else { - // 没网,离线加载,优先加载缓存(即使已经过期) - settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); -} - -// deprecated -settings.setRenderPriority(WebSettings.RenderPriority.HIGH); -settings.setDatabasePath(context.getDir("database", Context.MODE_PRIVATE).getPath()); -settings.setGeolocationDatabasePath(context.getFilesDir().getPath()); - -``` - -### WebViewClient -```java -// 拦截页面加载,返回true表示宿主app拦截并处理了该url,否则返回false由当前WebView处理 -// 此方法在API24被废弃,不处理POST请求 -public boolean shouldOverrideUrlLoading(WebView view, String url) { - return false; -} - -// 拦截页面加载,返回true表示宿主app拦截并处理了该url,否则返回false由当前WebView处理 -// 此方法添加于API24,不处理POST请求,可拦截处理子frame的非http请求 -@TargetApi(Build.VERSION_CODES.N) -public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return shouldOverrideUrlLoading(view, request.getUrl().toString()); -} - -// 此方法废弃于API21,调用于非UI线程 -// 拦截资源请求并返回响应数据,返回null时WebView将继续加载资源 -public WebResourceResponse shouldInterceptRequest(WebView view, String url) { - return null; -} - -// 此方法添加于API21,调用于非UI线程 -// 拦截资源请求并返回数据,返回null时WebView将继续加载资源 -@TargetApi(Build.VERSION_CODES.LOLLIPOP) -public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { - return shouldInterceptRequest(view, request.getUrl().toString()); -} - -// 页面(url)开始加载 -public void onPageStarted(WebView view, String url, Bitmap favicon) { -} - -// 页面(url)完成加载 -public void onPageFinished(WebView view, String url) { -} - -// 将要加载资源(url) -public void onLoadResource(WebView view, String url) { -} - -// 这个回调添加于API23,仅用于主框架的导航 -// 通知应用导航到之前页面时,其遗留的WebView内容将不再被绘制。 -// 这个回调可以用来决定哪些WebView可见内容能被安全地回收,以确保不显示陈旧的内容 -// 它最早被调用,以此保证WebView.onDraw不会绘制任何之前页面的内容,随后绘制背景色或需要加载的新内容。 -// 当HTTP响应body已经开始加载并体现在DOM上将在随后的绘制中可见时,这个方法会被调用。 -// 这个回调发生在文档加载的早期,因此它的资源(css,和图像)可能不可用。 -// 如果需要更细粒度的视图更新,查看 postVisualStateCallback(long, WebView.VisualStateCallback). -// 请注意这上边的所有条件也支持 postVisualStateCallback(long ,WebView.VisualStateCallback) -public void onPageCommitVisible(WebView view, String url) { -} - -// 此方法废弃于API23 -// 主框架加载资源时出错 -public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { -} - -// 此方法添加于API23 -// 加载资源时出错,通常意味着连接不到服务器 -// 由于所有资源加载错误都会调用此方法,所以此方法应尽量逻辑简单 -@TargetApi(Build.VERSION_CODES.M) -public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { - if (request.isForMainFrame()) { - onReceivedError(view, error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } -} - -// 此方法添加于API23 -// 在加载资源(iframe,image,js,css,ajax...)时收到了 HTTP 错误(状态码>=400) -public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { -} - - -// 是否重新提交表单,默认不重发 -public void onFormResubmission(WebView view, Message dontResend, Message resend) { - dontResend.sendToTarget(); -} - -// 通知应用可以将当前的url存储在数据库中,意味着当前的访问url已经生效并被记录在内核当中。 -// 此方法在网页加载过程中只会被调用一次,网页前进后退并不会回调这个函数。 -public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { -} - -// 加载资源时发生了一个SSL错误,应用必需响应(继续请求或取消请求) -// 处理决策可能被缓存用于后续的请求,默认行为是取消请求 -public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - handler.cancel(); -} - -// 此方法添加于API21,在UI线程被调用 -// 处理SSL客户端证书请求,必要的话可显示一个UI来提供KEY。 -// 有三种响应方式:proceed()/cancel()/ignore(),默认行为是取消请求 -// 如果调用proceed()或cancel(),Webview 将在内存中保存响应结果且对相同的"host:port"不会再次调用 onReceivedClientCertRequest -// 多数情况下,可通过KeyChain.choosePrivateKeyAlias启动一个Activity供用户选择合适的私钥 -@TargetApi(Build.VERSION_CODES.LOLLIPOP) -public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) { - request.cancel(); -} - -// 处理HTTP认证请求,默认行为是取消请求 -public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { - handler.cancel(); -} - -// 通知应用有个已授权账号自动登陆了 -public void onReceivedLoginRequest(WebView view, String realm, String account, String args) { -} -// 给应用一个机会处理按键事件 -// 如果返回true,WebView不处理该事件,否则WebView会一直处理,默认返回false -public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { - return false; -} - -// 处理未被WebView消费的按键事件 -// WebView总是消费按键事件,除非是系统按键或shouldOverrideKeyEvent返回true -// 此方法在按键事件分派时被异步调用 -public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - super.onUnhandledKeyEvent(view, event); -} - -// 通知应用页面缩放系数变化 -public void onScaleChanged(WebView view, float oldScale, float newScale) { -} - -``` - -### WebChromeClient -```java -// 获得所有访问历史项目的列表,用于链接着色。 -public void getVisitedHistory(ValueCallback callback) { -} - -//