From 2c7e910d2c7b09f0f0e270b32ca46a1a017c22ca Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 6 Sep 2018 16:23:04 +0800 Subject: [PATCH 001/247] update --- .../Git\347\256\200\344\273\213.md" | 60 +++++++++++++++++-- ...77\347\224\250\346\225\231\347\250\213.md" | 4 ++ .../DLNA\347\256\200\344\273\213.md" | 2 +- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git "a/JavaKnowledge/Git\347\256\200\344\273\213.md" "b/JavaKnowledge/Git\347\256\200\344\273\213.md" index 3e20b446..a55eb980 100644 --- "a/JavaKnowledge/Git\347\256\200\344\273\213.md" +++ "b/JavaKnowledge/Git\347\256\200\344\273\213.md" @@ -73,7 +73,7 @@ Git简介 `Git`主要分为四个区: - 工作区`(Working Area)` -- 暂存区`(Stage)` +- 暂存区`(Stage或Index Area)` - 本地仓库`(Local Repository)` - 远程仓库`(Remote Repository)` @@ -294,8 +294,10 @@ git push // 把所有文件从本地仓库推送进远程仓库 `git branch devBranch`创建名为`devBranch`的分支。 `git checkout devBranch`切换到`devBranch`分支。 - `git branch`查看当前仓库中的分支 - `git branch -r`查看远程仓库的分支 + `git checkout -b devBranch`创建+切换到分支`devBranch`。 + `git branch`查看当前仓库中的分支。 + `git branch -r`查看远程仓库的分支。 + `git branch -d devBranch`删除`devBranch`分支。 ``` origin/HEAD -> origin/master origin/developer @@ -358,7 +360,9 @@ git push // 把所有文件从本地仓库推送进远程仓库 在用`linux`的时候会自动生成一些以`~`结尾的备份文件,如果ignore掉呢?[https://github.com/github/gitignore/blob/master/Global/Linux.gitignore](https://github.com/github/gitignore/blob/master/Global/Linux.gitignore) - 撤销最后一次提交 - 有时候我们提交完了才发现漏掉了几个文件没有加或者提交信息写错了,想要撤销刚才的的提交操作。可以使用--amend选项重新提交:`git commit --amend` + 有时候我们提交完了才发现漏掉了几个文件没有加或者提交信息写错了,想要撤销刚才的的提交操作。可以使用--amend选项重新提交:`git commit --amend`,然后再执行`git push`操作。 + + - 查看远程仓库克隆地址 `git remote -v` @@ -410,7 +414,55 @@ git push // 把所有文件从本地仓库推送进远程仓库 这样你之前隐藏的内容就会重新出现了,你可以继续开发了。 +- Rebase操作 + + 多人在同一个分支上协作时,很容易出现冲突,即使没有冲突,在`push`代码之前也要先`pull`,在本地合并后再`push`,所以就经常会出现这样的分支: +```git +$ git log --graph --pretty=oneline --abbrev-commit +* d1be385 (HEAD -> master, origin/master) init hello +* e5e69f1 Merge branch 'dev' +|\ +| * 57c53ab (origin/dev, dev) fix env conflict +| |\ +| | * 7a5e5dd add env +| * | 7bd91f1 add new env +| |/ +* | 12a631b merged bug fix 101 +|\ \ +| * | 4c805e2 fix bug 101 +|/ / +* | e1e9c68 merge with no-ff +|\ \ +| |/ +| * f52c633 add merge +|/ +* cf810e4 conflict fixed +``` +看上去会很乱,有些强迫症的人会问:为什么`Git`的提交历史不能是一条干净的直线? +`rebase`操作就是解决这个问题的,它可以把分叉的提交历史整理变成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。 + +也就是说`gie merge`和`git rebase`做的事情其实是一样的。它们都被设计来将一个分支的更改并入到另一个分支中。 + +- git fetch与git pull的区别 + + `git`中`fetch`命令是将远程分支的最新内容拉到了本地,但是`fecth`后是看不到变化的,如果查看当前的分支,会发现此时本地多了一个`FETCH_HEAD`的指针,`checkout`到该指针后才可以查看远程分支的最新内容。 + 而`git pull`的作用相当于`fetch`和`merge`的组合,会自动合并: + + ```git + git fetch origin master + git merge FETCH_HEAD + ``` + +- git pull 与git pull --rebase的使用 + + 使用下面的关系区别这两个操作: + + ```git + git pull = git fetch + git merge + git pull --rebase = git fetch + git rebase + ``` + `git rebase`的过程中,有时会有`conflit`这时`Git`会停止`rebase`并让用户去解决冲突,解决完冲突后,用`git add`命令去更新这些内容,然后不用执行`git commit`,直接执行`git rebase --continue`这样`git`会继续`apply`余下的补丁。 --- diff --git "a/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" "b/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" index 1aee9b70..71132805 100644 --- "a/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" +++ "b/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" @@ -40,6 +40,8 @@ Vim使用教程 - `#l` 移动光标到改行第#个字的位置,如`5l` - `ctrl+g` 列出当前光标所在行的行号等信息 - `: #` 如输入: 15会跳到文章的第15行 +- `ctrl + d`向下翻页 +- `ctrl + u`向上翻页 删除文字 --- @@ -59,6 +61,7 @@ Vim使用教程 - `yw` 复制当前光标所在位置到字尾处的位置 - `#yw` 复制当前光标所在位置往后#个字 - `y$` 拷贝光标至本行结束位置 +- `y` 拷贝选中部分,在`Normal`模式下按`v`会进入到可视化模式,这时候可以上下移动进行选中某一部分,然后按`y`就可以复制了。 - `p` 粘贴 @@ -74,6 +77,7 @@ Vim使用教程 --- - `u` 撤销、回退 +- `ctrl + r` 恢复刚才的撤销操作 搜索 --- diff --git "a/VideoDevelopment/DLNA\347\256\200\344\273\213.md" "b/VideoDevelopment/DLNA\347\256\200\344\273\213.md" index 7c555536..7abc3486 100644 --- "a/VideoDevelopment/DLNA\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/DLNA\347\256\200\344\273\213.md" @@ -92,7 +92,7 @@ DLNA架构分为如下图7个层次: 3. Device Discovery&Control 设备发现和控制。 这一层是DLNA的基础协议框架。**DLNA用UPnP协议来实现设备的发现和控制**。下面重点看一下UPnP。 - `UPnP`,英文是`Universal Plug and play`,翻译过来就是通用即插即用。UPnP最开始Apple和Microsoft在搞,后来Apple不做了(这里多一嘴,为什么Apple不做了,因为Apple现在出了个),Microsoft还在继续做,Intel也加进来做,Sony,Moto等等也有加入。UPnP有个网站http://www.upnp.org/,我们发现DLNA的网页和UPnP的网页很像,颜色也差不多,就可以知道他们关系很好了。DNLA主要是在推UPnP。 + `UPnP`,英文是`Universal Plug and play`,翻译过来就是通用即插即用。UPnP最开始Apple和Microsoft在搞,后来Apple不做了(这里多一嘴,为什么Apple不做了,因为Apple现在出了个),Microsoft还在继续做,Intel也加进来做,Sony,Moto等等也有加入。UPnP有个网站[http://www.upnp.org/](http://www.upnp.org/),我们发现DLNA的网页和UPnP的网页很像,颜色也差不多,就可以知道他们关系很好了。DNLA主要是在推UPnP。 微软官方网站对UPnP的解释:通用即插即用 (UPnP) 是一种用于 PC 机和智能设备(或仪器)的常见对等网络连接的体系结构,尤其是在家庭中。UPnP 以 Internet 标准和技术(例如 TCP/IP、HTTP 和 XML)为基础,使这样的设备彼此可自动连接和协同工作,从而使网络(尤其是家庭网络)对更多的人成为可能。 举个例子。我们在自己的PC(win7)里面打开网络服务的UPnP选项,然后再家庭网络中共享一个装着视频的文件夹,然后买一台SmartTV回来打开就可以找到这台PC的共享文件夹,然后就直接在电视上选文件播放了。 From 568649aa5722e6bba1e9df573b2e542b747e7506 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 10 Sep 2018 12:00:17 +0800 Subject: [PATCH 002/247] update flipper part --- ...7\225\345\271\263\345\217\260Sonar.md.swp" | Bin 0 -> 16384 bytes ...0\257\225\345\271\263\345\217\260Sonar.md" | 31 +++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 "Tools&Library/.\350\260\203\350\257\225\345\271\263\345\217\260Sonar.md.swp" diff --git "a/Tools&Library/.\350\260\203\350\257\225\345\271\263\345\217\260Sonar.md.swp" "b/Tools&Library/.\350\260\203\350\257\225\345\271\263\345\217\260Sonar.md.swp" new file mode 100644 index 0000000000000000000000000000000000000000..9ee7fb83ad274e01fee51fcbe4c013bbf567f7c7 GIT binary patch literal 16384 zcmeHOYitzP6`mw*+O(lflRuRzB@@#GTf(~#k~B3j1hA9RVxTd!sz}6)cgMSfXLq(U zvj&IKtN|N?pQIQJ_yNJjjsv!N`mq?dO@FjXTdAp(D*2I+Wbf>*nuZ@$E46B+e)ry) z-L=7Kkn$s~cBRj|Gjkv3oO>SkoI9>s@`JS%;uD@F0zUT$!tEb+ZmGCXB0PSdAnb_x zf>AkaC}H|={?Fc?o=+1fo`T@e>O+FMDJu zGxdc94~qBQyZPgKT z<4he#{lcp>;m7&EoBx0KRY6DrN#Ga2&w=Y-5rmI`zb_DkzX9uk3Sb>@?QTJM7kCFa z2K*G*1%!b=epwLS0RH(ULHGyI3seFR0`K1?2=4)p0?U9$fCa$az|0o~;VQ5axEJ^u za1U_p3z!Qy2Mhx>z(U{w;C|rxouCu2fj!>at%PKMbLo& z@DT7#;4a`QWIqO+0q8mm12_9fk|c=uc&ehR<}WN-&aY&@f4yRD*NvLBVM{T5O2pEQ z!mzSkWUozZs0mtDWcDN0nmyg9MFVQMs7RpgH`IIwP1k5DUeNHYrjsZ}VUZxHp@?o+ zqE8Qbs%4*2rRy~wlf9A=Vcktnm=oZ+`Hag2kiJc>m7!BJN7MUS(%rGt>3aLZkiEA( zF%e5lG+)2cLQe0td%F|UGs#0kiK#|OXM{_ON&9FbKEUtW&8O|AVRCdRc_E&R_oem? zlLJFi5Clo2X)M_`M!JWR$M%vFjrNJA4`REaz}?Mwg7HYlk?U9b$oT2h)k)sa?%pqH zdO-RhRxddWNaUp!$qdSd;*S_gwSsqFe2SEq*q@jfPVX5aO^p}{{^-E=TD(`E0uVD}ABN+$M@b1~Xq5S~;+0ns#kr3G|-%o4BH zkhgpIYcWw1>!@Qju?~>BcMT}g!)t<3)w@ykSy4kVy&>7eYJ1r_Y=sVN%|Q>bHWW1xuGKd8j}PDrz8TIaezLAu(|v-SaYNOvKOF<8gA}1h`8~^n+Q6 zk0x;*==1%9sgo_>fv;By^iZJ7D(qrf$p7M|nEgesd|a7KOExT5W9fbRah`#8u7zA@ zneaDegm31kX`vNV%_2wd`55zz;uu73-?{y&)v&B_6X6=~&?AuPS=4Zr*ALe6kwp7jZm_a?N`OCv{m8P&^9ahAh zg5WYzsjt&zSe!=EEQgfR0_Vz; zZQxn8W=(lz<<=jreR}QI%1x^_l^2NK6~e8Lvv&Qa@@LnVZ(`lS!;X+96pQK8he<;d zccF=yp44bP89z@>joDpoaG~&}cF&&l(GG5*3l}aF?Y04ScYty&MS`%Kb4>^e;D38{PiRGpY#F>5S6aw{H;vd85?b+9c?M!J&I z2f=M>dfGnO0`Eqq4k4c4DZfcZTCn`C>mIgGjFYn+;GV?_G(nFh-6NQ+afaN>4jFZMSzMt{%rov?HGOP1)UNXdF1um)bLp;q4QXcI${# zx$U*f8gvF`fTyX^i!c&UCq3;1R*8%cB!>_wnh-e1l^AJ12&ZhfzXbKPdj^un587>m z(ldq}34VW_lo}nk&+SG4OO5xXCWh_UASSa<>`RS|CC~0>Ocje#Wv)*}JS$-lwvET6 z8^ay91xG08?l@Bs4lttlU{sj_PQ~aL(+89BZu?SwdS5%85k1z)b$ZmAuY;s+sAPfG z@v{_MEGYIf4>3COaq(6PU(Qcya$`-yA{M!4Q zu~p#K&Ex`Ou=l@0&UHd)@KsoNwhXhDlZ8X-?zsG=RU6kbXXh--6gk+HyE}nmB;$Ko zE7EkF#HXm!96tv#GV6!tsli4#Lu@phH(Z&5a^~&MG3W@$%cvE1p2_`9&?eH-KssJc zjZBdKW2E&0m?FK+d}PYz)n;gGi9m(sD5Y3g_C3gM5RrW-S8CjMa2d^Nn-g9h= zF}}r7I8HU)bXQkZbGFz<+pqh~B1+3-Gc+f=oQ||7hDl=sRMfFtCQy)P9~pu-Arm9y zz$>$eMA|adpO|`yTxo~B!}hty!D#R}xM?#7ibu(g#?;^`?ikw-Nb@+xWc+7TaPS-_ z5zeLOSw02~LN2c84&1l~Yh;P=39ferK<^?=ub zMZg2VpOOFn3V00|2YP^dU>EQcAPhVN+yOKr-`@ay8+a4B{RN;NcpCTyumHFX5P(MH z^)m1+;7;HJ$m@OJJ>VMfHt-hkJD>+>0P2Akft`R`3)q3*7GMG!fC_-g9@FGM|0@!B zM%Vq~x~Q+_K{4m2P>cYV-aV2WIVWPX=wnH{2meE(J{1VbszyBw-9Q!>7gI*Dp=VhO zPnbi<9>a*PG{Gnp!%7TVX*(U4=WY;5PDl#UJTW(XWRWSz=JGRYXe{x67&}R_g{hRzxR9p!W^s$5F+! zM89mwc&0}K!Nnr>ouW@hj;5J9q-O^CW4~&EEUk|A%NfR#mCmj?VwhdVa0$%){si)K zn{zo*LLtP8x-$ImprTNSx|`c%?)QI}sM`a@pomTr3Xd7}`5@E6S%c12p=TAJqSh*f zY&WxkEtVLO4LM{k=Jkqa-O(UlXWV&o&7D72>)d-es|~@k84JKLWgHU7@a9m1BBN8Q zp#7l)n@u$wU<<`-Fj>>kImzxa8%jXMF2fLIkjv&VM7b7(nG1ALR1BuB# zp7dl&LK1IJcd?m!d^eXohm^b*6i(fo&i!s?rT( z{8aKpJ!&nWmrE{<+`MZvdHf<+M6#5c*(YU-pG8i^3{?!CrDBLo&rIFb$#&ghgDZKK z?T9R-+`Os^-LKMjtvag1@F?x%}v07P+ zB1lr==_Nfk02d}kL*&LSK0jM_kGP87p_EV#_!Jolu!&qDS*D0{5fqducPt9iZl+=} zzCo#q7N8E{lbwJJL%nIr+R4`k0mCXV7OeDVZ>;LOK3hzUQiD> ze-zs#Uc|s&jVpdZi}0HnA{p8PD%3THlK}i8)q#?Cv#6AX#wKUsNGU8{0E3Qr -看到了吗? 左边现在有`log`、`network`和`sharedpreference`三个部分了。 但是这三个是远远不够的啊,我还想查看布局。 + +看到了吗?左边现在有`log`、`network`和`sharedpreference`三个部分了。 但是这三个是远远不够的啊,我还想查看布局。 这里需要注意一下,如果使用了`okhttp`,可以使用拦截器系统自动`hook`到现在的堆栈。 @@ -95,7 +96,7 @@ new OkHttpClient.Builder() - 增加对查看布局的支持 ```java -public class SonarApplication extends Application { +public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); From 1b5e11cc7e358c3ca59fd15eed1ebc8d28f7bd54 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 10 Sep 2018 12:03:57 +0800 Subject: [PATCH 003/247] update --- ...225\345\271\263\345\217\260Flipper.md.swp" | Bin 16384 -> 16384 bytes ...257\225\345\271\263\345\217\260Flipper.md" | 6 ++++-- 2 files changed, 4 insertions(+), 2 deletions(-) rename "Tools&Library/.\350\260\203\350\257\225\345\271\263\345\217\260Sonar.md.swp" => "Tools&Library/.\350\260\203\350\257\225\345\271\263\345\217\260Flipper.md.swp" (72%) rename "Tools&Library/\350\260\203\350\257\225\345\271\263\345\217\260Sonar.md" => "Tools&Library/\350\260\203\350\257\225\345\271\263\345\217\260Flipper.md" (99%) diff --git "a/Tools&Library/.\350\260\203\350\257\225\345\271\263\345\217\260Sonar.md.swp" "b/Tools&Library/.\350\260\203\350\257\225\345\271\263\345\217\260Flipper.md.swp" similarity index 72% rename from "Tools&Library/.\350\260\203\350\257\225\345\271\263\345\217\260Sonar.md.swp" rename to "Tools&Library/.\350\260\203\350\257\225\345\271\263\345\217\260Flipper.md.swp" index 9ee7fb83ad274e01fee51fcbe4c013bbf567f7c7..eb7fb2d80436c3aa4e688d0d2a9b697503c2d166 100644 GIT binary patch delta 414 zcmXBQKS;t+5Ww;G{AosLDusg$X$>}+&`>&vEkZ-k)Y78;AtD1YDq8YKMt>lP5|`GZ z9LlsuL=CkR4UM%`OChP@UQ>P3gU|7XyW`#Iy3}>)8uyP54Z8Qr8M-tuWf(sQBvvGq%9GFB(qf*k5MXsln)(WuIMHkz!T x`)ieSuC%%Sp|1)R>dBkiM7fYIpJhu|s -我们可以看到左边只有`log`部分。 + +我们可以看到左边只有`log`的部分。 ### 接下来在`app`中嵌入`flipper`的`sdk` @@ -183,4 +184,5 @@ public class MySonarPlugin implements SonarPlugin { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! +- +Good Luck! From 775fa1c69bf8f901d7d507dc12ad6db45c9f98eb Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 10 Sep 2018 19:05:17 +0800 Subject: [PATCH 004/247] update --- .../.Git\347\256\200\344\273\213.md.swp" | Bin 0 -> 40960 bytes "JavaKnowledge/Git\347\256\200\344\273\213.md" | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 "JavaKnowledge/.Git\347\256\200\344\273\213.md.swp" diff --git "a/JavaKnowledge/.Git\347\256\200\344\273\213.md.swp" "b/JavaKnowledge/.Git\347\256\200\344\273\213.md.swp" new file mode 100644 index 0000000000000000000000000000000000000000..1487e7e91ecc4d64a289da239ed906f5ae91c148 GIT binary patch literal 40960 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 是在添加到暂存区后,撤出暂存区使用,他只会把文件撤出暂存区,但是你的修改还在,仍然在工作区。当然如果使用`git reset --hard HEAD`这样就完了,工作区所有的内容都会被远程仓库最新代码覆盖。 -- `git checkout -- xxx.txt`是用于修改后未添加到暂存区时使用(如果修改后添加到暂存区后就没效果了,必须要先reset撤销暂存区后再使用checkout),这时候会把之前的修改覆盖掉。所以是危险的。 +- `git checkout -- xxx.txt`是用于修改后未添加到暂存区时使用(如果修改后添加到暂存区后就没效果了,必须要先`reset`撤销暂存区后再使用`checkout`),这时候会把之前的修改覆盖掉。所以是危险的。 - 隐藏操作 From 0ad9b7735bd0cbd230776cc4478e6e7032e7ae0c Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 10 Sep 2018 19:06:40 +0800 Subject: [PATCH 005/247] update --- .../.Git\347\256\200\344\273\213.md.swp" | Bin 40960 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 "JavaKnowledge/.Git\347\256\200\344\273\213.md.swp" diff --git "a/JavaKnowledge/.Git\347\256\200\344\273\213.md.swp" "b/JavaKnowledge/.Git\347\256\200\344\273\213.md.swp" deleted file mode 100644 index 1487e7e91ecc4d64a289da239ed906f5ae91c148..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40960 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 Date: Mon, 10 Sep 2018 19:07:16 +0800 Subject: [PATCH 006/247] update --- ...7\225\345\271\263\345\217\260Flipper.md.swp" | Bin 16384 -> 0 bytes ...0\257\225\345\271\263\345\217\260Flipper.md" | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 "Tools&Library/.\350\260\203\350\257\225\345\271\263\345\217\260Flipper.md.swp" diff --git "a/Tools&Library/.\350\260\203\350\257\225\345\271\263\345\217\260Flipper.md.swp" "b/Tools&Library/.\350\260\203\350\257\225\345\271\263\345\217\260Flipper.md.swp" deleted file mode 100644 index eb7fb2d80436c3aa4e688d0d2a9b697503c2d166..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 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 diff --git "a/Tools&Library/\350\260\203\350\257\225\345\271\263\345\217\260Flipper.md" "b/Tools&Library/\350\260\203\350\257\225\345\271\263\345\217\260Flipper.md" index 1263c1a9..63b7ebcc 100644 --- "a/Tools&Library/\350\260\203\350\257\225\345\271\263\345\217\260Flipper.md" +++ "b/Tools&Library/\350\260\203\350\257\225\345\271\263\345\217\260Flipper.md" @@ -1,4 +1,5 @@ -调试平台Flipper +调试平台Flippe + === 子日:工欲善其事必先利其器 From 76b6469110763ca861ee956b4e732c8a94a61285 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 30 Oct 2018 17:22:51 +0800 Subject: [PATCH 007/247] update --- ...\345\256\236\347\216\260(\344\270\200).md" | 2 +- ...\345\256\236\347\216\260(\344\272\214).md" | 97 +++++ ...\345\216\237\345\233\240(\344\270\211).md" | 395 ++++++++++++++++++ .../Git\347\256\200\344\273\213.md" | 2 +- 4 files changed, 494 insertions(+), 2 deletions(-) rename "AdavancedPart/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" => "AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" (99%) create mode 100644 "AdavancedPart/2.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\272\214).md" create mode 100644 "AdavancedPart/3.\347\203\255\344\277\256\345\244\215_addAssetPath\344\270\215\345\220\214\347\211\210\346\234\254\345\214\272\345\210\253\345\216\237\345\233\240(\344\270\211).md" diff --git "a/AdavancedPart/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" "b/AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" similarity index 99% rename from "AdavancedPart/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" rename to "AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" index 15246225..bf052c89 100644 --- "a/AdavancedPart/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" +++ "b/AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" @@ -1,5 +1,5 @@ -热修复实现 +热修复实现(一) === diff --git "a/AdavancedPart/2.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\272\214).md" "b/AdavancedPart/2.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\272\214).md" new file mode 100644 index 00000000..fd2ffe63 --- /dev/null +++ "b/AdavancedPart/2.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\272\214).md" @@ -0,0 +1,97 @@ + +热修复实现(一) +=== + +之前也分析过`InstantRun`的源码,前面也写了一篇热修复实现原理的文章。 + +But,最近遇到困难了,所在项目要做插件化,同事在开发过程中遇到了一个在5.0以下手机崩溃的问题,想着一起找找原因修复下。 +但是这个bug已经折腾了我两天了,只找到原因,并没有找出任何解决方案。 + +深深的感觉到之前的那些都是皮毛,没有真正的去做,真正的去处理一些细节,那些都是没任何意义的。 + +所以这里系统学习一下,既然学习,就找一个做的最好的来学。 + + +前面的文章也介绍了,目前存在的几个开源框架: + +- 手机淘宝基于Xposed进行了改进,产生了针对Android Dalvik虚拟机运行时的Java Method Hook技术-Dexposed。 +但这个方案犹豫底层Dalvik结构过于依赖,最终无法兼容Android 5.0以后的ART虚拟机。 + +- 支付宝提出了新的方案Andfix,它同样是一种底层结构替换的方案,也达到了运行时生效即时修复的效果,而且做到了Dalvik和ART全版本的兼容。然而它也 +是由局限性的,且不说其底层固定结构的替换方案稳定性不好,其使用范文也存在着诸多限制,虽然可以通过代码改造来绕过限制达到同样的 +修复目的,但是这种方式即不优雅也不方便。而且更大的问题是Andfix只提供了代码层面的修复,对于资源和so的修复都还未实现。 + +- 其他的就是微信Tinker、饿了么的Amigo、美团的Robust,不过他们都各自有各自的局限性,或者不够稳定、或者补丁过大、或者效率低下 +,或者使用起来太繁琐,大部分技术上看起来似乎可行,但是实际体验并不好。 + +我们学习的就是阿里巴巴的新一代非侵入式Android热修复方案-Sophix。 +它各个方面都比较优秀,使用也比较方便,唯一不支持的就是四大组件的修复。这是因为如果修复四大组件,必须在AndroidManifest里面预先插入代码组件,并且尽可能声明所有权限,这样就会给 +原先的app添加很多臃肿的代码,对app运行流程的侵入性很强。 + + +在Sophix中,唯一需要的就是初始化和请求补丁两行代码,甚至连入口Application类我们都不需要做任何修改。 + +这样就给了开发者最大的透明度和自由度。我们甚至重新开发了打包工具,使的补丁工具操作图形界面化,这种所见即所得的补丁生成 +方式也是阿里热修复独家的,因此,Sophix的接入成本也是目前市面上所有方案里最低的。 + + + + +代码修复 +--- + +代码修复有两大主要方案: + +- 阿里系的底层替换方案:底层替换方案限制颇多,但是时效性最好,加载轻快,立即见效。 + + 底层替换方案是在已经加载了的类中直接替换掉原有的方法,是在原来类的基础上进行修改的。因而无法实现对原有类进行方法和字段的增减,因为这样将破坏原有类的结构。 + 一旦补丁类中出现了方法的增加和减少,就会导致这个类以及整个dex的方法数的变化,方法数的变化伴随着方法索引的变化,这样在 + 访问方法时就无法正常的索引到正确的方法了。如果字段发生了增加或减少,和方法变化的情况一样,所有字段的索引都会发生变化。 + 而新方法中使用到这些老的示例对象时,访问新增字段就会会产生不可预期的结果。 + + 这是该类方案的固有限制,而底层替换方案最为人逅病的地方,在于底层替换的不稳定性。因为Hook方案,都是直接依赖修改虚拟机方法实体的具体字段。因为Art虚拟机和Dalvik虚拟机的不同,每个版本的虚拟机都要适配,而且Android系统是开源的,各个厂商都可以对代码进行改造,如果某个厂商进行了修改,那么这种通用性的替换机制就会出问题。这便是不稳定的根源。 + + + +- 腾讯系的类加载方案:类加载方案时效性差,需要重新冷启动才能见效,但修复范围广、限制少。 + +类加载方案的原理是在app重新启动后让classloader去加载新的类。因为在app运行到一半的时候,所有需要发生变更的类已经被加载过了,在Android上是无法对一个类进行卸载的。如果不重启,原来的类还在虚拟机中,就无法加载新类,因此只有在下次重启的时候, +在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类的时候,就会用新类,从而达到热修复的目的。 + + +为什么sophix更好? +--- + +既然底层替换方案和类加载方案各有优点,把他们联合起来就是最好的选择。Sophix就是同时涵盖了这两种方案,可以实现优势互补、完全兼顾的作用,可以灵活的根据实际情况自动切换。 + + + + + + +但是[Sophix](https://help.aliyun.com/document_detail/57064.html?spm=a2c4g.11186623.6.543.SPhMhO)有一个缺点就是,他不是开源的,而且是收费的。但是确实强大。 + + + + +- [Google Instant app](https://developer.android.com/topic/instant-apps/index.html) +- [微信热补丁实现](https://github.com/WeMobileDev/article/blob/master/%E5%BE%AE%E4%BF%A1Android%E7%83%AD%E8%A1%A5%E4%B8%81%E5%AE%9E%E8%B7%B5%E6%BC%94%E8%BF%9B%E4%B9%8B%E8%B7%AF.md#rd) +- [多dex分拆](http://my.oschina.net/853294317/blog/308583) +- [QQ空间热修复方案](https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a) +- [Android dex分包方案](http://my.oschina.net/853294317/blog/308583) +- [类加载器DexClassLoader](http://www.maplejaw.com/2016/05/24/Android%E6%8F%92%E4%BB%B6%E5%8C%96%E6%8E%A2%E7%B4%A2%EF%BC%88%E4%B8%80%EF%BC%89%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8DexClassLoader/) +- [基于cydia Hook在线热修复补丁方案](http://blog.csdn.net/xwl198937/article/details/49801975) +- [Android 热补丁动态修复框架小结](http://blog.csdn.net/lmj623565791/article/details/49883661) +- [美团Android DEX自动拆包及动态加载简介](http://tech.meituan.com/mt-android-auto-split-dex.html) +- [插件化从放弃到捡起](http://kymjs.com/column/plugin.html) +- [无需Root也能使用Xposed!](http://weishu.me/) +- [当你准备开发一个热修复框架的时候,你需要了解的一切](http://www.zjutkz.net/2016/05/23/%E5%BD%93%E4%BD%A0%E5%87%86%E5%A4%87%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E7%83%AD%E4%BF%AE%E5%A4%8D%E6%A1%86%E6%9E%B6%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E4%BD%A0%E9%9C%80%E8%A6%81%E4%BA%86%E8%A7%A3%E7%9A%84%E4%B8%80%E5%88%87/) + + +[1]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/InstantRun%E8%AF%A6%E8%A7%A3.md "InstantRun详解" + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/AdavancedPart/3.\347\203\255\344\277\256\345\244\215_addAssetPath\344\270\215\345\220\214\347\211\210\346\234\254\345\214\272\345\210\253\345\216\237\345\233\240(\344\270\211).md" "b/AdavancedPart/3.\347\203\255\344\277\256\345\244\215_addAssetPath\344\270\215\345\220\214\347\211\210\346\234\254\345\214\272\345\210\253\345\216\237\345\233\240(\344\270\211).md" new file mode 100644 index 00000000..4da9dd5c --- /dev/null +++ "b/AdavancedPart/3.\347\203\255\344\277\256\345\244\215_addAssetPath\344\270\215\345\220\214\347\211\210\346\234\254\345\214\272\345\210\253\345\216\237\345\233\240(\344\270\211).md" @@ -0,0 +1,395 @@ +在做热修复功能时Java层通过反射调用addAssetPath在Android5.0及以上系统没有问题,在Android 4.x版本找不到资源。 + +addAssetPath方法: +```java +/** + * Add an additional set of assets to the asset manager. This can be + * either a directory or ZIP file. Not for use by applications. Returns + * the cookie of the added asset, or 0 on failure. + * {@hide} + */ +public final int addAssetPath(String path) { + int res = addAssetPathNative(path); + return res; +} +private native final int addAssetPathNative(String path); +``` + +addAssetPath具体的实现方法是native层的。 + +[AssetManager.cpp源码](https://android.googlesource.com/platform/frameworks/base/+/android-4.4_r1.0.1/libs/androidfw/AssetManager.cpp) + +4.4及以前的版本调用addAssetPath方法时,只是把补丁包的路径添加到mAssetPath中,不会去重新解析,真正解析的代码是在app第一次执行AssetManager.getResTable()`方法的时候。 +一旦解析完一次后,mResource对象就不为nil,以后就会直接return掉,不会重新解析。 + +```c +bool AssetManager::addAssetPath(const String8& path, void** cookie) +{ + AutoMutex _l(mLock); + asset_path ap; + String8 realPath(path); + if (kAppZipName) { + realPath.appendPath(kAppZipName); + } + ap.type = ::getFileType(realPath.string()); + if (ap.type == kFileTypeRegular) { + ap.path = realPath; + } else { + ap.path = path; + ap.type = ::getFileType(path.string()); + if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) { + ALOGW("Asset path %s is neither a directory nor file (type=%d).", + path.string(), (int)ap.type); + return false; + } + } + // Skip if we have it already. + for (size_t i=0; i(this)->loadFileNameCacheLocked(); + const size_t N = mAssetPaths.size(); + // 真正解析package的地方 + for (size_t i=0; i(this)-> + mZipSet.getZipResourceTable(ap.path); + } + if (sharedRes == NULL) { + ass = const_cast(this)-> + mZipSet.getZipResourceTableAsset(ap.path); + if (ass == NULL) { + ALOGV("loading resource table %s\n", ap.path.string()); + ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + if (ass != NULL && ass != kExcludedAsset) { + ass = const_cast(this)-> + mZipSet.setZipResourceTableAsset(ap.path, ass); + } + } + + if (i == 0 && ass != NULL) { + // If this is the first resource table in the asset + // manager, then we are going to cache it so that we + // can quickly copy it out for others. + ALOGV("Creating shared resources for %s", ap.path.string()); + sharedRes = new ResTable(); + sharedRes->add(ass, (void*)(i+1), false, idmap); + sharedRes = const_cast(this)-> + mZipSet.setZipResourceTable(ap.path, sharedRes); + } + } + } else { + ALOGV("loading resource table %s\n", ap.path.string()); + Asset* ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + shared = false; + } + if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) { + if (rt == NULL) { + mResources = rt = new ResTable(); + updateResourceParamsLocked(); + } + ALOGV("Installing resource asset %p in to table %p\n", ass, mResources); + if (sharedRes != NULL) { + ALOGV("Copying existing resources for %s", ap.path.string()); + rt->add(sharedRes); + } else { + ALOGV("Parsing resources for %s", ap.path.string()); + rt->add(ass, (void*)(i+1), !shared, idmap); + } + if (!shared) { + delete ass; + } + } + if (idmap != NULL) { + delete idmap; + } + MY_TRACE_END(); + } + if (required && !rt) ALOGW("Unable to find resources file resources.arsc"); + if (!rt) { + mResources = rt = new ResTable(); + } + return rt; +} + + +const ResTable& AssetManager::getResources(bool required) const +{ + const ResTable* rt = getResTable(required); + return *rt; +} +``` +而当我们执行加载补丁的代码的时候,getResTable已经执行过多次了,Android Framework里面的代码会多次调用该方法。所以即使是使用addAssetPath,也只是添加到了mAssetPath,并不会发生解析,所以补丁包里面的资源就是完全不生效的。 + +而在android 5.0及以上的代码中: + +[Android 5.0 AssetManager.cpp源码](https://android.googlesource.com/platform/frameworks/base/+/android-5.0.0_r7/libs/androidfw/AssetManager.cpp) + +```c +bool AssetManager::addAssetPath(const String8& path, int32_t* cookie) +{ + AutoMutex _l(mLock); + asset_path ap; + String8 realPath(path); + if (kAppZipName) { + realPath.appendPath(kAppZipName); + } + ap.type = ::getFileType(realPath.string()); + if (ap.type == kFileTypeRegular) { + ap.path = realPath; + } else { + ap.path = path; + ap.type = ::getFileType(path.string()); + if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) { + ALOGW("Asset path %s is neither a directory nor file (type=%d).", + path.string(), (int)ap.type); + return false; + } + } + // Skip if we have it already. + for (size_t i=0; i(i+1); + } + return true; + } + } + ALOGV("In %p Asset %s path: %s", this, + ap.type == kFileTypeDirectory ? "dir" : "zip", ap.path.string()); + // Check that the path has an AndroidManifest.xml + Asset* manifestAsset = const_cast(this)->openNonAssetInPathLocked( + kAndroidManifest, Asset::ACCESS_BUFFER, ap); + if (manifestAsset == NULL) { + // This asset path does not contain any resources. + delete manifestAsset; + return false; + } + delete manifestAsset; + mAssetPaths.add(ap); + // new paths are always added at the end + if (cookie) { + *cookie = static_cast(mAssetPaths.size()); + } +#ifdef HAVE_ANDROID_OS + // Load overlays, if any + asset_path oap; + for (size_t idx = 0; mZipSet.getOverlay(ap.path, idx, &oap); idx++) { + mAssetPaths.add(oap); + } +#endif + if (mResources != NULL) { + // 重新调用该方法去解析package + appendPathToResTable(ap); + } + return true; +} +``` + +```c +bool AssetManager::appendPathToResTable(const asset_path& ap) const { + Asset* ass = NULL; + ResTable* sharedRes = NULL; + bool shared = true; + bool onlyEmptyResources = true; + MY_TRACE_BEGIN(ap.path.string()); + Asset* idmap = openIdmapLocked(ap); + size_t nextEntryIdx = mResources->getTableCount(); + ALOGV("Looking for resource asset in '%s'\n", ap.path.string()); + if (ap.type != kFileTypeDirectory) { + if (nextEntryIdx == 0) { + // The first item is typically the framework resources, + // which we want to avoid parsing every time. + sharedRes = const_cast(this)-> + mZipSet.getZipResourceTable(ap.path); + if (sharedRes != NULL) { + // skip ahead the number of system overlay packages preloaded + nextEntryIdx = sharedRes->getTableCount(); + } + } + if (sharedRes == NULL) { + ass = const_cast(this)-> + mZipSet.getZipResourceTableAsset(ap.path); + if (ass == NULL) { + ALOGV("loading resource table %s\n", ap.path.string()); + ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + if (ass != NULL && ass != kExcludedAsset) { + ass = const_cast(this)-> + mZipSet.setZipResourceTableAsset(ap.path, ass); + } + } + + if (nextEntryIdx == 0 && ass != NULL) { + // If this is the first resource table in the asset + // manager, then we are going to cache it so that we + // can quickly copy it out for others. + ALOGV("Creating shared resources for %s", ap.path.string()); + sharedRes = new ResTable(); + sharedRes->add(ass, idmap, nextEntryIdx + 1, false); +#ifdef HAVE_ANDROID_OS + const char* data = getenv("ANDROID_DATA"); + LOG_ALWAYS_FATAL_IF(data == NULL, "ANDROID_DATA not set"); + String8 overlaysListPath(data); + overlaysListPath.appendPath(kResourceCache); + overlaysListPath.appendPath("overlays.list"); + addSystemOverlays(overlaysListPath.string(), ap.path, sharedRes, nextEntryIdx); +#endif + sharedRes = const_cast(this)-> + mZipSet.setZipResourceTable(ap.path, sharedRes); + } + } + } else { + ALOGV("loading resource table %s\n", ap.path.string()); + ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + shared = false; + } + if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) { + ALOGV("Installing resource asset %p in to table %p\n", ass, mResources); + if (sharedRes != NULL) { + ALOGV("Copying existing resources for %s", ap.path.string()); + mResources->add(sharedRes); + } else { + ALOGV("Parsing resources for %s", ap.path.string()); + mResources->add(ass, idmap, nextEntryIdx + 1, !shared); + } + onlyEmptyResources = false; + if (!shared) { + delete ass; + } + } else { + ALOGV("Installing empty resources in to table %p\n", mResources); + mResources->addEmpty(nextEntryIdx + 1); + } + if (idmap != NULL) { + delete idmap; + } + MY_TRACE_END(); + return onlyEmptyResources; +} +const ResTable* AssetManager::getResTable(bool required) const +{ + ResTable* rt = mResources; + if (rt) { + return rt; + } + // Iterate through all asset packages, collecting resources from each. + AutoMutex _l(mLock); + if (mResources != NULL) { + return mResources; + } + if (required) { + LOG_FATAL_IF(mAssetPaths.size() == 0, "No assets added to AssetManager"); + } + if (mCacheMode != CACHE_OFF && !mCacheValid) { + const_cast(this)->loadFileNameCacheLocked(); + } + mResources = new ResTable(); + updateResourceParamsLocked(); + bool onlyEmptyResources = true; + const size_t N = mAssetPaths.size(); + // 也是调用appendPathToResTable去解析 + for (size_t i=0; i Date: Tue, 13 Nov 2018 16:26:06 +0800 Subject: [PATCH 008/247] add notes --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 49f3f2d4..37108c79 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Android学习笔记 === - [史上最适合Android开发者学习的Go语言教程](https://github.com/CharonChui/GolangStudyNote) +- [史上最适合Android开发者学习的iOS开发教程](https://github.com/CharonChui/iOSStudyNote) - [源码解析][43] - [自定义View详解][1] From 48004afaf2ce32821519116fb16ad2606d6f8b63 Mon Sep 17 00:00:00 2001 From: xuchuanren Date: Sat, 20 Apr 2019 14:55:27 +0800 Subject: [PATCH 009/247] update --- ...\346\225\231\347\250\213(\344\270\200).md" | 2 ++ ...\346\225\231\347\250\213(\344\270\203).md" | 3 ++- ...\346\225\231\347\250\213(\344\270\211).md" | 5 ++++ ...\346\225\231\347\250\213(\344\271\235).md" | 5 ++++ ...\346\225\231\347\250\213(\344\272\214).md" | 2 ++ ...\346\225\231\347\250\213(\344\272\224).md" | 5 ++++ ...\346\225\231\347\250\213(\345\205\253).md" | 5 ++++ ...\346\225\231\347\250\213(\345\205\255).md" | 4 +++ ...\346\225\231\347\250\213(\345\215\201).md" | 3 +++ ...\346\225\231\347\250\213(\345\233\233).md" | 4 +++ .../AudioTrack\347\256\200\344\273\213.md" | 27 +++++++++++++++++++ 11 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 "VideoDevelopment/AudioTrack\347\256\200\344\273\213.md" diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\200).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\200).md" index 1f68694b..1d48ec61 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\200).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\200).md" @@ -524,6 +524,8 @@ fun main(args: Array) { ``` +[下一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) + --- diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" index fc881cf1..3655351c 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" @@ -86,7 +86,8 @@ val sum: (Int, Int) -> Int = { x, y -> x + y } - +[上一篇:Kotlin学习教程(六)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md) +[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) --- diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" index 47b57630..a3c99eec 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" @@ -184,6 +184,11 @@ val s = "abc" val str = "$s.length is ${s.length}" // 求值结果为 "abc.length is 3" ``` + +[上一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) +[下一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" index 47093f89..9307e9c1 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" @@ -246,6 +246,11 @@ inline fun with(t: T, body: T.() -> Unit) { t.body() } +[上一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) +[下一篇:Kotlin学习教程](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%8D%81).md) + + + 参考 === diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" index fe2af3ed..4e101793 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" @@ -78,6 +78,8 @@ fun foo() { // 省略了 ": Unit" ``` +[上一篇:Kotlin学习教程(一)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%80).md) +[下一篇:Kotlin学习教程(三)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%89).md) --- diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" index 2a63c6c7..efa82eef 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" @@ -471,6 +471,11 @@ class MutableUser(val map: MutableMap) { } ``` + +[上一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) +[下一篇:Kotlin学习教程(六)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md) + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" index 8e0b7314..115cfb6f 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" @@ -546,6 +546,11 @@ fun testApply() { `run`函数和`apply`函数很像,只不过run函数是使用最后一行的返回,apply返回当前自己的对象。 + +[上一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) +[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" index fa43d426..709280e1 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" @@ -193,6 +193,10 @@ Toast msg : hello, EXTENSION +[上一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) +[下一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" index 9909a21d..f9cd42b0 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" @@ -37,6 +37,9 @@ Kotlin学习教程(十) 用到 +[上一篇:]() + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" index ed02ddf1..841eab63 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" @@ -568,6 +568,10 @@ for(num in nums) { ``` +[上一篇:Kotlin学习教程(三)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%89).md) +[下一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) + + [1]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%80).md "Kotlin学习教程(一)" --- diff --git "a/VideoDevelopment/AudioTrack\347\256\200\344\273\213.md" "b/VideoDevelopment/AudioTrack\347\256\200\344\273\213.md" new file mode 100644 index 00000000..1a4f8e30 --- /dev/null +++ "b/VideoDevelopment/AudioTrack\347\256\200\344\273\213.md" @@ -0,0 +1,27 @@ +AudioTrack简介 +--- + +Android系统提供了三种播放音频文件的方式: + +- SoundPoll:适合播放短促且对反应速度要求比较高的情况(如游戏音效、按键声等) +- AudioTrack:只支持播放解码后的PCM流,如果是文件的话只致辞WAV格式的音频文件,因为WAV格式的音频文件大部分都是PCM流,AudioTrack不创建解码器,所以只能播放不需要解码的WAV文件,但是CPU占用率低,内存消耗也比较小。因此如果是播放比较短时间的WAV音频文件,建议使用AudioTrack。 +- MediaPlayer:适合比较长且时间要求不那么高的情况,支持多种文件格式,如MP3、WAV、AAC等。其实MediaPlayer是基于AudioTrack的封装,内部也是使用AudioTrack,MediaPlayer在framework层也实例化了AudioTrack,MediaPlayer在framework层进行解码后,生成PCM流,然后代理委托给AudioTrack,最后AudioTrack传递给AudioFlinger进行混音,然后才传递给硬件播放。 + + +AudioTrack播放声音时不能直接把WAV文件传递给AudioTrack进行播放,必须传递buffer,通过write函数把需要播放的缓冲区buffer传递给AudioTrack,然后才能播放。 + + +上面一直说PCM,那PCM究竟是啥? + +简单的说,PCM是一种数据编码格式,CD唱片上刻录的就是直接用PCM格式编码的数据文件,WAV是一种声音文件格式,WAV里面包含的声音数据可以是采用PCM格式编码的声音数据,也可以是采用其他格式编码的声音数据,但是目前一般采用PCM编码的声音数据两者之间的区别就是:PCM是一个通信上的概念,脉冲编码调制。WAV是媒体概念,体现的是封装。WAV文件可以封装PCM编码信息,也可以封装其他编码格式,例如MP3等。 + + + +AudioTrack类可以完成Android平台上音频数据的输出任务。AudioTrack有两种数据加载模式: + +- MODE_STREAM:通过write一次次把音频数据写到AudioTrack中。和平时通过write系统调用往文件中写数据类似,但是这种工作方式每次都需要把数据从用户提供的Buffer中拷贝到AudioTrack中的Buffer,这样会在一定程度上引入延迟,为了解决这个问题,AudioTrack引入了第二种方式。 +- MODE_STATIC:这种模式下,在play之前只需要把所有数据通过一次write调用传递到AudioTrack中的内部缓冲区,后续就不必再传递数据。这种模式适用于铃声这种内存占用小,延时要求高的文件,但是它也有一个缺点,就是一次write的数据不能太多,否则系统无法分配足够的内存来存储全部数据。 + + + + From 68223d177ed1ae6cce4356dab63f73a30ab2849a Mon Sep 17 00:00:00 2001 From: CharonChui Date: Sat, 20 Apr 2019 14:58:08 +0800 Subject: [PATCH 010/247] update --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 37108c79..13fba1b5 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Android学习笔记 - [Android WebRTC简介][22] - [Android音视频开发知识(未完)][23] - [DLNA简介][24] + - [AudioTrack简介] - [图片加载][45] - [Glide简介(上)][25] @@ -458,7 +459,7 @@ Android学习笔记 [211]: https://github.com/CharonChui/AndroidNote/blob/master/RxJavaPart/6.RxJava%E8%AF%A6%E8%A7%A3%E4%B9%8B%E7%BA%BF%E7%A8%8B%E8%B0%83%E5%BA%A6%E5%8E%9F%E7%90%86(%E5%85%AD).md "6.RxJava详解之线程调度原理(六)" [212]: https://github.com/CharonChui/AndroidNote/blob/master/Dagger2/9.Dagger2%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90(%E4%B9%9D).md "9.Dagger2原理分析(九)" [213]: https://github.com/CharonChui/AndroidNote/blob/master/Tools%26Library/%E8%B0%83%E8%AF%95%E5%B9%B3%E5%8F%B0Sonar.md "调试平台Sonar" - +[214]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/AudioTrack%E7%AE%80%E4%BB%8B.md "AudioTrack简介" Developed By From 4dded7a59e48d3d10a32dbe7b161fbc751ca5757 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Sat, 20 Apr 2019 15:03:14 +0800 Subject: [PATCH 011/247] update --- ...55\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" | 2 +- ...55\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" | 2 +- ...55\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" | 2 +- ...55\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" | 2 +- ...55\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" | 2 +- ...55\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" | 2 +- ...55\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" | 2 +- ...55\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" | 2 +- ...55\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" index 3655351c..face6efd 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" @@ -86,7 +86,7 @@ val sum: (Int, Int) -> Int = { x, y -> x + y } -[上一篇:Kotlin学习教程(六)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md) +[上一篇:Kotlin学习教程(六)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md) [下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" index a3c99eec..cb0eb4e2 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" @@ -185,7 +185,7 @@ val str = "$s.length is ${s.length}" // 求值结果为 "abc.length is 3" ``` -[上一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) +[上一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) [下一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" index 9307e9c1..9d2b1b1c 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" @@ -246,7 +246,7 @@ inline fun with(t: T, body: T.() -> Unit) { t.body() } -[上一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) +[上一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) [下一篇:Kotlin学习教程](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%8D%81).md) diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" index 4e101793..a4d12dad 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" @@ -78,7 +78,7 @@ fun foo() { // 省略了 ": Unit" ``` -[上一篇:Kotlin学习教程(一)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%80).md) +[上一篇:Kotlin学习教程(一)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%80).md) [下一篇:Kotlin学习教程(三)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%89).md) --- diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" index efa82eef..8f1e87bb 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" @@ -472,7 +472,7 @@ class MutableUser(val map: MutableMap) { ``` -[上一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) +[上一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) [下一篇:Kotlin学习教程(六)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md) diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" index 115cfb6f..ee364cba 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" @@ -547,7 +547,7 @@ fun testApply() { `run`函数和`apply`函数很像,只不过run函数是使用最后一行的返回,apply返回当前自己的对象。 -[上一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) +[上一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) [下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" index 709280e1..f672121e 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" @@ -193,7 +193,7 @@ Toast msg : hello, EXTENSION -[上一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) +[上一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) [下一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" index f9cd42b0..e70017a7 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" @@ -37,7 +37,7 @@ Kotlin学习教程(十) 用到 -[上一篇:]() +[上一篇:Kotlin学习教程(九)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B9%9D).md) --- diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" index 841eab63..3cc7aa0c 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" @@ -568,7 +568,7 @@ for(num in nums) { ``` -[上一篇:Kotlin学习教程(三)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%89).md) +[上一篇:Kotlin学习教程(三)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%89).md) [下一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) From bdbafb5b83237ac5dcc50eb2c3034fb9e84110b1 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Sat, 20 Apr 2019 15:20:51 +0800 Subject: [PATCH 012/247] update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 13fba1b5..ae5838b2 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Android学习笔记 - [Android WebRTC简介][22] - [Android音视频开发知识(未完)][23] - [DLNA简介][24] - - [AudioTrack简介] + - [AudioTrack简介][214] - [图片加载][45] - [Glide简介(上)][25] From 9c1c70045ad5d1b8a13996975f984bde57c5151b Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 21 Jun 2019 11:13:08 +0800 Subject: [PATCH 013/247] =?UTF-8?q?add=20OOM=E9=97=AE=E9=A2=98=E5=88=86?= =?UTF-8?q?=E6=9E=90.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...56\351\242\230\345\210\206\346\236\220.md" | 746 ++++++++++++++++++ 1 file changed, 746 insertions(+) create mode 100644 "AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" diff --git "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" new file mode 100644 index 00000000..583fa429 --- /dev/null +++ "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" @@ -0,0 +1,746 @@ +OOM问题分析 +=== + +## 简介 + +OOM(OutOfMemoryError),最近线上版本出现了大量线程OOM的crash,尤其是华为Android 9.0系统的手机,占总OOM量的85%左右。 + +### OOM分类 + +#### [XXXClassName] of length XXX would overflow“是系统限制String/Array的长度所致,这种情况比较少。 + +#### java.lang.OutOfMemoryError: Failed to allocate a XXX byte allocation with XXX free bytes and XXXKB until OOM + +通常情况下是因为`java`堆内存不足导致的,即`Runtime.getRuntime().maxMemory()`获取到的最大内存无法满足要申请的内存大小时,这种情况比较好模拟,例如我们可以通过`new byte[]`的方式来申请超过`maxMemory()`的内存,但是也有一些情况是堆内存充裕,而且设备内存也充裕的情况下发生的。 +#### java.lang.OutOfMemoryError: Could not allocate JNI Env(代号JNIEnv) + +- cat /proc/pid/limits 描述linux系统对对应进程的限制: +``` +Limit Soft Limit Hard Limit Units +Max cpu time unlimited unlimited seconds +Max file size unlimited unlimited bytes +Max data size unlimited unlimited bytes +Max stack size 8388608 unlimited bytes +Max core file size 0 unlimited bytes +Max resident set unlimited unlimited bytes +Max processes 17235 17235 processes +Max open files 32768 32768 files +Max locked memory 67108864 67108864 bytes +Max address space unlimited unlimited bytes +Max file locks unlimited unlimited locks +Max pending signals 17235 17235 signals +Max msgqueue size 819200 819200 bytes +Max nice priority 40 40 +Max realtime priority 0 0 +Max realtime timeout unlimited unlimited us +``` +里面`Max open files`表示每个进程最大打开文件的数目,进程每打开一个文件就会产生一个文件描述符`fd`(记录在/proc/pid/fd中) + +验证: 触发大量的网络连接或者打开文件,每个连接处于独立的线程中并报出,每打开一个socket都会增加一个fd +```java + private Runnable increaseFDRunnable = new Runnable() { + @Override + public void run() { + try { + for (int i = 0; i < 1000; i++) { + new BufferedReader(new FileReader("/proc/" + Process.myPid() + "/status")); + } + Thread.sleep(Long.MAX_VALUE); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + }; +``` +#### java.lang.OutOfMemoryError: pthread_create(1040KB statck) failed: Out of memory(代号1040) + +根据OOM的crash日志,我们发现都是从`Thread.start()`方法开始的, +```java + public synchronized void start() { + /** + * This method is not invoked for the main method thread or "system" + * group threads created/set up by the VM. Any new functionality added + * to this method in the future may have to also be added to the VM. + * + * A zero status value corresponds to state "NEW". + */ + // Android-changed: throw if 'started' is true + if (threadStatus != 0 || started) + throw new IllegalThreadStateException(); + + /* Notify the group that this thread is about to be started + * so that it can be added to the group's list of threads + * and the group's unstarted count can be decremented. */ + group.add(this); + + started = false; + try { + nativeCreate(this, stackSize, daemon); + started = true; + } finally { + try { + if (!started) { + group.threadStartFailed(this); + } + } catch (Throwable ignore) { + /* do nothing. If start0 threw a Throwable then + it will be passed up the call stack */ + } + } + } +``` +上面的核心方法是`nativeCreate(this, stackSize, daemon);` +这个方法有三个参数: +- this : Thread对象自身 +- stackSize : the desired stack size for the new thread, or zero to indicate that this parameter is to be ignored.该新创建的线程的栈的大小,单位是byte,一般默认情况下都是0,我们全局搜这个变量复制的地方,可以看到Thread有一个构造函数可以设置这个值 +``` +public Thread(ThreadGroup group, Runnable target, String name, + long stackSize) { + init(group, target, name, stackSize); +} +``` +- daemon : Whether or not the thread is a daemon thread. + +而nativeCreate方法的native实现,是在art/runtime/native/java_lang_thread.cc中,因为OOM主要是在Android 9.0系统发生,所以这里基于9.0系统的源码分析:`https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/native/java_lang_Thread.cc`。 +```java +static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size, + jboolean daemon) { + // There are sections in the zygote that forbid thread creation. + Runtime* runtime = Runtime::Current(); + if (runtime->IsZygote() && runtime->IsZygoteNoThreadSection()) { + jclass internal_error = env->FindClass("java/lang/InternalError"); + CHECK(internal_error != nullptr); + env->ThrowNew(internal_error, "Cannot create threads in zygote"); + return; + } + Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE); +} +``` +里面又会调用到[art/runtime/thread.cc](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/thread.cc)中的Thread::CreateNativeThread方法: +```java +void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) { + CHECK(java_peer != nullptr); + Thread* self = static_cast(env)->GetSelf(); + if (VLOG_IS_ON(threads)) { + ScopedObjectAccess soa(env); + ArtField* f = jni::DecodeArtField(WellKnownClasses::java_lang_Thread_name); + ObjPtr java_name = + f->GetObject(soa.Decode(java_peer))->AsString(); + std::string thread_name; + if (java_name != nullptr) { + thread_name = java_name->ToModifiedUtf8(); + } else { + thread_name = "(Unnamed)"; + } + VLOG(threads) << "Creating native thread for " << thread_name; + self->Dump(LOG_STREAM(INFO)); + } + Runtime* runtime = Runtime::Current(); + // Atomically start the birth of the thread ensuring the runtime isn't shutting down. + bool thread_start_during_shutdown = false; + { + MutexLock mu(self, *Locks::runtime_shutdown_lock_); + if (runtime->IsShuttingDownLocked()) { + thread_start_during_shutdown = true; + } else { + runtime->StartThreadBirth(); + } + } + if (thread_start_during_shutdown) { + ScopedLocalRef error_class(env, env->FindClass("java/lang/InternalError")); + env->ThrowNew(error_class.get(), "Thread starting during runtime shutdown"); + return; + } + + // 1. native层创建thread + Thread* child_thread = new Thread(is_daemon); + // Use global JNI ref to hold peer live while child thread starts. + child_thread->tlsPtr_.jpeer = env->NewGlobalRef(java_peer); + // 2. FixStackSize方法里面会返回具体的Stack内存的大小 + stack_size = FixStackSize(stack_size); + // Thread.start is synchronized, so we know that nativePeer is 0, and know that we're not racing + // to assign it. + env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, + reinterpret_cast(child_thread)); + + // 3. java中的每一个线程都都应一个JNIEnv结构,这里的JNIEnvExt就是ART中的JNIEnv。下面的注释说明的很明白,这里可能会有oom + // Try to allocate a JNIEnvExt for the thread. We do this here as we might be out of memory and + // do not have a good way to report this on the child's side. + std::string error_msg; + std::unique_ptr child_jni_env_ext( + JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM(), &error_msg)); + int pthread_create_result = 0; + if (child_jni_env_ext.get() != nullptr) { + // 4. child_jni_env_ext.get() != nullptr 才会继续 + pthread_t new_pthread; + pthread_attr_t attr; + child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get(); + CHECK_PTHREAD_CALL(pthread_attr_init, (&attr), "new thread"); + CHECK_PTHREAD_CALL(pthread_attr_setdetachstate, (&attr, PTHREAD_CREATE_DETACHED), + "PTHREAD_CREATE_DETACHED"); + CHECK_PTHREAD_CALL(pthread_attr_setstacksize, (&attr, stack_size), stack_size); + // 5. 调用pthread_create创建线程,并返回结果 + pthread_create_result = pthread_create(&new_pthread, + &attr, + Thread::CreateCallback, + child_thread); + CHECK_PTHREAD_CALL(pthread_attr_destroy, (&attr), "new thread"); + if (pthread_create_result == 0) { + // 6. 结果为0才是创建成功 + // pthread_create started the new thread. The child is now responsible for managing the + // JNIEnvExt we created. + // Note: we can't check for tmp_jni_env == nullptr, as that would require synchronization + // between the threads. + child_jni_env_ext.release(); + return; + } + } + // Either JNIEnvExt::Create or pthread_create(3) failed, so clean up. + { + MutexLock mu(self, *Locks::runtime_shutdown_lock_); + runtime->EndThreadBirth(); + } + // Manually delete the global reference since Thread::Init will not have been run. + env->DeleteGlobalRef(child_thread->tlsPtr_.jpeer); + child_thread->tlsPtr_.jpeer = nullptr; + delete child_thread; + child_thread = nullptr; + // TODO: remove from thread group? + env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, 0); + { + std::string msg(child_jni_env_ext.get() == nullptr ? + StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) : + StringPrintf("pthread_create (%s stack) failed: %s", + PrettySize(stack_size).c_str(), strerror(pthread_create_result))); + ScopedObjectAccess soa(env); + soa.Self()->ThrowOutOfMemoryError(msg.c_str()); + } +} +``` +上面代码太多,分了四个部分: +1. native层创建thread +2. FixStackSize方法里面会返回具体的Stack内存的大小 +``` +static size_t FixStackSize(size_t stack_size) { + // A stack size of zero means "use the default". + if (stack_size == 0) { + stack_size = Runtime::Current()->GetDefaultStackSize(); + } + // Dalvik used the bionic pthread default stack size for native threads, + // so include that here to support apps that expect large native stacks. + stack_size += 1 * MB; + // It's not possible to request a stack smaller than the system-defined PTHREAD_STACK_MIN. + if (stack_size < PTHREAD_STACK_MIN) { + stack_size = PTHREAD_STACK_MIN; + } + if (Runtime::Current()->ExplicitStackOverflowChecks()) { + // It's likely that callers are trying to ensure they have at least a certain amount of + // stack space, so we should add our reserved space on top of what they requested, rather + // than implicitly take it away from them. + // 8k + stack_size += GetStackOverflowReservedBytes(kRuntimeISA); + } else { + // If we are going to use implicit stack checks, allocate space for the protected + // region at the bottom of the stack. + // 8k 8k + stack_size += Thread::kStackOverflowImplicitCheckSize + + GetStackOverflowReservedBytes(kRuntimeISA); + } + // Some systems require the stack size to be a multiple of the system page size, so round up. + stack_size = RoundUp(stack_size, kPageSize); + return stack_size; +} +``` + +// static const size_t kStackOverflowImplicitCheckSize = 8 * KB; +上面kStackOverflowImplicitCheckSize的值是8k,而前面是1m,1024k+8k+8k=1040k,这就是为什么crash信息里面java.lang.OutOfMemoryError pthread_create (1040KB stack) failed: Out of memory是1040kb的原因。 + +3. java中的每一个线程都都应一个JNIEnv结构,这里的JNIEnvExt就是ART中的JNIEnv。下面的注释说明的很明白,这里可能会有oom,这里具体要看[JNIEnvExt::Create()](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/jni_env_ext.cc)方法 +``` +const JNINativeInterface* JNIEnvExt::table_override_ = nullptr; + +JNIEnvExt* JNIEnvExt::Create(Thread* self_in, JavaVMExt* vm_in, std::string* error_msg) { + // 调用new JNIEnvExt构造函数 + std::unique_ptr ret(new JNIEnvExt(self_in, vm_in, error_msg)); + if (CheckLocalsValid(ret.get())) { + return ret.release(); + } + return nullptr; +} +``` +代码中发现特确实会返回nullptr,只有在CheckLocalsValid(ret.get())返回false的时候才会 +``` +bool JNIEnvExt::CheckLocalsValid(JNIEnvExt* in) NO_THREAD_SAFETY_ANALYSIS { + if (in == nullptr) { + return false; + } + return in->locals_.IsValid(); +} +``` +所以根本原因是因为 JNIEnvExt::table_override_ 仍然为nullptr导致的返回false +而它的赋值在GetFunctionTable方法中 +``` +const JNINativeInterface* JNIEnvExt::GetFunctionTable(bool check_jni) { + const JNINativeInterface* override = JNIEnvExt::table_override_; + if (override != nullptr) { + return override; + } + return check_jni ? GetCheckJniNativeInterface() : GetJniNativeInterface(); +} +``` +GetFunctionTable方法又被构造函数调用 +``` +JNIEnvExt::JNIEnvExt(Thread* self_in, JavaVMExt* vm_in, std::string* error_msg) + : self_(self_in), + vm_(vm_in), + local_ref_cookie_(kIRTFirstSegment), + locals_(kLocalsInitial, kLocal, IndirectReferenceTable::ResizableCapacity::kYes, error_msg), + monitors_("monitors", kMonitorsInitial, kMonitorsMax), + critical_(0), + check_jni_(false), + runtime_deleted_(false) { + MutexLock mu(Thread::Current(), *Locks::jni_function_table_lock_); + check_jni_ = vm_in->IsCheckJniEnabled(); + functions = GetFunctionTable(check_jni_); + unchecked_functions_ = GetJniNativeInterface(); +} +``` +构造函数中又使用了[IndirectReferenceTable](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/indirect_reference_table.cc)类,他的构造函数为 +``` +IndirectReferenceTable::IndirectReferenceTable(size_t max_count, + IndirectRefKind desired_kind, + ResizableCapacity resizable, + std::string* error_msg) + : segment_state_(kIRTFirstSegment), + kind_(desired_kind), + max_entries_(max_count), + current_num_holes_(0), + resizable_(resizable) { + CHECK(error_msg != nullptr); + CHECK_NE(desired_kind, kHandleScopeOrInvalid); + // Overflow and maximum check. + CHECK_LE(max_count, kMaxTableSizeInBytes / sizeof(IrtEntry)); + // max_count是常量512,而sizeof(IrtEntry)是8,所以table_bytes = 512 * 8 = 4k + const size_t table_bytes = max_count * sizeof(IrtEntry); + table_mem_map_.reset(MemMap::MapAnonymous("indirect ref table", nullptr, table_bytes, + PROT_READ | PROT_WRITE, false, false, error_msg)); + if (table_mem_map_.get() == nullptr && error_msg->empty()) { + *error_msg = "Unable to map memory for indirect ref table"; + } + if (table_mem_map_.get() != nullptr) { + table_ = reinterpret_cast(table_mem_map_->Begin()); + } else { + table_ = nullptr; + } + segment_state_ = kIRTFirstSegment; + last_known_previous_state_ = kIRTFirstSegment; +} +``` +如果上面失败的话,那就只有一种情况就是 MemMap::MapAnonymous 失败了,我们继续看[MemMap](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/mem_map.cc) +``` +MemMap* MemMap::MapAnonymous(const char* name, + uint8_t* expected_ptr, + size_t byte_count, + int prot, + bool low_4gb, + bool reuse, + std::string* error_msg, + bool use_ashmem) { +#ifndef __LP64__ + UNUSED(low_4gb); +#endif + use_ashmem = use_ashmem && !kIsTargetLinux; + if (byte_count == 0) { + return new MemMap(name, nullptr, 0, nullptr, 0, prot, false); + } + size_t page_aligned_byte_count = RoundUp(byte_count, kPageSize); + int flags = MAP_PRIVATE | MAP_ANONYMOUS; + if (reuse) { + // reuse means it is okay that it overlaps an existing page mapping. + // Only use this if you actually made the page reservation yourself. + CHECK(expected_ptr != nullptr); + DCHECK(ContainedWithinExistingMap(expected_ptr, byte_count, error_msg)) << *error_msg; + flags |= MAP_FIXED; + } + if (use_ashmem) { + if (!kIsTargetBuild) { + // When not on Android (either host or assuming a linux target) ashmem is faked using + // files in /tmp. Ensure that such files won't fail due to ulimit restrictions. If they + // will then use a regular mmap. + struct rlimit rlimit_fsize; + CHECK_EQ(getrlimit(RLIMIT_FSIZE, &rlimit_fsize), 0); + use_ashmem = (rlimit_fsize.rlim_cur == RLIM_INFINITY) || + (page_aligned_byte_count < rlimit_fsize.rlim_cur); + } + } + unique_fd fd; + if (use_ashmem) { + // android_os_Debug.cpp read_mapinfo assumes all ashmem regions associated with the VM are + // prefixed "dalvik-". + std::string debug_friendly_name("dalvik-"); + debug_friendly_name += name; + // 1. 创建 + fd.reset(ashmem_create_region(debug_friendly_name.c_str(), page_aligned_byte_count)); + if (fd.get() == -1) { + // We failed to create the ashmem region. Print a warning, but continue + // anyway by creating a true anonymous mmap with an fd of -1. It is + // better to use an unlabelled anonymous map than to fail to create a + // map at all. + PLOG(WARNING) << "ashmem_create_region failed for '" << name << "'"; + } else { + // We succeeded in creating the ashmem region. Use the created ashmem + // region as backing for the mmap. + flags &= ~MAP_ANONYMOUS; + } + } + // We need to store and potentially set an error number for pretty printing of errors + int saved_errno = 0; + // 2. 调用mmap映射到用户态内存地址空间 + void* actual = MapInternal(expected_ptr, + page_aligned_byte_count, + prot, + flags, + fd.get(), + 0, + low_4gb); + saved_errno = errno; + if (actual == MAP_FAILED) { + if (error_msg != nullptr) { + if (kIsDebugBuild || VLOG_IS_ON(oat)) { + PrintFileToLog("/proc/self/maps", LogSeverity::WARNING); + } + *error_msg = StringPrintf("Failed anonymous mmap(%p, %zd, 0x%x, 0x%x, %d, 0): %s. " + "See process maps in the log.", + expected_ptr, + page_aligned_byte_count, + prot, + flags, + fd.get(), + strerror(saved_errno)); + } + return nullptr; + } + if (!CheckMapRequest(expected_ptr, actual, page_aligned_byte_count, error_msg)) { + return nullptr; + } + return new MemMap(name, reinterpret_cast(actual), byte_count, actual, + page_aligned_byte_count, prot, reuse); +} +``` +上面的两个步骤中,不论第一个步骤执行成功与否,都会执行第二步,但是执行的行为不同 + +- 如果第一步执行成功,就会通过Andorid的匿名共享内存(Anonymous Shared Memory)分配4KB(一个page)内核态内存,然后再通过Linux的mmap调用映射到用户态虚拟内存地址空间。 +- 如果第一步执行失败,第二步就会通过Linux的mmap调用创建一段虚拟内存。 + +而上面失败的情况主要有: +- 第一步失败的情况一般是内核分配内存失败,这种情况下,整个设备OS的内存应该都处于非常紧张的状态。 +- 第二步失败的情况一般是进程虚拟内存地址空间耗尽。而且会打印Failed anonymous mmap的错误 + +所以这里child_jni_env_ext.get() == nullptr 通常是因为第二步失败,也就是进程虚拟内存地址空间耗尽。所以这就是代号JNIEnv OOM的原因。 + +``` +__BIONIC_ERRDEF( ENOMEM , 12, "Out of memory" ) +__BIONIC_ERRDEF( EMFILE , 24, "Too many open files" ) +``` +当然代号JNIEnv还有一种原因就是FD打开太多,到达了最大限制。 + +4. child_jni_env_ext.get() != nullptr 才会继续 +5. 调用pthread_create创建线程,并返回结果 +看一下[pthread_create](https://android.googlesource.com/platform/bionic.git/+/refs/tags/android-9.0.0_r41/libc/bionic/pthread_create.cpp)的代码 +``` +__BIONIC_WEAK_FOR_NATIVE_BRIDGE +int pthread_create(pthread_t* thread_out, pthread_attr_t const* attr, + void* (*start_routine)(void*), void* arg) { + ErrnoRestorer errno_restorer; + pthread_attr_t thread_attr; + if (attr == NULL) { + pthread_attr_init(&thread_attr); + } else { + thread_attr = *attr; + attr = NULL; // Prevent misuse below. + } + pthread_internal_t* thread = NULL; + void* child_stack = NULL; + // 1. 分配该线程对应的栈内存空间,如果返回result != 0 就直接返回result就说明失败了 + int result = __allocate_thread(&thread_attr, &thread, &child_stack); + if (result != 0) { + return result; + } + // Create a lock for the thread to wait on once it starts so we can keep + // it from doing anything until after we notify the debugger about it + // + // This also provides the memory barrier we need to ensure that all + // memory accesses previously performed by this thread are visible to + // the new thread. + thread->startup_handshake_lock.init(false); + thread->startup_handshake_lock.lock(); + thread->start_routine = start_routine; + thread->start_routine_arg = arg; + thread->set_cached_pid(getpid()); + int flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM | + CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID; + void* tls = reinterpret_cast(thread->tls); +#if defined(__i386__) + // On x86 (but not x86-64), CLONE_SETTLS takes a pointer to a struct user_desc rather than + // a pointer to the TLS itself. + user_desc tls_descriptor; + __init_user_desc(&tls_descriptor, false, tls); + tls = &tls_descriptor; +#endif + // 2. linux系统调用clone,执行真正的创建动作,而这个clone是创建新进程,Unix里面其实只有进程,而线程是POSIX标准定义的,因此这里的clone只是实现线程的一种手段。 clone后父进程和子进程共享内存, 因此当两个进程的内存共享之后,完全就符合“线程”的定义了。 + int rc = clone(__pthread_start, child_stack, flags, thread, &(thread->tid), tls, &(thread->tid)); + if (rc == -1) { + int clone_errno = errno; + // We don't have to unlock the mutex at all because clone(2) failed so there's no child waiting to + // be unblocked, but we're about to unmap the memory the mutex is stored in, so this serves as a + // reminder that you can't rewrite this function to use a ScopedPthreadMutexLocker. + thread->startup_handshake_lock.unlock(); + if (thread->mmap_size != 0) { + munmap(thread->attr.stack_base, thread->mmap_size); + } + async_safe_format_log(ANDROID_LOG_WARN, "libc", "pthread_create failed: clone failed: %s", + strerror(clone_errno)); + return clone_errno; + } + int init_errno = __init_thread(thread); + if (init_errno != 0) { + // Mark the thread detached and replace its start_routine with a no-op. + // Letting the thread run is the easiest way to clean up its resources. + atomic_store(&thread->join_state, THREAD_DETACHED); + __pthread_internal_add(thread); + thread->start_routine = __do_nothing; + thread->startup_handshake_lock.unlock(); + return init_errno; + } + // Publish the pthread_t and unlock the mutex to let the new thread start running. + *thread_out = __pthread_internal_add(thread); + thread->startup_handshake_lock.unlock(); + return 0; +} +``` +上面第一步中__allocate_thread方法的源码为: +``` +static int __allocate_thread(pthread_attr_t* attr, pthread_internal_t** threadp, void** child_stack) { + size_t mmap_size; + uint8_t* stack_top; + if (attr->stack_base == NULL) { + // The caller didn't provide a stack, so allocate one. + // Make sure the stack size and guard size are multiples of PAGE_SIZE. + if (__builtin_add_overflow(attr->stack_size, attr->guard_size, &mmap_size)) return EAGAIN; + if (__builtin_add_overflow(mmap_size, sizeof(pthread_internal_t), &mmap_size)) return EAGAIN; + mmap_size = __BIONIC_ALIGN(mmap_size, PAGE_SIZE); + attr->guard_size = __BIONIC_ALIGN(attr->guard_size, PAGE_SIZE); + // 调用mmap分配栈内存,而mmap分配的内存赋值给了stack_base, stack_base不光是线程执行的栈,其中还存储了线程的其他信息(线程名、ThreadLocal变量等,这些信息都定义在pthread_internal_t结构体中),而这个具体的大小就是前面我们分析的 1M + 8K + 8K = 1040K + attr->stack_base = __create_thread_mapped_space(mmap_size, attr->guard_size); + if (attr->stack_base == NULL) { + return EAGAIN; + } + stack_top = reinterpret_cast(attr->stack_base) + mmap_size; + } else { + // Remember the mmap size is zero and we don't need to free it. + mmap_size = 0; + stack_top = reinterpret_cast(attr->stack_base) + attr->stack_size; + } + // Mapped space(or user allocated stack) is used for: + // pthread_internal_t + // thread stack (including guard) + // To safely access the pthread_internal_t and thread stack, we need to find a 16-byte aligned boundary. + stack_top = reinterpret_cast( + (reinterpret_cast(stack_top) - sizeof(pthread_internal_t)) & ~0xf); + pthread_internal_t* thread = reinterpret_cast(stack_top); + if (mmap_size == 0) { + // If thread was not allocated by mmap(), it may not have been cleared to zero. + // So assume the worst and zero it. + memset(thread, 0, sizeof(pthread_internal_t)); + } + attr->stack_size = stack_top - reinterpret_cast(attr->stack_base); + thread->mmap_size = mmap_size; + thread->attr = *attr; + if (!__init_tls(thread)) { + if (thread->mmap_size != 0) munmap(thread->attr.stack_base, thread->mmap_size); + return EAGAIN; + } + __init_thread_stack_guard(thread); + *threadp = thread; + *child_stack = stack_top; + return 0; +} +``` +而__create_thread_mapped_space方法的源码为 +``` +static void* __create_thread_mapped_space(size_t mmap_size, size_t stack_guard_size) { + // Create a new private anonymous map. + int prot = PROT_READ | PROT_WRITE; + // MAP_ANONYMOUS即匿名内存映射是在Linux中分配大块内存的常用方式。其分配的是虚拟内存,对应页的物理内存并不会立即分配,而是在用到的时候,触发内核的缺页中断,然后中断处理函数再分配物理内存。 + int flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE; + void* space = mmap(NULL, mmap_size, prot, flags, -1, 0); + if (space == MAP_FAILED) { + async_safe_format_log(ANDROID_LOG_WARN, + "libc", + "pthread_create failed: couldn't allocate %zu-bytes mapped space: %s", + mmap_size, strerror(errno)); + return NULL; + } + // Stack is at the lower end of mapped space, stack guard region is at the lower end of stack. + // Set the stack guard region to PROT_NONE, so we can detect thread stack overflow. + if (mprotect(space, stack_guard_size, PROT_NONE) == -1) { + async_safe_format_log(ANDROID_LOG_WARN, "libc", + "pthread_create failed: couldn't mprotect PROT_NONE %zu-byte stack guard region: %s", + stack_guard_size, strerror(errno)); + munmap(space, mmap_size); + return NULL; + } + return space; +} +``` + +而对于结果不为0的情况,那就只能是这里mmap分配虚拟内存失败。 +所以代号1040的OOM也是因为虚拟内存分配失败导致的。 + +6. 结果为0才是创建成功 +7. 调用ThrowOutOfMemoryError报出错误信息 +如果child_jni_env_ext.get() == nullptr则报"Could not allocate JNI Env: %s", error_msg.c_str()的错误 +否则如果pthread_create_result != 0则报"pthread_create (%s stack) failed: %s" + + ``` + void Thread::ThrowOutOfMemoryError(const char* msg) { + LOG(ERROR) << StringPrintf("Throwing OutOfMemoryError \"%s\"%s", + msg, (throwing_OutOfMemoryError_ ? " (recursive case)" : "")); + ThrowLocation throw_location = GetCurrentLocationForThrow(); + if (!throwing_OutOfMemoryError_) { + throwing_OutOfMemoryError_ = true; + ThrowNewException(throw_location, "Ljava/lang/OutOfMemoryError;", msg); + throwing_OutOfMemoryError_ = false; + } else { + Dump(LOG(ERROR)); // The pre-allocated OOME has no stack, so help out and log one. + SetException(throw_location, Runtime::Current()->GetPreAllocatedOutOfMemoryError()); + } + } + ``` + + + +最后总结一下: 不管是代号JNIEnv还是1040的OOM都是因为进程内虚拟内存地址空间耗尽导致的。 +在一个32位系统中,如果是4G的内存空间,系统内核将使用最上层的1G虚拟空间,用户空间的内存就只剩下3G或者更少,而创建一个进程需要1040k的虚拟内存,所以假设创建一个线程什么都不干,那最多也只能最大能创建3000个线程 + + + +而我在项目中遇到的问题是第二个也就是线程过多导致的,Android系统基于linux,所以linux的限制对Android同样实用, + +- cat /proc/sys/kernel/threads-max 规定了每个进程创建线程数量的上限 + +验证:创建大量空线程,不做任何事情,直接sleep. +```java +private Runnable emptyRunnable = new Runnable() { + @Override + public void run() { + try { + for (int i = 0; i < 3000 ; i++) { + Thread.sleep(Long.MAX_VALUE); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +}; +``` + + +连接手机 +``` +adb shell +ps -A -l +``` + +``` +1 S 0 15304 2 0 19 0 - 0 0 ? 00:00:00 kworker/7:2 +5 S 1000 15701 1001 0 19 0 - 1092845 0 ? 00:00:00 ndroid.keychain +5 S 10458 15834 1001 0 29 -10 - 1100514 0 ? 00:00:02 com.example.oom +5 S 10014 15956 1001 0 19 0 - 1106511 0 ? 00:00:01 com.android.mms +1 S 0 16275 2 0 19 0 - 0 0 ? 00:00:00 kworker/1:2 +1 S 0 16542 2 0 19 0 - 0 0 ? 00:00:00 kworker/3:2 +1 S 0 16603 2 0 19 0 - 0 0 ? 00:00:00 kworker/2:2 +``` + +pid为15834 +``` +cat proc/15834/status +``` + +``` +PD1806:/ $ cat proc/15834/status +Name: com.example.oom +Umask: 0077 +State: S (sleeping) +Tgid: 15834 +Ngid: 0 +Pid: 15834 +PPid: 1001 +TracerPid: 0 +Uid: 10458 10458 10458 10458 +Gid: 10458 10458 10458 10458 +FDSize: 128 +Groups: 9997 20458 50458 +VmPeak: 4403108 kB +VmSize: 4402056 kB +VmLck: 0 kB +VmPin: 0 kB +VmHWM: 49108 kB +VmRSS: 48920 kB +RssAnon: 9268 kB +RssFile: 39540 kB +RssShmem: 112 kB +VmData: 1737808 kB +VmStk: 8192 kB +VmExe: 20 kB +VmLib: 163804 kB +VmPTE: 1000 kB +VmPMD: 32 kB +VmSwap: 15776 kB +Threads: 17 +SigQ: 0/21568 +SigPnd: 0000000000000000 +ShdPnd: 0000000000000000 +SigBlk: 0000000000001204 +SigIgn: 0000000000000000 +SigCgt: 00000006400084f8 +CapInh: 0000000000000000 +CapPrm: 0000000000000000 +CapEff: 0000000000000000 +CapBnd: 0000000000000000 +CapAmb: 0000000000000000 +Seccomp: 2 +Cpus_allowed: ff +Cpus_allowed_list: 0-7 +Mems_allowed: 1 +Mems_allowed_list: 0 +voluntary_ctxt_switches: 2132 +nonvoluntary_ctxt_switches: 328 + +``` + +``` +当线程数(可以在/proc/pid/status 中的threads项实时查看)超过/proc/sys/kernel/threads-max 中规定的上限时产生 OOM 崩溃。 +``` + +## 定位验证方法: + +Thread.UncaughtExceptionHandler捕获到OutOfMemoryError时记录/proc/pid目录下的如下信息: + +- /proc/pid/fd目录下文件数(fd数) +- /proc/pid/status中threads项(当前线程数目) +- 当前设备的内存信息 +- OOM的日志信息(出了堆栈信息还包含其他的一些warning信息 +- 在灰度版本中通过一个定时器10分钟dump出应用所有的线程,当线程数超过一定阈值时,将当前的线程上报并预警,通过对这种异常情况的捕捉 + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + From ffabfc92c811387f16d1e8b189fd61c309abde8c Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 21 Jun 2019 11:19:51 +0800 Subject: [PATCH 014/247] =?UTF-8?q?add=20OOM=E9=97=AE=E9=A2=98=E5=88=86?= =?UTF-8?q?=E6=9E=90.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ae5838b2..66214b8f 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,7 @@ Android学习笔记 - [RecyclerView专题][84] - [ConstraintLaayout简介][194] - [Android WorkManager][208] + - [OOM问题分析][215] - [Java基础及算法][53] - [八种排序算法][189] @@ -460,6 +461,7 @@ Android学习笔记 [212]: https://github.com/CharonChui/AndroidNote/blob/master/Dagger2/9.Dagger2%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90(%E4%B9%9D).md "9.Dagger2原理分析(九)" [213]: https://github.com/CharonChui/AndroidNote/blob/master/Tools%26Library/%E8%B0%83%E8%AF%95%E5%B9%B3%E5%8F%B0Sonar.md "调试平台Sonar" [214]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/AudioTrack%E7%AE%80%E4%BB%8B.md "AudioTrack简介" +[215]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/OOM%E9%97%AE%E9%A2%98%E5%88%86%E6%9E%90.md "OOM问题分析" Developed By From 8acd68eea2dff5bc97036977127fe6c15f17157a Mon Sep 17 00:00:00 2001 From: Charon Date: Fri, 21 Jun 2019 16:32:07 +0800 Subject: [PATCH 015/247] =?UTF-8?q?Update=20OOM=E9=97=AE=E9=A2=98=E5=88=86?= =?UTF-8?q?=E6=9E=90.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...56\351\242\230\345\210\206\346\236\220.md" | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" index 583fa429..0e7144b5 100644 --- "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" +++ "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" @@ -9,7 +9,7 @@ OOM(OutOfMemoryError),最近线上版本出现了大量线程OOM的crash,尤 #### [XXXClassName] of length XXX would overflow“是系统限制String/Array的长度所致,这种情况比较少。 -#### java.lang.OutOfMemoryError: Failed to allocate a XXX byte allocation with XXX free bytes and XXXKB until OOM +#### java.lang.OutOfMemoryError: "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM"; 通常情况下是因为`java`堆内存不足导致的,即`Runtime.getRuntime().maxMemory()`获取到的最大内存无法满足要申请的内存大小时,这种情况比较好模拟,例如我们可以通过`new byte[]`的方式来申请超过`maxMemory()`的内存,但是也有一些情况是堆内存充裕,而且设备内存也充裕的情况下发生的。 #### java.lang.OutOfMemoryError: Could not allocate JNI Env(代号JNIEnv) @@ -20,15 +20,15 @@ Limit Soft Limit Hard Limit Units Max cpu time unlimited unlimited seconds Max file size unlimited unlimited bytes Max data size unlimited unlimited bytes -Max stack size 8388608 unlimited bytes +Max stack size 8388608 unlimited bytes // 整个系统的 Max core file size 0 unlimited bytes Max resident set unlimited unlimited bytes -Max processes 17235 17235 processes -Max open files 32768 32768 files -Max locked memory 67108864 67108864 bytes +Max processes 17235 17235 processes // 整个系统的最大进程数,底层只有进程,线程也是通过进程实现的 +Max open files 32768 32768 files // 每个进程最大打开文件的数量 +Max locked memory 67108864 67108864 bytes // 线程创建过程中分配线程私有stack使用的mmap调用没有设置MAP_LOCKED,所以这个限制与线程创建过程无关 Max address space unlimited unlimited bytes Max file locks unlimited unlimited locks -Max pending signals 17235 17235 signals +Max pending signals 17235 17235 signals // c层信号个数阈值,与线程创建过程无关 Max msgqueue size 819200 819200 bytes Max nice priority 40 40 Max realtime priority 0 0 @@ -212,6 +212,7 @@ void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_siz { std::string msg(child_jni_env_ext.get() == nullptr ? StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) : + // 具体的错误信息由pthread_create_result的返回的错误码给出。 StringPrintf("pthread_create (%s stack) failed: %s", PrettySize(stack_size).c_str(), strerror(pthread_create_result))); ScopedObjectAccess soa(env); @@ -338,7 +339,7 @@ IndirectReferenceTable::IndirectReferenceTable(size_t max_count, last_known_previous_state_ = kIRTFirstSegment; } ``` -如果上面失败的话,那就只有一种情况就是 MemMap::MapAnonymous 失败了,我们继续看[MemMap](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/mem_map.cc) +如果上面失败的话,那就只有一种情况就是 MemMap::MapAnonymous 失败了,而MemMap::MapAnonymous的作用是为JNIEnv结构体中的Indirect_Reference_table(C层用于存储JNI局部/全局变量)申请内存,我们继续看[MemMap](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/mem_map.cc) ``` MemMap* MemMap::MapAnonymous(const char* name, uint8_t* expected_ptr, @@ -383,6 +384,7 @@ MemMap* MemMap::MapAnonymous(const char* name, debug_friendly_name += name; // 1. 创建 fd.reset(ashmem_create_region(debug_friendly_name.c_str(), page_aligned_byte_count)); + // == -1 就说明是fd超过了系统限制的最大fd量,错误信息中会有Too many open files的提示 if (fd.get() == -1) { // We failed to create the ashmem region. Print a warning, but continue // anyway by creating a true anonymous mmap with an fd of -1. It is @@ -429,6 +431,40 @@ MemMap* MemMap::MapAnonymous(const char* name, page_aligned_byte_count, prot, reuse); } ``` +这里面又用到了`ashmem_create_region()`方法,该方法的作用就是创建一块ashmen匿名共享内存,并返回一个文件描述符,我们看一下[ashmem_create_region](https://android.googlesource.com/platform/system/core/+/4f6e8d7a00cbeda1e70cc15be9c4af1018bdad53/libcutils/ashmem-dev.c)的源码: +``` +/* + * ashmem_create_region - creates a new ashmem region and returns the file + * descriptor, or <0 on error + * + * `name' is an optional label to give the region (visible in /proc/pid/maps) + * `size' is the size of the region, in page-aligned bytes + */ +int ashmem_create_region(const char *name, size_t size) +{ + int fd, ret; + // 打开一个fd + fd = open(ASHMEM_DEVICE, O_RDWR); + if (fd < 0) + return fd; + if (name) { + char buf[ASHMEM_NAME_LEN]; + strlcpy(buf, name, sizeof(buf)); + ret = ioctl(fd, ASHMEM_SET_NAME, buf); + if (ret < 0) + goto error; + } + ret = ioctl(fd, ASHMEM_SET_SIZE, size); + if (ret < 0) + goto error; + return fd; +error: + close(fd); + return ret; +} +``` + + 上面的两个步骤中,不论第一个步骤执行成功与否,都会执行第二步,但是执行的行为不同 - 如果第一步执行成功,就会通过Andorid的匿名共享内存(Anonymous Shared Memory)分配4KB(一个page)内核态内存,然后再通过Linux的mmap调用映射到用户态虚拟内存地址空间。 From f28a590de7240a8056fb42767ddd16170cc9c407 Mon Sep 17 00:00:00 2001 From: Charon Date: Fri, 21 Jun 2019 18:23:28 +0800 Subject: [PATCH 016/247] =?UTF-8?q?Update=20OOM=E9=97=AE=E9=A2=98=E5=88=86?= =?UTF-8?q?=E6=9E=90.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...56\351\242\230\345\210\206\346\236\220.md" | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" index 0e7144b5..fef7ec94 100644 --- "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" +++ "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" @@ -536,6 +536,7 @@ int pthread_create(pthread_t* thread_out, pthread_attr_t const* attr, if (thread->mmap_size != 0) { munmap(thread->attr.stack_base, thread->mmap_size); } + // clone失败就会报出clone failed的错误 async_safe_format_log(ANDROID_LOG_WARN, "libc", "pthread_create failed: clone failed: %s", strerror(clone_errno)); return clone_errno; @@ -659,7 +660,8 @@ static void* __create_thread_mapped_space(size_t mmap_size, size_t stack_guard_s 最后总结一下: 不管是代号JNIEnv还是1040的OOM都是因为进程内虚拟内存地址空间耗尽导致的。 -在一个32位系统中,如果是4G的内存空间,系统内核将使用最上层的1G虚拟空间,用户空间的内存就只剩下3G或者更少,而创建一个进程需要1040k的虚拟内存,所以假设创建一个线程什么都不干,那最多也只能最大能创建3000个线程 +在一个32位系统中,如果是4G的内存空间,系统内核将使用最上层的1G虚拟空间,用户空间的内存就只剩下3G或者更少,而创建一个进程需要1040k的虚拟内存,所以假设创建一个线程什么都不干,那最多也只能最大能创建3000个线程。当逻辑地址空间不足(已用逻辑空间地址可以查看 /proc/pid/status中的VmPeak/VmSize查看),就会报出创建线程的OOM问题,`W/libc: pthread_create failed: couldn't allocate 1069056-bytes mapped space: Out of memory +W/art: Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Try again"` @@ -710,32 +712,32 @@ PD1806:/ $ cat proc/15834/status Name: com.example.oom Umask: 0077 State: S (sleeping) -Tgid: 15834 +Tgid: 15834 // 进程组的ID Ngid: 0 -Pid: 15834 -PPid: 1001 -TracerPid: 0 +Pid: 15834 // 进程ID +PPid: 1001 // 当前进程的父进程 +TracerPid: 0 // 跟踪当前进程的进程ID,如果是0表示没有跟踪 Uid: 10458 10458 10458 10458 Gid: 10458 10458 10458 10458 -FDSize: 128 +FDSize: 128 // 当前分配的文件描述符,这个值不是当前进程使用文件描述符的上线 Groups: 9997 20458 50458 -VmPeak: 4403108 kB -VmSize: 4402056 kB +VmPeak: 4403108 kB // 当前进程运行过程中所占用内存的峰值 +VmSize: 4402056 kB // 已用逻辑空间地址,虚拟内存大小。整个进程使用虚拟内存大小,是VmLib, VmExe, VmData, 和 VmStk的总和。 VmLck: 0 kB VmPin: 0 kB -VmHWM: 49108 kB -VmRSS: 48920 kB +VmHWM: 49108 kB // 程序得到分配到物理内存的峰值 +VmRSS: 48920 kB // 程序现在正在使用的物理内存 RssAnon: 9268 kB RssFile: 39540 kB RssShmem: 112 kB -VmData: 1737808 kB -VmStk: 8192 kB -VmExe: 20 kB -VmLib: 163804 kB -VmPTE: 1000 kB +VmData: 1737808 kB // 所占用的虚拟内存 +VmStk: 8192 kB // 任务在用户态的栈的大小 (stack_vm) +VmExe: 20 kB // 程序所拥有的可执行虚拟内存的大小,代码段,不包括任务使用的库 (end_code-start_code) +VmLib: 163804 kB // 被映像到任务的虚拟内存空间的库的大小 (exec_lib) +VmPTE: 1000 kB // 该进程的所有页表的大小,单位:kb VmPMD: 32 kB VmSwap: 15776 kB -Threads: 17 +Threads: 17 // 当前的线程数 SigQ: 0/21568 SigPnd: 0000000000000000 ShdPnd: 0000000000000000 @@ -773,8 +775,13 @@ Thread.UncaughtExceptionHandler捕获到OutOfMemoryError时记录/proc/pid目录 +## 什么情况下虚拟内存地址空间才会耗尽 +说面分析了那么多,结论就是因为虚拟内存空间耗尽导致的,但是究竟什么情况才会出现耗尽的情况? +[Virtual Memory and Linux](https://events.linuxfoundation.org/sites/events/files/slides/elc_2016_mem.pdf) +[Android进程的内存管理分析](https://blog.csdn.net/gemmem/article/details/8920039) + --- - 邮箱 :charon.chui@gmail.com From 243005abb515c77f7a63eafa8fb0e7cb6f275942 Mon Sep 17 00:00:00 2001 From: Charon Date: Mon, 24 Jun 2019 15:37:04 +0800 Subject: [PATCH 017/247] =?UTF-8?q?Update=20OOM=E9=97=AE=E9=A2=98=E5=88=86?= =?UTF-8?q?=E6=9E=90.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" index fef7ec94..ab1e520f 100644 --- "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" +++ "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" @@ -431,7 +431,7 @@ MemMap* MemMap::MapAnonymous(const char* name, page_aligned_byte_count, prot, reuse); } ``` -这里面又用到了`ashmem_create_region()`方法,该方法的作用就是创建一块ashmen匿名共享内存,并返回一个文件描述符,我们看一下[ashmem_create_region](https://android.googlesource.com/platform/system/core/+/4f6e8d7a00cbeda1e70cc15be9c4af1018bdad53/libcutils/ashmem-dev.c)的源码: +这里面又用到了`ashmem_create_region()`方法,该方法的作用就是创建一块匿名共享内存(Anonymous Shared Memory-Ashmem),并返回一个文件描述符,我们看一下[ashmem_create_region](https://android.googlesource.com/platform/system/core/+/4f6e8d7a00cbeda1e70cc15be9c4af1018bdad53/libcutils/ashmem-dev.c)的源码: ``` /* * ashmem_create_region - creates a new ashmem region and returns the file From 2003963841caded55edf99fcc65aa9d004834260 Mon Sep 17 00:00:00 2001 From: Charon Date: Mon, 24 Jun 2019 19:35:17 +0800 Subject: [PATCH 018/247] =?UTF-8?q?Update=20OOM=E9=97=AE=E9=A2=98=E5=88=86?= =?UTF-8?q?=E6=9E=90.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...56\351\242\230\345\210\206\346\236\220.md" | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" index ab1e520f..259c6c46 100644 --- "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" +++ "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" @@ -280,18 +280,7 @@ bool JNIEnvExt::CheckLocalsValid(JNIEnvExt* in) NO_THREAD_SAFETY_ANALYSIS { return in->locals_.IsValid(); } ``` -所以根本原因是因为 JNIEnvExt::table_override_ 仍然为nullptr导致的返回false -而它的赋值在GetFunctionTable方法中 -``` -const JNINativeInterface* JNIEnvExt::GetFunctionTable(bool check_jni) { - const JNINativeInterface* override = JNIEnvExt::table_override_; - if (override != nullptr) { - return override; - } - return check_jni ? GetCheckJniNativeInterface() : GetJniNativeInterface(); -} -``` -GetFunctionTable方法又被构造函数调用 +从代码上看,基本排除是传入的参数nullptr导致的,所以根本原因是locals.IsValid返回了false,而locals是JNIEnvExt的一个成员变量,在JNIEnvExt构造的时候通过成员列表方式初始化 ``` JNIEnvExt::JNIEnvExt(Thread* self_in, JavaVMExt* vm_in, std::string* error_msg) : self_(self_in), @@ -308,7 +297,15 @@ JNIEnvExt::JNIEnvExt(Thread* self_in, JavaVMExt* vm_in, std::string* error_msg) unchecked_functions_ = GetJniNativeInterface(); } ``` -构造函数中又使用了[IndirectReferenceTable](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/indirect_reference_table.cc)类,他的构造函数为 + +而[locals_.isValid](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/indirect_reference_table.cc)的方法的源码为: +``` +bool IndirectReferenceTable::IsValid() const { + return table_mem_map_.get() != nullptr; +} +``` +所以只可能是table_men_map为nullptr导致的。 +而[IndirectReferenceTable](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/indirect_reference_table.cc)类,他的构造函数为 ``` IndirectReferenceTable::IndirectReferenceTable(size_t max_count, IndirectRefKind desired_kind, @@ -339,6 +336,8 @@ IndirectReferenceTable::IndirectReferenceTable(size_t max_count, last_known_previous_state_ = kIRTFirstSegment; } ``` + + 如果上面失败的话,那就只有一种情况就是 MemMap::MapAnonymous 失败了,而MemMap::MapAnonymous的作用是为JNIEnv结构体中的Indirect_Reference_table(C层用于存储JNI局部/全局变量)申请内存,我们继续看[MemMap](https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r41/runtime/mem_map.cc) ``` MemMap* MemMap::MapAnonymous(const char* name, @@ -471,7 +470,7 @@ error: - 如果第一步执行失败,第二步就会通过Linux的mmap调用创建一段虚拟内存。 而上面失败的情况主要有: -- 第一步失败的情况一般是内核分配内存失败,这种情况下,整个设备OS的内存应该都处于非常紧张的状态。 +- 第一步失败的情况一般是内核分配内存失败,这种情况下,整个设备OS的内存应该都处于非常紧张的状态。但是我们从crash的信息里面看用户的内存还是挺充足的,所以排除这种情况。 - 第二步失败的情况一般是进程虚拟内存地址空间耗尽。而且会打印Failed anonymous mmap的错误 所以这里child_jni_env_ext.get() == nullptr 通常是因为第二步失败,也就是进程虚拟内存地址空间耗尽。所以这就是代号JNIEnv OOM的原因。 @@ -779,6 +778,8 @@ Thread.UncaughtExceptionHandler捕获到OutOfMemoryError时记录/proc/pid目录 说面分析了那么多,结论就是因为虚拟内存空间耗尽导致的,但是究竟什么情况才会出现耗尽的情况? +Android系统给每个进程分配了一定的虚拟地址空间大小,进程使用的虚拟空间如果超过阈值,就会触发OOM。所以只可能是线程太多,消耗了大部分虚拟内存地址空间,从而引发了当前进程空间不足。 + [Virtual Memory and Linux](https://events.linuxfoundation.org/sites/events/files/slides/elc_2016_mem.pdf) [Android进程的内存管理分析](https://blog.csdn.net/gemmem/article/details/8920039) From 78e11db34bb84c87db4bff4aff2f4abeddb0c393 Mon Sep 17 00:00:00 2001 From: Charon Date: Mon, 24 Jun 2019 22:16:55 +0800 Subject: [PATCH 019/247] =?UTF-8?q?Update=20OOM=E9=97=AE=E9=A2=98=E5=88=86?= =?UTF-8?q?=E6=9E=90.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...56\351\242\230\345\210\206\346\236\220.md" | 89 ++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" index 259c6c46..a2589ca0 100644 --- "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" +++ "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" @@ -777,11 +777,94 @@ Thread.UncaughtExceptionHandler捕获到OutOfMemoryError时记录/proc/pid目录 ## 什么情况下虚拟内存地址空间才会耗尽 -说面分析了那么多,结论就是因为虚拟内存空间耗尽导致的,但是究竟什么情况才会出现耗尽的情况? +说面分析了那么多,结论就是因为虚拟内存空间耗尽导致的,但是究竟什么情况才会出现耗尽的情况? + +内存是程序运行时的存储地址空间,可分为虚拟地址空间和物理地址空间。虚拟地址空间是相对进程而言的,每个进程都有独立的地址空间(如32位程序都有4GB的虚拟地址空间)。物理地址空间就是由硬件(内存条)提供的存储空间,物理地址空间被所有进程共享。 + +Linux采用虚拟内存管理技术,每个进程都有各自独立的进程地址空间(即4G的线性虚拟空间),无法直接访问物理内存。这样起到保护操作系统,并且让用户程序可使用比实际物理内存更大的地址空间。 + +4G进程地址空间被划分两部分,内核空间和用户空间。用户空间(包括代码、数据、堆、共享库以及栈)从0到3G,内核空间(包括内核中的代码和数据结构)从3G到4G; + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/vm_linux.png?raw=true) + +用户进程通常情况只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等情况可访问到内核空间; +用户空间对应进程,所以当进程切换,用户空间也会跟着变化; +内核空间是由内核负责映射,不会跟着进程变化;内核空间地址有自己对应的页表,用户进程各自有不同额页表。 + +从程序角度看,我们谈到的地址空间一般是虚拟地址空间,通过malloc或new分配的内存都虚拟地址空间的内存。虚拟地址空间与物理地址空间的都是以page为最小管理单元,page的大小因系统而异,一般都是4KB。虚拟地址空间有到物理地址空间的映射,如果要访问的虚拟地址空间没有映射到物理地址空间,操作系统会产生缺页中断,将虚拟地址空间映射到物理地址空间。 + +因此,程度的虚拟地址空间比物理的地址空间要大的多。在较多进程同时运行时,物理地址空间有可能不够,操作系统会将一部物理地址空间的内容交换到磁盘,从而腾挪出一部分物理地址空间来。磁盘上的交换区,在linux上叫swap area,windows时叫page file。 + +android底层基于linux,不过android是没有交换区的(为什么没有?),所以android系统的内存资源就更加宝贵。为更合理、充分利用有限内存资源,android引入一个low-memory-killer机制,在内存不足,根据规则回收一部分低优先级的进程,从而释放他们占有的内存。 + +进程的内存空间只是虚拟内存,而程序运行需要的是物理内存(ram),在必要时,操作系统会将程序运行中申请的虚拟内存映射到ram,让进程能够使用物理内存。进程所操作的空间都是虚拟地址空间,无法直接操作ram。java程序发生OOM并不表示ram不足,如果ram真的不足,android的memory killer就会发挥作用,它会杀死一些优先级比较低的进程来释放物理内存,让高优先级程序得到更多的内存。 + + Android系统给每个进程分配了一定的虚拟地址空间大小,进程使用的虚拟空间如果超过阈值,就会触发OOM。所以只可能是线程太多,消耗了大部分虚拟内存地址空间,从而引发了当前进程空间不足。 -[Virtual Memory and Linux](https://events.linuxfoundation.org/sites/events/files/slides/elc_2016_mem.pdf) -[Android进程的内存管理分析](https://blog.csdn.net/gemmem/article/details/8920039) +`adb shell dumpsys meminfo packagename` 可以查看占用的内存信息 +``` +PD1806:/system/bin $ dumpsys meminfo com.example.oom +Applications Memory Usage (in Kilobytes): +Uptime: 31807223 Realtime: 31807223 + +** MEMINFO in pid 11380 [com.example.oom] ** + Pss Private Private SwapPss Heap Heap Heap + Total Dirty Clean Dirty Size Alloc Free + ------ ------ ------ ------ ------ ------ ------ + Native Heap 64227 64176 0 28 77824 72483 5340 + Dalvik Heap 2158 2124 0 24 3590 2693 897 + Dalvik Other 20804 20804 0 0 + Stack 92 92 0 0 + Ashmem 2 0 0 0 + Gfx dev 892 892 0 0 + Other dev 12 0 12 0 + .so mmap 8595 212 6180 16 + .apk mmap 2388 1964 60 0 + .ttf mmap 105 0 0 0 + .dex mmap 2375 176 552 0 + .oat mmap 176 0 112 0 + .art mmap 6796 6356 120 0 + Other mmap 60 4 4 0 + EGL mtrack 29808 29808 0 0 + GL mtrack 3000 3000 0 0 + Unknown 44350 44332 0 1 + TOTAL 185909 173940 7040 69 81414 75176 6237 + + App Summary + Pss(KB) + ------ + Java Heap: 8600 + Native Heap: 64176 + Code: 9256 + Stack: 92 + Graphics: 33700 + Private Other: 65156 + System: 4929 + + TOTAL: 185909 TOTAL SWAP PSS: 69 + + Objects + Views: 27 ViewRootImpl: 1 + AppContexts: 5 Activities: 1 + Assets: 7 AssetManagers: 0 + Local Binders: 14 Proxy Binders: 31 + Parcel memory: 4 Parcel count: 20 + Death Recipients: 1 OpenSSL Sockets: 0 + WebViews: 0 + + SQL + MEMORY_USED: 0 + PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0 +``` + + +- [Virtual Memory and Linux](https://events.linuxfoundation.org/sites/events/files/slides/elc_2016_mem.pdf) +- [Android进程的内存管理分析](https://blog.csdn.net/gemmem/article/details/8920039) +- [Android系统匿名共享内存(Anonymous Shared Memory)C++调用接口分析](https://blog.csdn.net/luoshengyang/article/details/6939890) +- [Android系统匿名共享内存Ashmem(Anonymous Shared Memory)驱动程序源代码分析](https://blog.csdn.net/luoshengyang/article/details/6664554) +- [Android系统匿名共享内存Ashmem(Anonymous Shared Memory)简要介绍和学习计划](https://blog.csdn.net/luoshengyang/article/details/6651971) +- [虚拟内存那点事](https://sylvanassun.github.io/2017/10/29/2017-10-29-virtual_memory/ ) --- From 46fa7f88af0d6fda77fc0611a58bceab43a3b2bc Mon Sep 17 00:00:00 2001 From: Charon Date: Mon, 24 Jun 2019 22:40:15 +0800 Subject: [PATCH 020/247] =?UTF-8?q?Update=20OOM=E9=97=AE=E9=A2=98=E5=88=86?= =?UTF-8?q?=E6=9E=90.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...56\351\242\230\345\210\206\346\236\220.md" | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" index a2589ca0..793904c7 100644 --- "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" +++ "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" @@ -858,6 +858,61 @@ Uptime: 31807223 Realtime: 31807223 PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0 ``` +查看当前手机的内存信息可以通过`cat /proc/meminfo`来查看 +``` +1|PD1806:/ $ cat /proc/meminfo +MemTotal: 5772000 kB +MemFree: 129500 kB +MemAvailable: 2594764 kB +Buffers: 3968 kB +Cached: 2330100 kB +SwapCached: 12780 kB +Active: 2678740 kB +Inactive: 759120 kB +Active(anon): 804284 kB +Inactive(anon): 303532 kB +Active(file): 1874456 kB +Inactive(file): 455588 kB +Unevictable: 3500 kB +Mlocked: 3500 kB +SwapTotal: 2097148 kB +SwapFree: 488020 kB +Dirty: 60 kB +Writeback: 0 kB +AnonPages: 1102064 kB +Mapped: 743796 kB 映射文件大小 +Shmem: 1416 kB +Slab: 548448 kB +SReclaimable: 241428 kB +SUnreclaim: 307020 kB +KernelStack: 171856 kB +PageTables: 108432 kB +NFS_Unstable: 0 kB +Bounce: 0 kB +WritebackTmp: 0 kB +CommitLimit: 4983148 kB // 请的内存总数超过这个阈值就算overcommit,CommitLimit 就是overcommit的阈值,申请的内存总数超过CommitLimit的话就算是overcommit。 +Committed_AS: 131533804 kB // 表示所有进程已经申请的内存总大小,(注意是已经申请的,不是已经分配的),如果 Committed_AS 超过 CommitLimit 就表示发生了 overcommit,超出越多表示 overcommit 越严重。Committed_AS 的含义换一种说法就是,如果要绝对保证不发生OOM (out of memory) 需要多少物理内存。 +VmallocTotal: 263061440 kB +VmallocUsed: 0 kB +VmallocChunk: 0 kB +CmaTotal: 217088 kB +CmaFree: 1740 kB +NR_KMALLOC: 23312 kB +NR_VMALLOC: 33844 kB +NR_DMA_NOR: 0 kB +NR_DMA_CMA: 58348 kB +NR_ION: 268600 kB +free_ion: 121060 kB +free_ion_pool: 121060 kB +free_ion_heap: 0 kB +NR_GPU: 267812 kB +free_gpu: 154260 kB +zram_size: 609440 kB +zcache_size: 0 kB +pcppages: 6944 kB +ALL_MEM: 5675448 kB +``` + - [Virtual Memory and Linux](https://events.linuxfoundation.org/sites/events/files/slides/elc_2016_mem.pdf) - [Android进程的内存管理分析](https://blog.csdn.net/gemmem/article/details/8920039) From b1332d938e459b2aed0da00ec447c763636cf223 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 26 Nov 2019 21:02:46 +0800 Subject: [PATCH 021/247] add ExoPlayer part --- .../\345\217\215\347\274\226\350\257\221.md" | 88 ++ .../Icon\345\210\266\344\275\234.md" | 16 + .../1. ExoPlayer\347\256\200\344\273\213.md" | 227 ++++ ...er MediaSource\347\256\200\344\273\213.md" | 122 ++ ...271\213prepare\346\226\271\346\263\225.md" | 1088 +++++++++++++++++ ...re\345\272\217\345\210\227\345\233\276.md" | 43 + ...\206\346\236\220\344\271\213PlayerView.md" | 343 ++++++ ...32\344\277\241\345\215\217\350\256\256.md" | 85 ++ ...72\347\241\200\347\237\245\350\257\206.md" | 20 +- 9 files changed, 2015 insertions(+), 17 deletions(-) create mode 100644 "BasicKnowledge/\345\217\215\347\274\226\350\257\221.md" create mode 100644 "Tools&Library/Icon\345\210\266\344\275\234.md" create mode 100644 "VideoDevelopment/ExoPlayer/1. ExoPlayer\347\256\200\344\273\213.md" create mode 100644 "VideoDevelopment/ExoPlayer/2. ExoPlayer MediaSource\347\256\200\344\273\213.md" create mode 100644 "VideoDevelopment/ExoPlayer/3. ExoPlayer\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213prepare\346\226\271\346\263\225.md" create mode 100644 "VideoDevelopment/ExoPlayer/4. ExoPlayer\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213prepare\345\272\217\345\210\227\345\233\276.md" create mode 100644 "VideoDevelopment/ExoPlayer/5. ExoPlayer\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213PlayerView.md" create mode 100644 "VideoDevelopment/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" diff --git "a/BasicKnowledge/\345\217\215\347\274\226\350\257\221.md" "b/BasicKnowledge/\345\217\215\347\274\226\350\257\221.md" new file mode 100644 index 00000000..51a0aeee --- /dev/null +++ "b/BasicKnowledge/\345\217\215\347\274\226\350\257\221.md" @@ -0,0 +1,88 @@ +反编译 +=== + +- [资源文件获取Apktool](https://ibotpeaches.github.io/Apktool/install/) + 按照官网的指示配置完成后,执行apktool命令 + + ``` + apktool d xxx.apk + // 如果提示-bash: /usr/local/bin/apktool: Permission denied + cd /usr/local/bin + sudo chmod +x apktool + sudo chmod +x apktool.jar + ``` + 执行完成后会在apktool.jar的目录下升级一个文件,里面可以看到xml的信息 + ``` + cd /usr/loca/bin + open . + ``` +- [源码文件获取dex2jar](https://github.com/pxb1988/dex2jar) + 将apk文件解压后获取class.dex文件,然后将dex文件拷贝到dex2jar目录中 + ``` + sh d2j-dex2jar.sh classes.dex + d2j-dex2jar.sh: line 36: ./d2j_invoke.sh: Permission denied + // chmod一下 + sudo chmod +x d2j_invoke.sh + sh d2j-dex2jar.sh classes.dex + ``` + 执行完成后会在当前目录中生成一个 classes-dex2jar.jar + +- [jar包源码查看工具jd-gui](https://github.com/java-decompiler/jd-gui) + 里面有osx的版本,安装后直接打开上面用dex2jar编译出来的.jar文件就可以查看源码了 + + +### 反编译后的源码修改 + +修改代码及资源,最好的方式是修改apktool反编译后的资源级smali代码。JD-GUI查看的java代码不适宜修改,因为修改后还需要重新转换成smali,才能重新编译打包会apk。 +至于smali的修改,则要学习smali语言的语法了,smali是一种类似汇编语言的语言,具体语法可自行上网学习。 +在smali文件夹中找到与具体类对应的smali文件,然后进行修改 +可以用到[java2smali](https://plugins.jetbrains.com/plugin/7385-java2smali)将java代码转换成smali代码 + +### 重新打包 + +``` +B0000000134553m:bin xuchuanren$ apktool b xxxfilename +I: Using Apktool 2.4.0 +I: Checking whether sources has changed... +I: Smaling smali folder into classes.dex... +I: Checking whether resources has changed... +I: Building resources... +S: WARNING: Could not write to ( instead... +S: Please be aware this is a volatile directory and frameworks could go missing, please utilize --frame-path if the default storage directory is unavailable +I: Copying libs... (/lib) +I: Building apk file... +I: Copying unknown files/dir... +I: Built apk... + +``` +生成的apk在该文件xxxfilename中的dist目录中 + +### 重新签名 + +重新签名用的是jarsigner命令 +``` +jarsigner -verbose -keystore [your_key_store_path] -signedjar [signed_apk_name] [usigned_apk_name] [your_key_store_alias] -digestalg SHA1 -sigalg MD5withRSA +``` + +- [your_key_store_path]:密钥所在位置的绝对路径 +- [signed_apk_name]:签名后安装包名称 +- [usigned_apk_name]:未签名的安装包名称 +- [your_key_store_alias]:密钥的别名 + +如: + +``` +jarsigner -verbose -keystore /development/key.keystore -signedjar signed_apk.apk xxx.apk charon -digestalg SHA1 -sigalg MD5withRSA +Enter Passphrase for keystore: + adding: META-INF/MANIFEST.MF + adding: META-INF/CHARON.SF + adding: META-INF/CHARON.RSA + +``` +执行完后会在当前目录下生成签名后的apk文件 + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Tools&Library/Icon\345\210\266\344\275\234.md" "b/Tools&Library/Icon\345\210\266\344\275\234.md" new file mode 100644 index 00000000..5ecc3964 --- /dev/null +++ "b/Tools&Library/Icon\345\210\266\344\275\234.md" @@ -0,0 +1,16 @@ +Icon制作 + +=== + +子日:工欲善其事必先利其器 + +[logoko在线制作](https://www.logoko.com.cn) + +[图标工厂,一键生成各种分辨率图片](http://icon.wuruihong.com/) + + +--- + +- 邮箱 :charon.chui@gmail.com +- +Good Luck! diff --git "a/VideoDevelopment/ExoPlayer/1. ExoPlayer\347\256\200\344\273\213.md" "b/VideoDevelopment/ExoPlayer/1. ExoPlayer\347\256\200\344\273\213.md" new file mode 100644 index 00000000..b5b10235 --- /dev/null +++ "b/VideoDevelopment/ExoPlayer/1. ExoPlayer\347\256\200\344\273\213.md" @@ -0,0 +1,227 @@ +ExoPlayer简介 +--- + +[ExoPlayer](https://github.com/google/ExoPlayer)是google开源的应用级媒体播放器项目。 + + +与内置的MediaPlayer相比,ExoPlayer的优点主要有: + +- 支持通过Http(DASH)和SmoothStreaming进行动态自适应流,这两种都不受MediaPlayer的支持。它还支持其他格式的数据资源,比如MP4、M4A、FMP4、MKV、MP3、Ogg、WAV、FLV等 +- 支持高级的HLS个性,比如正确处理#EXT-X-DISCONTINUITY标签 +- 无缝连接,合并和循环播放多媒体的能力 +- 和应用一起更新播放器(ExoPlayer),因为ExoPlayer是一个集成到应用APK的库,可以随着应用的更新把ExoPlayer更新到一个更新的版本。 +- 在不同的Android版本和设备上很少会有不同的表现。 +- 能够自定义和扩展播放器,以适应各种不同的需求 +- 各个组件都可以自定义,还可以接入ffmpeg组件 + +缺点就是,在音频播放时ExoPlayer会比MediaPlayer消耗更多的电量。 + +集成 +--- + +在app的module中的`build.gradle`文件添加如下依赖(这种方式是添加全部的依赖): +`implementation 'com.google.android.exoplayer:exoplayer:2.X.X'` + +如果不需要依赖全部的类库,也可以只选择添加你需要的类库,例如下面的方式是只需要播放DASH内容的app,就是只添加Core、DASH和UI库: +``` +implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X' +implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X' +implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X' +``` + +下面列出了所有的依赖库: + +- `exoplayer-core`:核心功能库(基础库、必要) +- `exoplayer-dash`:支持DASH +- `exoplayer-hls`:支持HLS +- `exoplayer-smoothstreaming`:支持SmoothStreaming +- `exoplayer-ui`:使用ExoPlayer所需的UI部分和资源 + +上面讲到了DASH、HLS、SmoothStreaming,具体的介绍可以参考[流媒体通信协议]() + +除了上面的几个类库外,ExoPlayer还有很多扩展库提供一些额外的功能,有一些可以直接通过JCenter进行依赖,有一些需要手动去编译,具体的可以通过[扩展库目录](https://github.com/google/ExoPlayer/tree/release-v2/extensions/)中的README来查看详细的内容。 +可以通过JCenter进行依赖的类库和扩展可以从[ExoPlayer的Binatry](https://bintray.com/google/exoplayer)上查看。 + +如果依赖完后仍然不行,你需要将build.gradle文件中的android节点打开Java 8的支持: +``` +compileOptions { + targetCompatibility = 1.8 +} +``` + + +ExoPlayer库的核心是ExoPlayer接口,ExoPlayer公开了传统的高级媒体播放器功能,例如缓冲、播放、暂停、seek等。在具体实现方面,该开源库对播放器的媒体类型、存储方式、位置、渲染方式等进行了最少的实现,旨在让开发者自定义各种特性。ExoPlayer的实现不是直接实现加载和呈现媒体,而是将这项工作委托给各种组件。主要有: + +- TrackSelector:轨道提取器,从MediaSource中提取各个轨道的二进制数据,交给Renderer渲染,创建播放器时传入。 +- Renderer:对多媒体中的各个轨道(音轨、视频轨、字母轨等)数据进行渲染,渲染就是"播放",把二进制文件渲染成声音、画面,创建播放器时传入。 +- MediaSource:定义多媒体数据源,这个类的功能就是从Uri中读取多媒体文件的二进制数据。MediaSource在播放开始时通过ExoPlayer.prepare()注入。 +- LoadControl:对MediaSource进行控制,比如什么时候开始缓冲、缓冲多少等。 + +它们之间的关系是: + +渲染器(Render) ---刷数据--->提取器(Extraor) ----读取数据---> 加载控制器(LoadControl) ----控制数据加载方式---> 媒体源(MediaSource) + + + + +该库提供了这些组件的默认实现,既能满足大部分需求,也可通过自定义来实现特殊的需求。例如可以通过自定义LoadControl来更改播放器的缓冲策略,或自定义Renderer来渲染Android本身不支持的编解码器。 + +[支持的格式](https://exoplayer.dev/supported-formats.html) + + +### 创建播放器 + +为了满足不同的需求,ExoPlayer提供了一个工厂类ExoPlayerFactory,通过该工厂类来创建一个ExoPlayer实例,大多数情况下直接使用`ExoPlayerFactory.newSimpleInstance(context)`方法即可: + +``` +public static SimpleExoPlayer newSimpleInstance(Context context) { + return newSimpleInstance(context, new DefaultTrackSelector()); +} +``` +它返回的SimpleExoPlayer是一个实现ExoPlayer接口并添加了一些额外的高级播放器功能。 + +### 把播放器实例附着到一个View上 + +ExoPlayer库提供了一个PlayerView(A high level view for Player media playbacks. It displays video, subtitles and album art during playback, and displays playback controls using a PlayerControlView.)类,他封装了PlayerControlView和渲染视频的一个默认的SurfaceView以及字幕等功能。可以通过xml中surface_type来指定视频播放的Surface类型,除了值spherical_view(这是球形视频播放一个特殊的值)时,允许值是surface_view,texture_view和none。如果视图仅用于音频播放,则应使用none以避免必须创建Surface,因为这样做可能耗费资源。 + +如果视图是用于常规视频播放那么surface_view或texture_view 应该使用。对于视频播放,相比TextureView,SurfaceView有许多好处: + +- 显着降低了许多设备的功耗。 +- 更准确的帧定时,使视频播放更流畅。 +- 播放受DRM保护的内容时支持安全输出。 +因此,相比较于TextureView,SurfaceView应尽可能优先考虑。 TextureView只有在SurfaceView不符合您需求的情况下才能使用。一个示例是在Android N之前需要平滑动画或滚动视频表面,如下所述。对于这种情况,最好 TextureView 只在SDK_INT小于24(Android N)时使用, 否则,使用SurfaceView。 + +SurfaceView在Android N之前,渲染未与视图动画正确同步。在早期版本SurfaceView中,当放入滚动容器或受到动画影响时,这可能会导致不必要的效果 。这些效果包括视图的内容看起来略微落后于它应该显示的位置,并且视图在受到动画时变黑。为了在Android N之前实现流畅的动画或视频滚动,因此必须使用TextureView而不是SurfaceView。 + +xml声明: +``` + +``` + +```kotlin +private lateinit var mPlayer: SimpleExoPlayer +private fun initView(context: Context) { + val view = View.inflate(context, R.layout.video_view, this) + mPlayer = ExoPlayerFactory.newSimpleInstance(context) + // 通过PlayerView.setPlayer方法来将ExoPlayer绑定到View上 + mPlayerView.player = mPlayer +} +``` + +### 开始播放 + +ExoPlayer中将每一种媒体资源都封装成MediaSource,如果想要播放一种媒体资源,首先需要为他创建对应的MediaSource对象,然后把这个对象传递给ExoPlayer.prepared方法。ExoPlayer提供了多种MediaSource的实现类,例如播放[DASH](https://exoplayer.dev/dash.html)的DashMediaSource,[SmoothStreaming](https://exoplayer.dev/smoothstreaming.html)的SsMediaSource,[HLS](https://exoplayer.dev/hls.html)的HlsMediaSource,以及[常规媒体文件](https://exoplayer.dev/progressive.html)的ProgressiveMediaSource。 + + +``` + fun playSingleMp4Video(url: String) { + val uri = Uri.parse(url) + val mediaSource = buildMediaSource(uri) + // setPlayWhenReady()方法可以设置prepared完成后是否自动播放,如果已经准备好了该方法可实现开始和暂停播放的功能 + mPlayer.playWhenReady = true + // setShuffleModeEnabled控制播放列表 setPlaybackParameters控制亮度和音量 + + // 设置监听 + mPlayer.addListener(mPlayerEventListener) + mPlayer.addVideoListener(mPlayerVideoListener) + // 移除监听 + // mPlayer.removeListener(mPlayerEventListener) + mPlayer.prepare(mediaSource, false, false) + } + + private fun buildMediaSource(uri: Uri): MediaSource { + val dataSourceFactory = DefaultDataSourceFactory(context, Util.getUserAgent(context, "you application name")) + return ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(uri) + } + + private var mPlayerEventListener = object : EventListener { + override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { + when (playbackState) { + Player.STATE_IDLE -> { + // 出事状态、播放器停止或播放失败时的状态 + } + Player.STATE_BUFFERING -> { + // 开始加载数据,无法立即从当前位置进行播放 + } + Player.STATE_READY -> { + // ready状态,可以直接从当前位置播放 + } + Player.STATE_ENDED -> { + // 播放完成 + } + } + } + + override fun onPlayerError(error: ExoPlaybackException?) { + // error.getSourceException()可以获取更多信息 + when(error?.type) { + ExoPlaybackException.TYPE_SOURCE -> { + // 加载资源时出错 + } + + ExoPlaybackException.TYPE_RENDERER -> { + // 渲染时出错 + } + + ExoPlaybackException.TYPE_UNEXPECTED -> { + // 意外 + } + + ExoPlaybackException.TYPE_OUT_OF_MEMORY -> { + // OOM + } + + ExoPlaybackException.TYPE_REMOTE -> { + // 远程错误 + } + } + } + } + + private var mPlayerVideoListener = object : VideoListener { + override fun onVideoSizeChanged( + width: Int, + height: Int, + unappliedRotationDegrees: Int, + pixelWidthHeightRatio: Float + ) { + + } + + override fun onRenderedFirstFrame() { + + } + + override fun onSurfaceSizeChanged(width: Int, height: Int) { + + } + } +``` + + +### 资源释放 + +``` +if (mPlayer != null) { + mPlayer.removeListener(mPlayerEventListener) + mPlayer.removeVideoListener(mPlayerVideoListener) + mPlayer.release() +} +``` + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + + + + + diff --git "a/VideoDevelopment/ExoPlayer/2. ExoPlayer MediaSource\347\256\200\344\273\213.md" "b/VideoDevelopment/ExoPlayer/2. ExoPlayer MediaSource\347\256\200\344\273\213.md" new file mode 100644 index 00000000..b32d754f --- /dev/null +++ "b/VideoDevelopment/ExoPlayer/2. ExoPlayer MediaSource\347\256\200\344\273\213.md" @@ -0,0 +1,122 @@ +2. ExoPlayer MediaSource简介 +--- + +在ExoPlayer里每一种媒体资源都是被MediaSource来代表的。 +``` +/** + * Defines and provides media to be played by an {@link com.google.android.exoplayer2.ExoPlayer}. A + * MediaSource has two main responsibilities: + * + *
    + *
  • To provide the player with a {@link Timeline} defining the structure of its media, and to + * provide a new timeline whenever the structure of the media changes. The MediaSource + * provides these timelines by calling {@link SourceInfoRefreshListener#onSourceInfoRefreshed} + * on the {@link SourceInfoRefreshListener}s passed to {@link + * #prepareSource(SourceInfoRefreshListener, TransferListener)}. + *
  • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are + * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a + * way for the player to load and read the media. + *
+ * + * All methods are called on the player's internal playback thread, as described in the {@link + * com.google.android.exoplayer2.ExoPlayer} Javadoc. They should not be called directly from + * application code. Instances can be re-used, but only for one {@link + * com.google.android.exoplayer2.ExoPlayer} instance simultaneously. + */ +public interface MediaSource { + .... +} + +``` + + +ExoPlayer中MediaSource接口有很多实现类: + +- ProgressiveMediaSource: 常规资源 +- SsMediaSource: SmoothStreaming资源 +- HlsMediaSource: HLS资源 +- DashMediaSource: DASH资源 +- LoopingMediaSource:循环播放 +- MergingMediaSource:合并 +- ClippingMediaSource:裁剪 +- ConcatenatingMediaSource:列表 + + + +### ConcatenatingMediaSource + +连接资源的转换是无缝的。这种连接不要求是相同格式的资源(例如可以把包含480P H264视频文件和包含720P VP9的视频文件很好地连接在一起)。它们甚至可以是不同的类型(比如可以将一个视频和一个纯音频流很好地连接在一起)。并且在一个连接里一个类型的MediaSource可以被多次使用。 + +在一个ConcatenatingMediaSource里可以通过添加,删除和移动MediaSource动态地修改播放列表。同样在播放视频之前或者是正在播放的过程中可以通过调用相应的ConcatenatingMediaSource方法动态修改播放列表。播放器会正确地自动处理这些动态修改。例如正在播放的MediaSource被移动了,播放不会中断并且播放完成后会自动播放它后面的一个MediaSource资源。如果正在播放的MediaSource被删除了,播放器会自动移动到第一个存在的后继者去播放,如果没有后继者的话,播放器将会转到结束的状态。 + +``` +val mediaSource = buildMediaSource(uri) +val mediaSource2 = buildMediaSource(uri) + +val concatenatingMediaSource = ConcatenatingMediaSource(mediaSource, mediaSource2) +mPlayer.prepare(concatenatingMediaSource) + +private fun buildMediaSource(uri: Uri): MediaSource { + val dataSourceFactory = DefaultDataSourceFactory(context, Util.getUserAgent(context, "you application name")) + return ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(uri) +} +``` + + +### ClippingMediaSource + +剪辑视频 + +``` +// 从5s到10s +val clippingMediaSource = ClippingMediaSource(mediaSource, 5000, 10000) +mPlayer.prepare(clippingMediaSource) +``` + + +### LoopingMediaSource + +循环播放,如果想要一直循环,可以使用ExoPlayer.setRepeatMode. + +``` +// 循环5次 +val loopingMediaSource = LoopingMediaSource(mediaSource, 5) +mPlayer.prepare(loopingMediaSource) +``` + + + +### MergingMediaSource + +如果一个视频文件和一个单独的字母文件,可以使用MergingMediaSource合并成一个资源播放 + +### 高级组合 + +假如有两个视频A和B,下面的例子展示了怎么一起使用LoopingMediaSource和ConcatenatingMediaSource来播放A、A、B序列。 + +``` +val mediaSource = buildMediaSource(uri) +val mediaSource2 = buildMediaSource(uri) + +val loopingMediaSource = LoopingMediaSource(mediaSource, 2) +val concatenatingMediaSource = ConcatenatingMediaSource(loopingMediaSource, mediaSource2) +mPlayer.prepare(concatenatingMediaSource) +``` + + + +ExoPlayer提供下载媒体以进行离线播放的功能。这里就不细说了。 + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + + + + + diff --git "a/VideoDevelopment/ExoPlayer/3. ExoPlayer\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213prepare\346\226\271\346\263\225.md" "b/VideoDevelopment/ExoPlayer/3. ExoPlayer\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213prepare\346\226\271\346\263\225.md" new file mode 100644 index 00000000..92e1f2f1 --- /dev/null +++ "b/VideoDevelopment/ExoPlayer/3. ExoPlayer\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213prepare\346\226\271\346\263\225.md" @@ -0,0 +1,1088 @@ +3. ExoPlayer源码分析之prepare方法 +--- + +上面两篇文章说了ExoPlayer简单的使用,都是些API,没什么用,查查文档就可以了。我们要去学习ExoPlayer整体是怎么设计的。以下基于2.10.6版本. + +### 找入口 + +分析的时候首先要找到一个切入点,上一篇文章中写了怎么去创建使用,我们再看一遍代码,如下: +``` +View.inflate(context, R.layout.video_view, this) +mPlayer = ExoPlayerFactory.newSimpleInstance(context) +mPlayerView.player = mPlayer + +val uri = Uri.parse(url) +val mediaSource = buildMediaSource(uri) // ProgressiveMediaSource +mPlayer.prepare(mediaSource) +``` + +从上面的代码看出来其实主要就是三步: +1. 通过ExoPlayerFactory创建ExoPlayer实例对象,当然这里创建的是SimpleExoPlayer() +2. 将视频url封装成MediaSource类 +3. 调用ExoPlayer.prepare()方法,并将MediaSource类对象作为参数传入。 + +我们下面分析就只看上面的1和3部分,因为2最终也是通过3方法的参数传入的。 + + +### 类图 +先看一下`ExoPlayer`接口的实现: + +``` +/** + * An extensible media player that plays MediaSources. Instances can be obtained from ExoPlayerFactory. + */ +public interface ExoPlayer extends Player { + void retry(); + void prepare(MediaSource mediaSource); + PlayerMessage createMessage(PlayerMessage.Target target); +} +``` + +它实现了Player接口: +``` +public interface Player { + interface AudioComponent + interface VideoComponent + interface TextComponent + interface MetadataComponent + interface MetadataComponent + interface EventListener + int STATE_IDLE = 1; + int STATE_BUFFERING = 2; + int STATE_READY = 3; + int STATE_ENDED = 4; + AudioComponent getAudioComponent(); + VideoComponent getVideoComponent(); + TextComponent getTextComponent(); + MetadataComponent getMetadataComponent(); + Looper getApplicationLooper(); + void addListener(EventListener listener); + int getPlaybackState(); + boolean isPlaying(); + void setPlayWhenReady(boolean playWhenReady); + void setRepeatMode(); + void setShuffleModeEnabled(boolean shuffleModeEnabled); + void seekTo(long positionMs); + void stop(); + void release(); + long getCurrentPosition(); + long getCurrentPosition(); +} +``` + +Player接口有一个默认的实现类BasePlayer: + +``` +/** Abstract base {@link Player} which implements common implementation independent methods. */ +public abstract class BasePlayer implements Player { + +} +``` + + +### ExoPlayer创建部分 + +获取ExoPlayer对象的实例是通过ExoPlayerFactory来实现的,这里面提供了一些静态的方法: +``` +public static SimpleExoPlayer newSimpleInstance(Context context) { + return newSimpleInstance(context, new DefaultTrackSelector()); +} + +public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) { + return newSimpleInstance(context, new DefaultRenderersFactory(context), trackSelector); +} + +public static SimpleExoPlayer newSimpleInstance( + Context context, RenderersFactory renderersFactory, TrackSelector trackSelector) { + return newSimpleInstance(context, renderersFactory, trackSelector, new DefaultLoadControl()); +} + +public static ExoPlayer newInstance( + Context context, Renderer[] renderers, TrackSelector trackSelector) { + return newInstance(context, renderers, trackSelector, new DefaultLoadControl()); +} +``` + +通过这个工厂类可以获取SimpleExoPlayer或者ExoPlayer接口的实例。我们上面是通过该工厂类的ExoPlayerFactory.newSimpleInstance(context)来获取的SimpleExoPlayer。而newSimpleInstance()方法的实现如下: +``` +public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + BandwidthMeter bandwidthMeter, + AnalyticsCollector.Factory analyticsCollectorFactory, + Looper looper) { + return new SimpleExoPlayer( + context, + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + bandwidthMeter, + analyticsCollectorFactory, + looper); +} +``` +可以看到它最后调用了new SimpleExoPlayer,并且传入了一些参数,这些参数主要有: + +- TrackSelector:轨道提取器,The component of an ExoPlayer responsible for selecting tracks to be consumed by each of the player's Renderers. The DefaultTrackSelector implementation should be suitable for most use cases.从MediaSource中提取各个轨道的二进制数据,交给Renderer渲染,它有个selectTracks()方法,会返回TrackSelection数组,TrackSelection就是对轨道进行解析的,因为一个文件有多个轨道:音频轨、视频轨、文字轨等,所以也需要多个TrackSelection。 +- Renderer:对多媒体中的各个轨道(音轨、视频轨、字母轨等)数据进行渲染,渲染就是"播放",把二进制文件渲染成声音、画面,创建播放器时传入。 +RenderersFactory的作用就是创建Renderers,每个资源可能有音频、视频等多个轨道,每个Render对应一个轨道,Renderer对应的有VideoRender、AudioRender、TextRenderer、MetadataRenderers等等 +- LoadControl:对MediaSource进行控制,比如什么时候开始缓冲、缓冲多少等 主要是记录一些位置。 +- MediaSource:定义多媒体数据源,这个类的功能就是从Uri中读取多媒体文件的二进制数据。MediaSource在播放开始时通过ExoPlayer.prepare()注入,它有一个主要的方法就是createPeriod(),用来创建Period对象,这个对象里面会真正的做资源处理。 +- Looper:为了ExoPlayer中消息的处理 +- BandwidthMeter:监控当前的带宽情况 + +它们之间的关系是: + +渲染器(Render) ---刷数据--->提取器(Extraor) ----读取数据---> 加载控制器(LoadControl) ----控制数据加载方式---> 媒体源(MediaSource) + +扯远了,继续看SimpleExoPlayer的构造函数: +``` +public class SimpleExoPlayer extends BasePlayer + implements ExoPlayer, + Player.AudioComponent, + Player.VideoComponent, + Player.TextComponent, + Player.MetadataComponent { + private final ExoPlayerImpl player; + private final Handler eventHandler; + + protected SimpleExoPlayer( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, // 轨道提取器 + LoadControl loadControl, // 加载控制器 + @Nullable DrmSessionManager drmSessionManager, // drm session管理 刚才传的是null + BandwidthMeter bandwidthMeter, // 带宽监测 + AnalyticsCollector.Factory analyticsCollectorFactory, // 用于分析 + Clock clock, // 传的是SystemClick,可以获取系统时间,为了创建HandlerWrapper + Looper looper) { + this.bandwidthMeter = bandwidthMeter; + componentListener = new ComponentListener(); + videoListeners = new CopyOnWriteArraySet<>(); + audioListeners = new CopyOnWriteArraySet<>(); + textOutputs = new CopyOnWriteArraySet<>(); + metadataOutputs = new CopyOnWriteArraySet<>(); + videoDebugListeners = new CopyOnWriteArraySet<>(); + audioDebugListeners = new CopyOnWriteArraySet<>(); + eventHandler = new Handler(looper); + // 获取Render来进行渲染 + renderers = + renderersFactory.createRenderers( + eventHandler, + componentListener, + componentListener, + componentListener, + componentListener, + drmSessionManager); + + // Set initial values. + audioVolume = 1; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + audioAttributes = AudioAttributes.DEFAULT; + videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; + currentCues = Collections.emptyList(); + + // 创建ExoPlayerImp的实例,并将render trackselector loadControl bandwidthmeter looper等传入 + // Build the player and associated objects. + player = + new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); + analyticsCollector = analyticsCollectorFactory.createAnalyticsCollector(player, clock); + addListener(analyticsCollector); + addListener(componentListener); + videoDebugListeners.add(analyticsCollector); + videoListeners.add(analyticsCollector); + audioDebugListeners.add(analyticsCollector); + audioListeners.add(analyticsCollector); + addMetadataOutput(analyticsCollector); + bandwidthMeter.addEventListener(eventHandler, analyticsCollector); + if (drmSessionManager instanceof DefaultDrmSessionManager) { + ((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector); + } + audioFocusManager = new AudioFocusManager(context, componentListener); + } +} +``` + +该方法里面调用了new ExoPlayerImpl(),继续看一下ExoPlayerImpl的构造函数: +``` +final class ExoPlayerImpl extends BasePlayer implements ExoPlayer { + public ExoPlayerImpl( + Renderer[] renderers, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + Clock clock, + Looper looper) { + Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" + + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]"); + Assertions.checkState(renderers.length > 0); + this.renderers = Assertions.checkNotNull(renderers); + this.trackSelector = Assertions.checkNotNull(trackSelector); + this.playWhenReady = false; + this.repeatMode = Player.REPEAT_MODE_OFF; + this.shuffleModeEnabled = false; + this.listeners = new CopyOnWriteArrayList<>(); + emptyTrackSelectorResult = + new TrackSelectorResult( + new RendererConfiguration[renderers.length], + new TrackSelection[renderers.length], + null); + // 媒体资源的片段,TimeLine和Period都是媒体资源片段状态相关的 + period = new Timeline.Period(); + // 播放器相关参数,default是默认的1倍速 + playbackParameters = PlaybackParameters.DEFAULT; + // seek的参数 + seekParameters = SeekParameters.DEFAULT; + playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE; + // 通过传递进来的looper创建eventHandler,并将message交给ExoPlayerImpl.this.handleEvent方法 + eventHandler = + new Handler(looper) { + @Override + public void handleMessage(Message msg) { + ExoPlayerImpl.this.handleEvent(msg); + } + }; + // 创建一个空的播放对象信息,直到开播的时候再赋值 + playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult); + pendingListenerNotifications = new ArrayDeque<>(); + // 创建ExoPlayerImplInternal对象internalPlayer + internalPlayer = + new ExoPlayerImplInternal( + renderers, + trackSelector, + emptyTrackSelectorResult, + loadControl, + bandwidthMeter, + playWhenReady, + repeatMode, + shuffleModeEnabled, + eventHandler, + clock); + // 创建internalPlayer使用的handler + internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); + } + +} +``` + +里面又创建了ExoPlayerImplInternal,并把那些参数全部传递到这里面了: + +``` +public ExoPlayerImplInternal( + Renderer[] renderers, + TrackSelector trackSelector, + TrackSelectorResult emptyTrackSelectorResult, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + boolean playWhenReady, + @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled, + Handler eventHandler, + Clock clock) { + this.renderers = renderers; + this.trackSelector = trackSelector; + this.emptyTrackSelectorResult = emptyTrackSelectorResult; + this.loadControl = loadControl; + this.bandwidthMeter = bandwidthMeter; + this.playWhenReady = playWhenReady; + this.repeatMode = repeatMode; + this.shuffleModeEnabled = shuffleModeEnabled; + this.eventHandler = eventHandler; + this.clock = clock; + this.queue = new MediaPeriodQueue(); + + backBufferDurationUs = loadControl.getBackBufferDurationUs(); + retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); + + seekParameters = SeekParameters.DEFAULT; + playbackInfo = + PlaybackInfo.createDummy(/* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult); + playbackInfoUpdate = new PlaybackInfoUpdate(); + rendererCapabilities = new RendererCapabilities[renderers.length]; + for (int i = 0; i < renderers.length; i++) { + renderers[i].setIndex(i); + rendererCapabilities[i] = renderers[i].getCapabilities(); + } + mediaClock = new DefaultMediaClock(this, clock); + pendingMessages = new ArrayList<>(); + enabledRenderers = new Renderer[0]; + // Timeline、Window、Period后面的文章会单独讲一下 + window = new Timeline.Window(); + period = new Timeline.Period(); + // 初始化trackSelector + trackSelector.init(/* listener= */ this, bandwidthMeter); + + // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can + // not normally change to this priority" is incorrect. + internalPlaybackThread = new HandlerThread("ExoPlayerImplInternal:Handler", + Process.THREAD_PRIORITY_AUDIO); + // 启动一个线程,准备接受任务 + internalPlaybackThread.start(); + // 这个地方比较关键clock.createHandler的意思是使用特定的looper和特定callback来处理消息 + // 而这里传的callback是this,所以通过该handler发送的消息都会传递到ExoPlayerImplInternal类的handleMessage方法 + // clock是一个可以获取系统时间的类,又通过clock创建了一个ExoPlayerImplInternal使用的handler + handler = clock.createHandler(internalPlaybackThread.getLooper(), this); + } +``` + +看到这里应该明白了其实ExoPlayerImplInternal才是最终实现的核心类。 +到这里我们上面mPlayer = ExoPlayerFactory.newSimpleInstance(context)的部分已经看完了,剩下的就是mPlayer.prepare(concatenatingMediaSource). + +### ExoPlayer.prepare()部分 + +我们找到SimpleExoPlayer.prepare()方法: +``` + @Override + public void prepare(MediaSource mediaSource) { + prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); + } + + @Override + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + verifyApplicationThread(); + if (this.mediaSource != null) { + this.mediaSource.removeEventListener(analyticsCollector); + analyticsCollector.resetForNewMediaSource(); + } + this.mediaSource = mediaSource; + mediaSource.addEventListener(eventHandler, analyticsCollector); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); + // 更新当前是否是准备完成后自动开始播放 + updatePlayWhenReady(getPlayWhenReady(), playerCommand); + // player是ExoPlayerImpl对象 + player.prepare(mediaSource, resetPosition, resetState); + } +``` +所以要继续看ExoPlayerImpl.prepare()方法: +``` + @Override + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + playbackError = null; + this.mediaSource = mediaSource; + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + resetPosition, resetState, /* playbackState= */ Player.STATE_BUFFERING); + // Trigger internal prepare first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this prepare. The internal player can't change the playback info immediately + // because it uses a callback. + hasPendingPrepare = true; + pendingOperationAcks++; + // 调用了ExoPlayerImplInternal的prepare方法 + internalPlayer.prepare(mediaSource, resetPosition, resetState); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false); + } +``` +所以要继续再看ExoPlayerImplInternal的prepare方法: +``` + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + // 调用handler.sendMessage,上面在handler初始化的时候说过,该handler发送的消息会走到该类的handleMessage方法。 + handler + .obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource) + .sendToTarget(); + } +``` +所以接下来要继续看一下ExoPlayerImplInternal.handleMessage()方法,这个方法比较多,我们这里只要看MSG_PREPARE部分: +``` + public boolean handleMessage(Message msg) { + try { + switch (msg.what) { + case MSG_PREPARE: + // 通过msg取出MediaSource对象并作为参数调用prepareInternal()方法 + prepareInternal( + (MediaSource) msg.obj, + /* resetPosition= */ msg.arg1 != 0, + /* resetState= */ msg.arg2 != 0); + break; + case MSG_SET_PLAY_WHEN_READY: + setPlayWhenReadyInternal(msg.arg1 != 0); + break; + case MSG_SET_REPEAT_MODE: + setRepeatModeInternal(msg.arg1); + break; + case MSG_SET_SHUFFLE_ENABLED: + setShuffleModeEnabledInternal(msg.arg1 != 0); + break; + case MSG_DO_SOME_WORK: + doSomeWork(); + break; + .......... + } +``` + +里面调用了prepareInternal()方法: +``` +private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + pendingPrepareCount++; + // 这个方法是做一些资源的释放初始化等操作,移除MSG_DO_SOME_WORK消息,然后重置Render,把之前的MediaSource释放等 + resetInternal( + /* resetRenderers= */ false, /* releaseMediaSource= */ true, resetPosition, resetState); + // onPrepared方法内部只是reset了 + loadControl.onPrepared(); + this.mediaSource = mediaSource; + // 更新当前的状态到buffering + setState(Player.STATE_BUFFERING); + // 开始资源准备,并且添加对应的监听, 具体MediaSource里面怎么去实现的prepareSource,等我们讲完doSomeWork后再说 + mediaSource.prepareSource(/* listener= */ this, bandwidthMeter.getTransferListener()); + // 又发送了MSG_DO_SOME_WORK + handler.sendEmptyMessage(MSG_DO_SOME_WORK); +} +``` +这里又是两部分: +- mediaSource.prepareSource()方法,这里面是关于MediaSource作用的部分 +- 发送MSG_DO_SOME_WORK的消息 + + +#### MediaSource.prepareSource() +在看之前有必要先看一下MediaSource接口: +``` +Defines and provides media to be played by an ExoPlayer. A MediaSource has two main responsibilities: +To provide the player with a Timeline defining the structure of its media, and to provide a new timeline whenever the structure of the media changes. The MediaSource provides these timelines by calling MediaSource.SourceInfoRefreshListener.onSourceInfoRefreshed on the MediaSource.SourceInfoRefreshListeners passed to prepareSource(MediaSource.SourceInfoRefreshListener, TransferListener). +To provide MediaPeriod instances for the periods in its timeline. MediaPeriods are obtained by calling createPeriod(MediaSource.MediaPeriodId, Allocator, long), and provide a way for the player to load and read the media. +``` +大体的意思就是一个MediaSource要有两个重要的职责: +- 将一个定义媒体结构的Timeline对象提供给player,并且在媒体变化的时候去提供一个新的Timeline对象 +- 对Timeline中的媒体片段提供MediaPeriod对象,player通过MediaPeriod中提供的方式来读取媒体数据 + +继续看一下mediaSource.prepareSource()方法,它的实现是在BaseMediaSource类中的: +``` +public final void prepareSource( + SourceInfoRefreshListener listener, + @Nullable TransferListener mediaTransferListener) { + Looper looper = Looper.myLooper(); + Assertions.checkArgument(this.looper == null || this.looper == looper); + sourceInfoListeners.add(listener); + if (this.looper == null) { + this.looper = looper; + // 开始的时候会调用该方法 + prepareSourceInternal(mediaTransferListener); + } else if (timeline != null) { + listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); + } +} +``` +继续看prepareSourceInternal()方法,该方法是子类去实现的,这里我们用普通视频格式的ProgressiveMediaSource类来看: +``` +public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + transferListener = mediaTransferListener; + notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable); +} + +private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { + timelineDurationUs = durationUs; + timelineIsSeekable = isSeekable; + // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223. + refreshSourceInfo( + new SinglePeriodTimeline( + timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, tag), + /* manifest= */ null); +} + +protected final void refreshSourceInfo(Timeline timeline, @Nullable Object manifest) { + this.timeline = timeline; + this.manifest = manifest; + for (SourceInfoRefreshListener listener : sourceInfoListeners) { + listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); + } + } +``` +而这里的listener是谁呢? 是ExoPlayerImplInternal,继续看ExoPlayerImplInternal.onSourceInfoRefreshed()方法: +``` +public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { + handler.obtainMessage(MSG_REFRESH_SOURCE_INFO, + new MediaSourceRefreshInfo(source, timeline, manifest)).sendToTarget(); + } +``` + +接着就是发送MSG_REFRESH_SOURCE_INFO消息,然后我们到handleMessage()中看一下: +``` + public boolean handleMessage(Message msg) { + try { + switch (msg.what) { + case MSG_PREPARE: + prepareInternal( + (MediaSource) msg.obj, + /* resetPosition= */ msg.arg1 != 0, + /* resetState= */ msg.arg2 != 0); + break; + case MSG_REFRESH_SOURCE_INFO: + handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj); + break; + .... + } + } +} +``` +Message发送过来的是MediaSourceRefreshInfo对象,然后将其作为参数传递给handleSourceInfoRefreshed()方法,里面逻辑太多了,大体都是一些更新播放信息和状态的操作。 + + +#### MSG_DO_SOME_WORK消息 +handleMessage方法中在收到MSG_DO_SOME_WORK后调用了doSomeWork()方法,我们看一下他里面的实现: +``` +private void doSomeWork() throws ExoPlaybackException, IOException { + // 获取系统时间 + long operationStartTimeMs = clock.uptimeMillis(); + // 通过MediaPeriodQueue更新媒体文件的片段,里面会去prepare下一个片段 + updatePeriods(); + if (!queue.hasPlayingPeriod()) { + // We're still waiting for the first period to be prepared. + maybeThrowPeriodPrepareError(); + scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS); + return; + } + // 获取当前播放器的媒体片段 + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + + TraceUtil.beginSection("doSomeWork"); + // 更新播放位置 + updatePlaybackPositions(); + long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; + + playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs - backBufferDurationUs, + retainBackBufferFromKeyframe); + + boolean renderersEnded = true; + boolean renderersReadyOrEnded = true; + // renderers会比较多,有音频、视频、文本,Renderer接口有好几个实现类 + for (Renderer renderer : enabledRenderers) { + // TODO: Each renderer should return the maximum delay before which it wishes to be called + // again. The minimum of these values should then be used as the delay before the next + // invocation of this method. + // 开始让Renderer进入准备状态 + renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs); + renderersEnded = renderersEnded && renderer.isEnded(); + // Determine whether the renderer is ready (or ended). We override to assume the renderer is + // ready if it needs the next sample stream. This is necessary to avoid getting stuck if + // tracks in the current period have uneven durations. See: + // https://github.com/google/ExoPlayer/issues/1874 + boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded() + || rendererWaitingForNextStream(renderer); + if (!rendererReadyOrEnded) { + renderer.maybeThrowStreamError(); + } + renderersReadyOrEnded = renderersReadyOrEnded && rendererReadyOrEnded; + } + if (!renderersReadyOrEnded) { + maybeThrowPeriodPrepareError(); + } + + long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; + if (renderersEnded + && (playingPeriodDurationUs == C.TIME_UNSET + || playingPeriodDurationUs <= playbackInfo.positionUs) + && playingPeriodHolder.info.isFinal) { + setState(Player.STATE_ENDED); + stopRenderers(); + } else if (playbackInfo.playbackState == Player.STATE_BUFFERING + && shouldTransitionToReadyState(renderersReadyOrEnded)) { + setState(Player.STATE_READY); + // prepare的时候会走到这里,如果prepared完成需要自动播放并且所有Renderer目前都已经是准备好的状态那就会开始渲染的功能 + if (playWhenReady) { + startRenderers(); + } + } else if (playbackInfo.playbackState == Player.STATE_READY + && !(enabledRenderers.length == 0 ? isTimelineReady() : renderersReadyOrEnded)) { + rebuffering = playWhenReady; + setState(Player.STATE_BUFFERING); + stopRenderers(); + } + + if (playbackInfo.playbackState == Player.STATE_BUFFERING) { + for (Renderer renderer : enabledRenderers) { + renderer.maybeThrowStreamError(); + } + } + + if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY) + || playbackInfo.playbackState == Player.STATE_BUFFERING) { + // 发送下一个MSG_DO_SOME_WORK的消息,这样来达到循环 + scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS); + } else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) { + scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); + } else { + handler.removeMessages(MSG_DO_SOME_WORK); + } + + TraceUtil.endSection(); +} +``` + +可以看出,doSomeWork()方法会不断的去绘制,然后每次绘制完一部分就会继续循环调用该方法去绘制下一个媒体片段。上面可以看到核心功能在updatePeriods()方法,通过循环来不断的去加载下一个媒体片段,来达到播放的功能,所以接下来看一下该方法: +``` + private void updatePeriods() throws ExoPlaybackException, IOException { + if (mediaSource == null) { + // The player has no media source yet. + return; + } + if (pendingPrepareCount > 0) { + // We're waiting to get information about periods. + mediaSource.maybeThrowSourceInfoRefreshError(); + return; + } + + // Update the loading period if required. + // 下面会看该方法的实现,这个方法就是去尝试加载下一个媒体片段 + maybeUpdateLoadingPeriod(); + + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); + if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { + setIsLoading(false); + } else if (!playbackInfo.isLoading) { + maybeContinueLoading(); + } + + if (!queue.hasPlayingPeriod()) { + // We're waiting for the first period to be prepared. + return; + } + + // Advance the playing period if necessary. + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + boolean advancedPlayingPeriod = false; + // 如果prepare完成后需要开始播放的话 + while (playWhenReady + && playingPeriodHolder != readingPeriodHolder + && rendererPositionUs >= playingPeriodHolder.getNext().getStartPositionRendererTime()) { + // All enabled renderers' streams have been read to the end, and the playback position reached + // the end of the playing period, so advance playback to the next period. + if (advancedPlayingPeriod) { + // If we advance more than one period at a time, notify listeners after each update. + maybeNotifyPlaybackInfoChanged(); + } + int discontinuityReason = + playingPeriodHolder.info.isLastInTimelinePeriod + ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION + : Player.DISCONTINUITY_REASON_AD_INSERTION; + MediaPeriodHolder oldPlayingPeriodHolder = playingPeriodHolder; + // advancePlayingPeriod()方法内部会去设置queue中要播放的playingPeroidHolder + // 这里对于MediaPeriodQueue有必要说一下,它里面有三个MediaPeriodHolder对象,分别是 + // playingMediaPeriodHolder、readingMediaPeriodHolder、loadingMediaPeriodHolder + playingPeriodHolder = queue.advancePlayingPeriod(); + // 调用enableRenderers()方法,然后开始渲染 + updatePlayingPeriodRenderers(oldPlayingPeriodHolder); + playbackInfo = + playbackInfo.copyWithNewPosition( + playingPeriodHolder.info.id, + playingPeriodHolder.info.startPositionUs, + playingPeriodHolder.info.contentPositionUs, + getTotalBufferedDurationUs()); + playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); + updatePlaybackPositions(); + advancedPlayingPeriod = true; + } + ........ + } +``` + +maybeUpdateLoadingPeriod()是用来检测是否加载下一个媒体片段的: +``` +private void maybeUpdateLoadingPeriod() throws IOException { + // 如果现在有一个正在loading的片段,会重新去评估 + queue.reevaluateBuffer(rendererPositionUs); + // 检查是否需要加载下一个媒体片段 + if (queue.shouldLoadNextMediaPeriod()) { + // 获取下一个媒体片段信息 + MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo); + if (info == null) { + maybeThrowSourceInfoRefreshError(); + } else { + // 1. 调用了MediaPeriodQueue.enqueueNextMediaPeriod()方法 + MediaPeriod mediaPeriod = + queue.enqueueNextMediaPeriod( + rendererCapabilities, + trackSelector, + loadControl.getAllocator(), + mediaSource, + info); + // 2. 调用下一个媒体片段的prepare方法。MediaPeriod接口有很多实现类,例如DASH、HLS等 + // 所以每个不同类型的资源,它的prepare方法都是不一样的 + mediaPeriod.prepare(this, info.startPositionUs); + setIsLoading(true); + // 更新状态 + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + } + } + } +``` + +上面又分了两部分: +- 获取MediaPeriod +- 调用MediaPeriod.prepare方法 + + +#### 先看获取MediaPeriod的部分 + +MediaPeriodQueue.enqueueNextMediaPeriod方法的实现如下: +``` + public MediaPeriod enqueueNextMediaPeriod( + RendererCapabilities[] rendererCapabilities, + TrackSelector trackSelector, + Allocator allocator, + MediaSource mediaSource, + MediaPeriodInfo info) { + long rendererPositionOffsetUs = + loading == null + ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET + ? info.contentPositionUs + : 0) + : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs); + + // 创建MediaPeriodHolder + MediaPeriodHolder newPeriodHolder = + new MediaPeriodHolder( + rendererCapabilities, + rendererPositionOffsetUs, + trackSelector, + allocator, + mediaSource, + info); + if (loading != null) { + Assertions.checkState(hasPlayingPeriod()); + loading.setNext(newPeriodHolder); + } + oldFrontPeriodUid = null; + loading = newPeriodHolder; + length++; + return newPeriodHolder.mediaPeriod; + } +``` +而MediaPeriodHolder的构造函数是: +``` + public MediaPeriodHolder( + RendererCapabilities[] rendererCapabilities, + long rendererPositionOffsetUs, + TrackSelector trackSelector, + Allocator allocator, + MediaSource mediaSource, + MediaPeriodInfo info) { + this.rendererCapabilities = rendererCapabilities; + this.rendererPositionOffsetUs = rendererPositionOffsetUs; + this.trackSelector = trackSelector; + this.mediaSource = mediaSource; + this.uid = info.id.periodUid; + this.info = info; + sampleStreams = new SampleStream[rendererCapabilities.length]; + mayRetainStreamFlags = new boolean[rendererCapabilities.length]; + mediaPeriod = + createMediaPeriod( + info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs); + } +``` +接着是createMediaPeriod(): +``` + /** Returns a media period corresponding to the given {@code id}. */ + private static MediaPeriod createMediaPeriod( + MediaPeriodId id, + MediaSource mediaSource, + Allocator allocator, + long startPositionUs, + long endPositionUs) { + MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs); + if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { + mediaPeriod = + new ClippingMediaPeriod( + mediaPeriod, /* enableInitialDiscontinuity= */ true, /* startUs= */ 0, endPositionUs); + } + return mediaPeriod; + } +``` +然后就是MediaSource.createPeriod()方法,我们用ProgressiveMediaPeriod来看: +``` + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + DataSource dataSource = dataSourceFactory.createDataSource(); + if (transferListener != null) { + dataSource.addTransferListener(transferListener); + } + return new ProgressiveMediaPeriod( + uri, + dataSource, + extractorsFactory.createExtractors(), + loadableLoadErrorHandlingPolicy, + createEventDispatcher(id), + this, + allocator, + customCacheKey, + continueLoadingCheckIntervalBytes); + } +``` + +#### 再看MediaPeriod.prepare部分 +这里还是用普通视频的ProgressiveMediaPeriod来看: +``` + public void prepare(Callback callback, long positionUs) { + this.callback = callback; + loadCondition.open(); + // 又调用了startLoading() + startLoading(); + } +``` + +接着看startLoading()方法: +``` + private void startLoading() { + // 注意这个loadable,里面有uri和DataSource + ExtractingLoadable loadable = + new ExtractingLoadable( + uri, dataSource, extractorHolder, /* extractorOutput= */ this, loadCondition); + if (prepared) { + SeekMap seekMap = getPreparedState().seekMap; + Assertions.checkState(isPendingReset()); + if (durationUs != C.TIME_UNSET && pendingResetPositionUs > durationUs) { + loadingFinished = true; + pendingResetPositionUs = C.TIME_UNSET; + return; + } + loadable.setLoadPosition( + seekMap.getSeekPoints(pendingResetPositionUs).first.position, pendingResetPositionUs); + pendingResetPositionUs = C.TIME_UNSET; + } + extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); + // loader是构造函数里面创建的一个加载线程 + long elapsedRealtimeMs = + loader.startLoading( + loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType)); + eventDispatcher.loadStarted( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ loadable.seekTimeUs, + durationUs, + elapsedRealtimeMs); + } +``` +接着看一下Loader类的startLoading()方法: +``` + public long startLoading( + T loadable, Callback callback, int defaultMinRetryCount) { + Looper looper = Looper.myLooper(); + Assertions.checkState(looper != null); + fatalError = null; + long startTimeMs = SystemClock.elapsedRealtime(); + new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0); + return startTimeMs; + } +``` +里面启动了LoadTask,LoadTask是一个Runnable的实现类,我们就直接看它的run方法: + +``` +private final class LoadTask extends Handler implements Runnable { + public void run() { + try { + executorThread = Thread.currentThread(); + if (!canceled) { + TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName()); + try { + // 最终调用的是这个,而loadable是啥?他是上面创建的ExtractingLoadable + loadable.load(); + } finally { + TraceUtil.endSection(); + } + } + if (!released) { + sendEmptyMessage(MSG_END_OF_SOURCE); + } + } catch (IOException e) { + if (!released) { + obtainMessage(MSG_IO_EXCEPTION, e).sendToTarget(); + } + } catch (InterruptedException e) { + // The load was canceled. + Assertions.checkState(canceled); + if (!released) { + sendEmptyMessage(MSG_END_OF_SOURCE); + } + } catch (Exception e) { + // This should never happen, but handle it anyway. + Log.e(TAG, "Unexpected exception loading stream", e); + if (!released) { + obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget(); + } + } catch (OutOfMemoryError e) { + // This can occur if a stream is malformed in a way that causes an extractor to think it + // needs to allocate a large amount of memory. We don't want the process to die in this + // case, but we do want the playback to fail. + Log.e(TAG, "OutOfMemory error loading stream", e); + if (!released) { + obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget(); + } + } catch (Error e) { + // We'd hope that the platform would kill the process if an Error is thrown here, but the + // executor may catch the error (b/20616433). Throw it here, but also pass and throw it from + // the handler thread so that the process dies even if the executor behaves in this way. + Log.e(TAG, "Unexpected error loading stream", e); + if (!released) { + obtainMessage(MSG_FATAL_ERROR, e).sendToTarget(); + } + throw e; + } + } + +} +``` + +要想看ExtractingLoadable.load()方法,需要先看一下ExtractingLoadable类,以及他的构造函数: +``` +/** Loads the media stream and extracts sample data from it. */ +final class ExtractingLoadable implements Loadable, IcyDataSource.Listener { + // DataSource是从流中读取数据的部分 + public ExtractingLoadable( + Uri uri, + DataSource dataSource, + ExtractorHolder extractorHolder, + ExtractorOutput extractorOutput, + ConditionVariable loadCondition) { + this.uri = uri; + this.dataSource = new StatsDataSource(dataSource); + this.extractorHolder = extractorHolder; + this.extractorOutput = extractorOutput; + this.loadCondition = loadCondition; + this.positionHolder = new PositionHolder(); + this.pendingExtractorSeek = true; + this.length = C.LENGTH_UNSET; + dataSpec = buildDataSpec(/* position= */ 0); + } + + public void load() throws IOException, InterruptedException { + int result = Extractor.RESULT_CONTINUE; + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + ExtractorInput input = null; + try { + long position = positionHolder.position; + // DataSpec是表示数据区域的对象,buildDataSpec会通过Uri和position来确定具体的数据区间 + dataSpec = buildDataSpec(position); + // 获取数据的长度 + length = dataSource.open(dataSpec); + if (length != C.LENGTH_UNSET) { + length += position; + } + Uri uri = Assertions.checkNotNull(dataSource.getUri()); + icyHeaders = IcyHeaders.parse(dataSource.getResponseHeaders()); + DataSource extractorDataSource = dataSource; + if (icyHeaders != null && icyHeaders.metadataInterval != C.LENGTH_UNSET) { + extractorDataSource = new IcyDataSource(dataSource, icyHeaders.metadataInterval, this); + icyTrackOutput = icyTrack(); + icyTrackOutput.format(ICY_FORMAT); + } + input = new DefaultExtractorInput(extractorDataSource, position, length); + // Extractor是从视频容器中获取数据的,selectExtractor方法内部会去选择对应的Extractor + // 如果是MP4的话,这里就会返回Mp4Extractor对象 + Extractor extractor = extractorHolder. (input, extractorOutput, uri); + + // MP3 live streams commonly have seekable metadata, despite being unseekable. + if (icyHeaders != null && extractor instanceof Mp3Extractor) { + ((Mp3Extractor) extractor).disableSeeking(); + } + + if (pendingExtractorSeek) { + extractor.seek(position, seekTimeUs); + pendingExtractorSeek = false; + } + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + loadCondition.block(); + // extractor不断从input流中取读取数据,如果是mp4extractor的话 + // 里面会先执行readAtomHeader,然后atomheader解析完毕后去执行processMoovAtom + // 等moovatom执行完后就会通过message回调onPrepared方法 + result = extractor.read(input, positionHolder); + if (input.getPosition() > position + continueLoadingCheckIntervalBytes) { + position = input.getPosition(); + loadCondition.close(); + // 执行完成后会执行到onContinueLoadingRequestedRunnable中 + handler.post(onContinueLoadingRequestedRunnable); + } + } + } finally { + if (result == Extractor.RESULT_SEEK) { + result = Extractor.RESULT_CONTINUE; + } else if (input != null) { + positionHolder.position = input.getPosition(); + } + Util.closeQuietly(dataSource); + } + } + } +} +``` +我们看到数据加载完成后会执行到onContinueLoadingRequestedRunnable中: +而该Runnable的实现: +``` +onContinueLoadingRequestedRunnable = + () -> { + if (!released) { + Assertions.checkNotNull(callback) + .onContinueLoadingRequested(ProgressiveMediaPeriod.this); + } + }; +``` +会调用到onContinueLoadingRequested,该实现类就是ExoPlayerImplInternal类: +``` + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + handler.obtainMessage(MSG_SOURCE_CONTINUE_LOADING_REQUESTED, source).sendToTarget(); + } +``` +又是发消息,我们继续看下handleMessage方法里面的MSG_SOURCE_CONTINUE_LOADING_REQUESTED,收到该message会执行以下方法: +``` +private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) { + if (!queue.isLoading(mediaPeriod)) { + // Stale event. + return; + } + queue.reevaluateBuffer(rendererPositionUs); + // 尝试去加载下一个MediaPeriod + maybeContinueLoading(); +} + + private void maybeContinueLoading() { + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); + long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs(); + if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { + setIsLoading(false); + return; + } + long bufferedDurationUs = + getTotalBufferedDurationUs(/* bufferedPositionInLoadingPeriodUs= */ nextLoadPositionUs); + boolean continueLoading = + loadControl.shouldContinueLoading( + bufferedDurationUs, mediaClock.getPlaybackParameters().speed); + setIsLoading(continueLoading); + if (continueLoading) { + loadingPeriodHolder.continueLoading(rendererPositionUs); + } + } +``` +到这里基本讲完了doSomeWork()部分。 + + +总结一下: +1. SimpleExoPlayer.prepare()里面调用ExoPlayerImpl.prepare()然后再调用ExoPlayerImplInternal.prepare()然后ExoPlayerImplInternal里面通过handler发送MSG_PREPARE,收到message后会执行prepareInternal()方法,这里面先是调用MediaSouorce.prepare去做一些初始化操作,然后再发送MSG_DO_SOME_WORK,收到改消息后执行doSomeWork()(这个方法执行完后会发消息,再循环执行doSomeWork()方法),这里面再调用updatePeriods()方法。 +2. 这个updatePeroid()方法呢灰常重要,里面又是三步: 第一个步骤是去加载MediaPeroid,然后调用MediaPeriod.prepare()方法,这里面又调用ExtractingLoadable去加载数据,里面会用DataSource和Extractor去解析数据(解析mp4文件的moov数据后执行onPrepared回调)。 第二步是在playWhenReady后,会执行queue.advancePlayingPeriod(),这里面会去设置要播放的playingMediaPeriodHolder。第三部是执行updatePlayingPeriodRenderers()方法,这里面会取playingMediaPeriodHolder然后调用enableRenderers()方法,该方法里面循环遍历renderers数组,对每个都调用renderer.start()。 + +下一篇文章会通过序列图的方式来描述一下这部分的调用逻辑。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + + + + + diff --git "a/VideoDevelopment/ExoPlayer/4. ExoPlayer\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213prepare\345\272\217\345\210\227\345\233\276.md" "b/VideoDevelopment/ExoPlayer/4. ExoPlayer\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213prepare\345\272\217\345\210\227\345\233\276.md" new file mode 100644 index 00000000..3c5b9ad3 --- /dev/null +++ "b/VideoDevelopment/ExoPlayer/4. ExoPlayer\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213prepare\345\272\217\345\210\227\345\233\276.md" @@ -0,0 +1,43 @@ +4. ExoPlayer源码分析之prepare序列图 +--- + +上面源码分析完了后,发现一脸懵逼,只是简单的知道了怎么调用的,都是啥,但是并不知道每个类具体是干啥的,以及他们之间的关联关系 + + +```seq +SimpleExoPlayer->ExoPlayerImpl: player.prepare(mediaSource) +ExoPlayerImpl->ExoPlayerImplInternal: internalPlayer.prepare(mediaSource) +ExoPlayerImplInternal->ExoPlayerImplInternal: MSG_PREPARE +ExoPlayerImplInternal->ExoPlayerImplInternal: prepareInternal +ExoPlayerImplInternal->MediaSource: mediaSource.prepareSource() +ExoPlayerImplInternal->ExoPlayerImplInternal: DO_SOME_WORK +ExoPlayerImplInternal->MediaPeriodQueue: doSomeWork() +ExoPlayerImplInternal->Renderer: renderer.render() +MediaPeriodQueue->MediaPeriodHolder: queue.getPlayingPeriod() +MediaPeriodHolder->MediaPeriod: createMediaPeriod() +ExoPlayerImplInternal->MediaPeriod: doSomeWork(updatePeriods()) +MediaPeriod->ExtractingLoadable: prepare() +ExtractingLoadable->DataSource: load() +ExtractingLoadable->Extracotr(Mp4Extractor): load() +Extracotr(Mp4Extractor)->Extracotr(Mp4Extractor): readAtomHeader() processAtomEnded()processMoovAtom +Extracotr(Mp4Extractor)->ProgressiveMediaPeriod: endTracks() +ProgressiveMediaPeriod->ExoPlayerImplInternal: onPrepared(MediaPeriod source) +ExoPlayerImplInternal->ExoPlayerImplInternal: handlePeriodPrepared +ExoPlayerImplInternal->Renderer: enableRenderer +Renderer->Renderer: if (isPlaying) {renderer.start()} +``` + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + + + + + diff --git "a/VideoDevelopment/ExoPlayer/5. ExoPlayer\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213PlayerView.md" "b/VideoDevelopment/ExoPlayer/5. ExoPlayer\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213PlayerView.md" new file mode 100644 index 00000000..003ac8ed --- /dev/null +++ "b/VideoDevelopment/ExoPlayer/5. ExoPlayer\346\272\220\347\240\201\345\210\206\346\236\220\344\271\213PlayerView.md" @@ -0,0 +1,343 @@ +5. ExoPlayer源码分析之PlayerView +--- + +前面两篇文章大体看了一下ExoPlayer.prepare()部分,接下来,我们要看一下UI部分的封装,PlayerView: + +xml声明: +``` + +``` + +```kotlin +private lateinit var mPlayer: SimpleExoPlayer +private fun initView(context: Context) { + val view = View.inflate(context, R.layout.video_view, this) + mPlayer = ExoPlayerFactory.newSimpleInstance(context) + // 通过PlayerView.setPlayer方法来将ExoPlayer绑定到View上 + mPlayerView.player = mPlayer +} +``` + +``` +public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider { + // 构造函数 + public PlayerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + LayoutInflater.from(context).inflate(playerLayoutId, this); + componentListener = new ComponentListener(); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + + // Content frame. + contentFrame = findViewById(R.id.exo_content_frame); + if (contentFrame != null) { + setResizeModeRaw(contentFrame, resizeMode); + } + + // Shutter view. + shutterView = findViewById(R.id.exo_shutter); + if (shutterView != null && shutterColorSet) { + shutterView.setBackgroundColor(shutterColor); + } + + // 看对应的SurfaceView的类型,创建SurfaceView或TextureView + if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { + ViewGroup.LayoutParams params = + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + switch (surfaceType) { + case SURFACE_TYPE_TEXTURE_VIEW: + surfaceView = new TextureView(context); + break; + case SURFACE_TYPE_MONO360_VIEW: + SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context); + sphericalSurfaceView.setSingleTapListener(componentListener); + surfaceView = sphericalSurfaceView; + break; + default: + surfaceView = new SurfaceView(context); + break; + } + surfaceView.setLayoutParams(params); + contentFrame.addView(surfaceView, 0); + } else { + surfaceView = null; + } + + // Ad overlay frame layout. + adOverlayFrameLayout = findViewById(R.id.exo_ad_overlay); + + // Overlay frame layout. + overlayFrameLayout = findViewById(R.id.exo_overlay); + + // Artwork view. + artworkView = findViewById(R.id.exo_artwork); + this.useArtwork = useArtwork && artworkView != null; + if (defaultArtworkId != 0) { + defaultArtwork = ContextCompat.getDrawable(getContext(), defaultArtworkId); + } + + // Subtitle view. + subtitleView = findViewById(R.id.exo_subtitles); + if (subtitleView != null) { + subtitleView.setUserDefaultStyle(); + subtitleView.setUserDefaultTextSize(); + } + + // Buffering view. + bufferingView = findViewById(R.id.exo_buffering); + if (bufferingView != null) { + bufferingView.setVisibility(View.GONE); + } + this.showBuffering = showBuffering; + + // Error message view. + errorMessageView = findViewById(R.id.exo_error_message); + if (errorMessageView != null) { + errorMessageView.setVisibility(View.GONE); + } + + // Playback control view. + PlayerControlView customController = findViewById(R.id.exo_controller); + View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); + if (customController != null) { + this.controller = customController; + } else if (controllerPlaceholder != null) { + // Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are + // transferred, but standard attributes (e.g. background) are not. + this.controller = new PlayerControlView(context, null, 0, attrs); + controller.setId(R.id.exo_controller); + controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); + ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); + int controllerIndex = parent.indexOfChild(controllerPlaceholder); + parent.removeView(controllerPlaceholder); + parent.addView(controller, controllerIndex); + } else { + this.controller = null; + } + this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; + this.controllerHideOnTouch = controllerHideOnTouch; + this.controllerAutoShow = controllerAutoShow; + this.controllerHideDuringAds = controllerHideDuringAds; + this.useController = useController && controller != null; + hideController(); + } + + + public void setPlayer(@Nullable Player player) { + if (this.player == player) { + return; + } + if (this.player != null) { + this.player.removeListener(componentListener); + Player.TextComponent oldTextComponent = this.player.getTextComponent(); + if (oldTextComponent != null) { + oldTextComponent.removeTextOutput(componentListener); + } + } + this.player = player; + // 把Player设置给PlayerControlView + if (useController) { + controller.setPlayer(player); + } + if (subtitleView != null) { + subtitleView.setCues(null); + } + updateBuffering(); + updateErrorMessage(); + updateForCurrentTrackSelections(/* isNewPlayer= */ true); + if (player != null) { + // 注意下这里 + Player.VideoComponent newVideoComponent = player.getVideoComponent(); + if (newVideoComponent != null) { + if (surfaceView instanceof TextureView) { + newVideoComponent.setVideoTextureView((TextureView) surfaceView); + } else if (surfaceView instanceof SphericalSurfaceView) { + ((SphericalSurfaceView) surfaceView).setVideoComponent(newVideoComponent); + } else if (surfaceView instanceof SurfaceView) { + newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView); + } + newVideoComponent.addVideoListener(componentListener); + } + Player.TextComponent newTextComponent = player.getTextComponent(); + if (newTextComponent != null) { + newTextComponent.addTextOutput(componentListener); + } + player.addListener(componentListener); + maybeShowController(false); + } else { + hideController(); + } + } +} +``` + +构造函数里面只是创建了SurfaceView,然后添加了一些其他的View,例如PlayerControlView。 + +看到这里有个问题,PlayerView里面会去创建SurfaceView,那它是怎么传递到ExoPlayerl里面的? + +``` + // 注意下这里 + Player.VideoComponent newVideoComponent = player.getVideoComponent(); + if (newVideoComponent != null) { + if (surfaceView instanceof TextureView) { + newVideoComponent.setVideoTextureView((TextureView) surfaceView); + } else if (surfaceView instanceof SphericalSurfaceView) { + ((SphericalSurfaceView) surfaceView).setVideoComponent(newVideoComponent); + } else if (surfaceView instanceof SurfaceView) { + newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView); + } + newVideoComponent.addVideoListener(componentListener); + } +``` + +newVideoComponent.setVideoTextureView里面会调用SimpleExoPlayer中的setVideoSurfaceView: +``` + @Override + public void setVideoSurfaceView(SurfaceView surfaceView) { + setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); + } + + @Override + public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) { + verifyApplicationThread(); + removeSurfaceCallbacks(); + this.surfaceHolder = surfaceHolder; + if (surfaceHolder == null) { + setVideoSurfaceInternal(null, false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + } else { + surfaceHolder.addCallback(componentListener); + Surface surface = surfaceHolder.getSurface(); + if (surface != null && surface.isValid()) { + setVideoSurfaceInternal(surface, /* ownsSurface= */ false); + Rect surfaceSize = surfaceHolder.getSurfaceFrame(); + maybeNotifySurfaceSizeChanged(surfaceSize.width(), surfaceSize.height()); + } else { + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + } + } + } +``` +然后会调用setVideoSurfaceInternal: +``` + private void setVideoSurfaceInternal(@Nullable Surface surface, boolean ownsSurface) { + // Note: We don't turn this method into a no-op if the surface is being replaced with itself + // so as to ensure onRenderedFirstFrame callbacks are still called in this case. + List messages = new ArrayList<>(); + // 遍历所有renderer,如果是视频类型 + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { + messages.add( + // 发送msg给MediaCodecVideoRenderer + player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setPayload(surface).send()); + } + } + // 如果之前有surface,把之前的释放掉 + if (this.surface != null && this.surface != surface) { + // We're replacing a surface. Block to ensure that it's not accessed after the method returns. + try { + for (PlayerMessage message : messages) { + message.blockUntilDelivered(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + // If we created the previous surface, we are responsible for releasing it. + if (this.ownsSurface) { + this.surface.release(); + } + } + this.surface = surface; + this.ownsSurface = ownsSurface; + } +``` + +接下来继续看一下MediaCodecVideoRenderer里面处理Message的部分: +``` + @Override + public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { + if (messageType == C.MSG_SET_SURFACE) { + // 直接调用setSurface()方法 + setSurface((Surface) message); + } else if (messageType == C.MSG_SET_SCALING_MODE) { + scalingMode = (Integer) message; + MediaCodec codec = getCodec(); + if (codec != null) { + codec.setVideoScalingMode(scalingMode); + } + } else if (messageType == C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) { + frameMetadataListener = (VideoFrameMetadataListener) message; + } else { + super.handleMessage(messageType, message); + } + } +``` +接着看一下setSurface()方法: +``` +private void setSurface(Surface surface) throws ExoPlaybackException { + if (surface == null) { + // Use a dummy surface if possible. + if (dummySurface != null) { + surface = dummySurface; + } else { + MediaCodecInfo codecInfo = getCodecInfo(); + if (codecInfo != null && shouldUseDummySurface(codecInfo)) { + dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); + surface = dummySurface; + } + } + } + // We only need to update the codec if the surface has changed. + if (this.surface != surface) { + this.surface = surface; + @State int state = getState(); + MediaCodec codec = getCodec(); + if (codec != null) { + if (Util.SDK_INT >= 23 && surface != null && !codecNeedsSetOutputSurfaceWorkaround) { + setOutputSurfaceV23(codec, surface); + } else { + releaseCodec(); + maybeInitCodec(); + } + } + if (surface != null && surface != dummySurface) { + // If we know the video size, report it again immediately. + maybeRenotifyVideoSizeChanged(); + // We haven't rendered to the new surface yet. + clearRenderedFirstFrame(); + if (state == STATE_STARTED) { + setJoiningDeadlineMs(); + } + } else { + // The surface has been removed. + clearReportedVideoSize(); + clearRenderedFirstFrame(); + } + } else if (surface != null && surface != dummySurface) { + // The surface is set and unchanged. If we know the video size and/or have already rendered to + // the surface, report these again immediately. + maybeRenotifyVideoSizeChanged(); + maybeRenotifyRenderedFirstFrame(); + } + } +``` + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + + + + + diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" new file mode 100644 index 00000000..178e777f --- /dev/null +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" @@ -0,0 +1,85 @@ +流媒体通信协议 +=== + + +流媒体(Streaming Media)是指采用流式传输技术在网络上连续实时播放的媒体格式,如音频、视频或多媒体文件,采用流媒体技术使得数据包得以像流水一样发送, 如果没有流媒体技术, 那么我们就要像以前用迅雷下电影一样, 下载整个影片才能观看。 + + + +RTP +--- + +`Real-time Transport Protocol`(实时传输协议):是一种网络传输协议,运行在`UDP` 协议之上,`RTP`协议详细说明了在互联网上传递音频和视频的标准数据包格式。`RTP`协议常用于流媒体系统(配合`RTSP`协议)。 + +RTCP +--- +`Real-time Transport Control Protocol`或`RTP Control Protocol`或简写`RTCP`,实时传输控制协议,是实时传输协议(`RTP`)的一个姐妹协议。`RTCP`为`RTP`媒体流提供信道外(`out-of-band`)控制。`RTCP`本身并不传输数据,但和`RTP`一起协作将多媒体数据打包和发送。`RTCP` 定期在流多媒体会话参加者之间传输控制数据。`RTCP`的主要功能是为`RTP`所提供的服务质量(`Quality of Service`)提供反馈。 + +RTSP +--- +`Real Time Streaming Protocol`(实时流传输协议实时流传输协议):是用来控制声音或影像的多媒体串流协议,`RTSP`提供了一个可扩展框架,使实时数据,如音频与视频的受控、点播成为可能。该协议定义了一对多应用程序如何有效地通过`IP`网络传送多媒体数据。`RTSP` 在体系结构上位于`RTP`和`RTCP`之上,它使用`TCP`或`UDP`完成数据传输。使用`RTSP`时,客户机和服务器都可以发出请求,即`RTSP`可以是双向的。 +`RTSP`与`RTP`最大的区别在于:`RTSP`是一种双向实时数据传输协议,它允许客户端向服务器端发送请求,如回放、快进、倒退等操作。当然,`RTSP`可基于`RTP`来传送数据,还可以选择`TCP`、`UDP`、组播`UDP`等通道来发送数据,具有很好的扩展性。它时一种类似与`http`协议的网络应用层协议。 + +RTMP +--- +`Real Time Messaging Protocol`(实时消息传送协议):是`Adobe Systems`公司为`Flash`播放器和服务器之间音频、视频和数据传输开发的开放协议。协议基于`TCP`,是一个协议族(默认端口1935),包括`RTMP`基本协议及`RTMPT/RTMPS/RTMPE`等多种变种。`RTMP` 是一种设计用来进行实时数据通信的网络协议,主要用来在`Flash/AIR`平台和支持`RTMP`协议的流媒体/交互服务器之间进行音视频和数据通信。市面上绝大部分PC秀场使用的都是它,他有低延迟(2s左右)、稳定性高、技术完善、高支持度、编码兼容性高等特点。但是RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉。 + +RTMP的整体流程为: + +视频采集器 -> 支持RTMP的视频编码器 -> 网络传输 -> 流媒体服务器 -> 网络 -> 客户端 + + +FLV +--- +`Flash Video`:是Adobe公司推出的另一种视频格式,是一种在网络上传输的流媒体数据存储容器格式。其格式相对简单轻量,不需要很大的媒体头部信息。整个FLV由Header和Body以及其他Tag组成。因此加载速度极快。它是基于HTTP/80传输,可以避免被防火墙拦截的问题,除此之外,它可以通过 HTTP 302 跳转灵活调度/负载均衡,支持使用 HTTPS 加密传输,也能够兼容支持 Android,iOS 的移动端。但是由于它的传输特性,会让流媒体资源缓存在本地客户端,在保密性方面不够好,因为网络流量较大,它也不适合做拉流协议。 + +HDS +--- +`HTTP Dynamic Streaming`:是Adobe公司的传统流媒体解决方案RTMP+FLV的组合。 + + +HLS +--- +`HTTP Live Streaming`:是苹果公司实现的基于`HTTP`的流媒体传输协议,可实现流媒体的直播和点播,主要应用在`IOS`系统,为`IOS`设备提供音视频直播和点播方案。`HLS`点播,基本上就是常见的分段`HTTP`点播,不同在于,它的分段非常小。相对于常见的流媒体直播协议,例如`RTMP`协议、`RTSP`协议、`MMS`协议等,`HLS`直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。`HLS`协议在服务器端将 +直播数据流存储为连续的、很短时长的媒体文件(`MPEG-TS`格式),而客户端则不断的下载并播放这些小文件。因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。由此可见,基本上可以认为,`HLS`是以点播的技术方式来实现直播。由于数据通过`HTTP`协议传输,所以完全不用考虑防火墙或者代理的问题,而且分段文件的时长很短,客户端可以很快的选择和切换码率,以适应不同带宽条件下的播放。不过`HLS`的这种技术特点,决定了它的延迟一般总是会高于普通的流媒体直播协议。它也解决了RTMP协议存在的一些问题,例如RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉,而HLS使用的是HTTP协议传输数据(80端口),不会遇到被防火墙屏蔽的情况。 + +在开始一个流媒体会话时,客户端会下载一个包含元数据的extended M3U(m3u8) playlist文件,用于寻找可用的媒体流。 + +HLS的整体流程为: + +视频源文件 -> 服务器(编码器、流分割器) -> 分发器(索引文件、*.ts) -> 传输网络 -> 客户端 + +HDS +--- +`Http Dynamic Streaming`:是一个由Adobe公司模仿HLS协议提出的另一个基于Http的流媒体传输协议。其模式与HLS类似,也是索引文件和媒体切片文件结合的下载方式,但是又要比HLS协议更复杂。 + +DASH +--- + +DASH(MPEG-DASH)全称为Dynamic Adaptive Streaming over HTTP。是国标标准组MPEG 2014年推出的技术标准,主要目标是形成IP网络承载单一格式的流媒体并提供高效与高质量服务的统一方案,解决多制式传输方案(HTTP Live Streaming, Microsoft Smooth Streaming, HTTP Dynamic Streaming)并存格局下的存储与服务能力浪费、运营高成本与复杂度、系统间互操作弱等问题。 + +DASH是基于HTTP的动态自适应的比特率流技术,使用的传输协议是TCP(有些老的客户端直播会采用UDP协议直播, 例如YY, 齐齐视频等). 和HLS, HDS技术类似, 都是把视频分割成一小段一小段, 通过HTTP协议进行传输,客户端得到之后进行播放;不同的是MPEG-DASH支持MPEG-2 TS、MP4等多种格式, 可以将视频按照多种编码切割, 下载下来的媒体格式既可以是ts文件也可以是mp4文件, 所以当客户端加载视频时, 按照当前的网速和支持的编码加载相应的视频片段进行播放. + +因为是分片的,客户端可以自由选择需要播放的媒体分片,可以实现`Adaptive Bitrate Streaming`技术,不同画质内容无缝切换,提供更好的播放体验。 + +YouTube采用DASH!其网页端及移动端APP都使用了DASH。 + +DASH的整个流程: + +内容生成服务器(编码模块、封装模块) -> 流媒体服务器(MPD、媒体文件、HTTP服务器) <-> DASH客户端(控制引擎、媒体引擎、HTTP接入容器) + + +Smooth Streaming +--- +微软提供的一套解决方案,是IIS的媒体服务扩张,用于支持基于HTTP的自适应串流,它的文件切片为mp4,索引文件为ism/ismc。 + + + +***视频编码标准和音频编码标准是`H.264`和`AAC`,这两种标准分别是当今实际应用中编码效率最高的视频标准和音频标准。*** + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" index 15abf838..4d9e0371 100644 --- "a/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -118,26 +118,12 @@ 它在5.1的基础上又增加了中左和中右两个发音点,以求达到更加完美的境界。由于成本比较高,没有广泛普及。 - -流媒体协议 +YUV --- - -- `RTP`:(`Real-time Transport Protocol`)实时传输协议,是一种网络传输协议,运行在`UDP` 协议之上,`RTP`协议详细说明了在互联网上传递音频和视频的标准数据包格式。`RTP`协议常用于流媒体系统(配合`RTSP`协议)。 - -- `RTCP`:(Real-time Transport Control Protocol)或`RTP Control Protocol`或简写`RTCP`,实时传输控制协议,是实时传输协议(`RTP`)的一个姐妹协议。`RTCP`为`RTP`媒体流提供信道外(`out-of-band`)控制。`RTCP`本身并不传输数据,但和`RTP`一起协作将多媒体数据打包和发送。`RTCP` 定期在流多媒体会话参加者之间传输控制数据。`RTCP`的主要功能是为`RTP`所提供的服务质量(`Quality of Service`)提供反馈。 - -- `RTSP`:(`Real Time Streaming Protocol`)实时流传输协议实时流传输协议,是用来控制声音或影像的多媒体串流协议,`RTSP`提供了一个可扩展框架,使实时数据,如音频与视频的受控、点播成为可能。该协议定义了一对多应用程序如何有效地通过`IP`网络传送多媒体数据。`RTSP` 在体系结构上位于`RTP`和`RTCP`之上,它使用`TCP`或`UDP`完成数据传输。使用`RTSP`时,客户机和服务器都可以发出请求,即`RTSP`可以是双向的。 -`RTSP`与`RTP`最大的区别在于:`RTSP`是一种双向实时数据传输协议,它允许客户端向服务器端发送请求,如回放、快进、倒退等操作。当然,`RTSP`可基于`RTP`来传送数据,还可以选择`TCP`、`UDP`、组播`UDP`等通道来发送数据,具有很好的扩展性。它时一种类似与`http`协议的网络应用层协议。 - -- `RTMP`:(`Real Time Messaging Protocol`)实时消息传送协议,是`Adobe Systems`公司为`Flash`播放器和服务器之间音频、视频和数据传输开发的开放协议。协议基于`TCP`,是一个协议族,包括`RTMP`基本协议及`RTMPT/RTMPS/RTMPE`等多种变种。`RTMP` 是一种设计用来进行实时数据通信的网络协议,主要用来在`Flash/AIR`平台和支持`RTMP`协议的流媒体/交互服务器之间进行音视频和数据通信。 - -- `HLS`:(`HTTP Live Streaming`)是苹果公司实现的基于`HTTP`的流媒体传输协议,可实现流媒体的直播和点播,主要应用在`IOS`系统,为`IOS`设备提供音视频直播和点播方案。`HLS`点播,基本上就是常见的分段`HTTP`点播,不同在于,它的分段非常小。相对于常见的流媒体直播协议,例如`RTMP`协议、`RTSP`协议、`MMS`协议等,`HLS`直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。`HLS`协议在服务器端将 -直播数据流存储为连续的、很短时长的媒体文件(`MPEG-TS`格式),而客户端则不断的下载并播放这些小文件。因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。由此可见,基本上可以认为,`HLS`是以点播的技术方式来实现直播。由于数据通过`HTTP`协议传输,所以完全不用考虑防火墙或者代理的问题,而且分段文件的时长很短,客户端可以很快的选择和切换码率,以适应不同带宽条件下的播放。不过`HLS`的这种技术特点,决定了它的延迟一般总是会高于普通的流媒体直播协议。 - -- `HTTP`:(`HyperText Transfer Protocol`)超文本传输协议,运行在`TCP`之上,这个协议是大家非常熟悉的,它也可以用到视频业务中来。 +YUV(也成YCbCr)是电视系统所采用的一种颜色编码方法。其中Y表示明亮度也就是灰阶值,它是基础信号。U和V表示的则是色度,UV的作用是描述影像色彩及饱和度,它们用于指定像素的颜色。U和V不是基础信号,他俩都是被正交调制的。 -***视频编码标准和音频编码标准是`H.264`和`AAC`,这两种标准分别是当今实际应用中编码效率最高的视频标准和音频标准。*** +YUV和RGB视频信号相比,最大的优点在于只需要占用极少的带宽,YUV只需要占用RGB一般的带宽。 相关知识: From b5968e2b7331fb715e2ed2edadef6aaa52610ecf Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 27 Nov 2019 11:59:08 +0800 Subject: [PATCH 022/247] update --- ...\347\256\200\344\273\213(\344\270\200).md" | 2 + ...\351\233\206\346\210\220(\344\272\214).md" | 5 + .../3.Lifecycle(\344\270\211).md" | 2 + .../4.LiveData(\345\233\233).md" | 12 +- .../5.ViewModel(\344\272\224).md" | 4 + .../6.Room(\345\205\255).md" | 3 + KotlinCourse/.idea/KotlinCourse.iml | 8 - KotlinCourse/.idea/misc.xml | 6 - KotlinCourse/.idea/modules.xml | 8 - KotlinCourse/.idea/workspace.xml | 389 ------------------ README.md | 19 +- .../Icon\345\210\266\344\275\234.md" | 2 +- .../1. ExoPlayer\347\256\200\344\273\213.md" | 3 + ...er MediaSource\347\256\200\344\273\213.md" | 4 + ...271\213prepare\346\226\271\346\263\225.md" | 4 + ...re\345\272\217\345\210\227\345\233\276.md" | 3 +- ...\206\346\236\220\344\271\213PlayerView.md" | 1 + 17 files changed, 51 insertions(+), 424 deletions(-) delete mode 100644 KotlinCourse/.idea/KotlinCourse.iml delete mode 100644 KotlinCourse/.idea/misc.xml delete mode 100644 KotlinCourse/.idea/modules.xml delete mode 100644 KotlinCourse/.idea/workspace.xml diff --git "a/ArchitectureComponents/1.\347\256\200\344\273\213(\344\270\200).md" "b/ArchitectureComponents/1.\347\256\200\344\273\213(\344\270\200).md" index c64c9ede..2d3b4742 100644 --- "a/ArchitectureComponents/1.\347\256\200\344\273\213(\344\270\200).md" +++ "b/ArchitectureComponents/1.\347\256\200\344\273\213(\344\270\200).md" @@ -125,6 +125,8 @@ override fun onDestroy() { 包含本地的数据库等,网络`api`等,这些基本上和现有的一些`MVVM`,以及`Clean`架构的组合比较相似 +[下一篇: 2.集成(二)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/2.%E9%9B%86%E6%88%90(%E4%BA%8C).md) + --- - 邮箱 :charon.chui@gmail.com diff --git "a/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" "b/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" index 44c7cb32..83e237a4 100644 --- "a/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" +++ "b/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" @@ -153,6 +153,11 @@ dependencies { } ``` + +[上一篇: 1.简介(一)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/1.%E7%AE%80%E4%BB%8B(%E4%B8%80).md) +[下一篇: 3.Lifecycle(三)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/3.Lifecycle(%E4%B8%89).md) + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/ArchitectureComponents/3.Lifecycle(\344\270\211).md" "b/ArchitectureComponents/3.Lifecycle(\344\270\211).md" index 205df86a..b095eacd 100644 --- "a/ArchitectureComponents/3.Lifecycle(\344\270\211).md" +++ "b/ArchitectureComponents/3.Lifecycle(\344\270\211).md" @@ -165,6 +165,8 @@ public class MyActivity extends Activity implements LifecycleOwner { - 不要在`ViewModel`中持有任何`View/Activity`的`context`。否则会造成内存泄露。 +[上一篇: 2.集成(二)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/2.%E9%9B%86%E6%88%90(%E4%BA%8C).md) +[下一篇: 4.LiveData(四)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/4.LiveData(%E5%9B%9B).md) --- diff --git "a/ArchitectureComponents/4.LiveData(\345\233\233).md" "b/ArchitectureComponents/4.LiveData(\345\233\233).md" index a1f97f42..33bf70ec 100644 --- "a/ArchitectureComponents/4.LiveData(\345\233\233).md" +++ "b/ArchitectureComponents/4.LiveData(\345\233\233).md" @@ -303,16 +303,8 @@ class MyViewModel extends ViewModel { - - - - - - - - - - +[上一篇: 3.Lifecycle(三)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/3.Lifecycle(%E4%B8%89).md) +[下一篇: 5.ViewModel(五)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/5.ViewModel(%E4%BA%94).md) diff --git "a/ArchitectureComponents/5.ViewModel(\344\272\224).md" "b/ArchitectureComponents/5.ViewModel(\344\272\224).md" index a7bf84f5..98ea2f37 100644 --- "a/ArchitectureComponents/5.ViewModel(\344\272\224).md" +++ "b/ArchitectureComponents/5.ViewModel(\344\272\224).md" @@ -128,6 +128,10 @@ public class DetailFragment extends LifecycleFragment { +[上一篇: 4.LiveData(四)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/4.LiveData(%E5%9B%9B).md) +[下一篇: 6.Room(六)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/6.Room(%E5%85%AD).md) + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/ArchitectureComponents/6.Room(\345\205\255).md" "b/ArchitectureComponents/6.Room(\345\205\255).md" index 3765783a..aeb29823 100644 --- "a/ArchitectureComponents/6.Room(\345\205\255).md" +++ "b/ArchitectureComponents/6.Room(\345\205\255).md" @@ -507,6 +507,9 @@ static final Migration MIGRATION_2_3 = new Migration(2, 3) { ``` +[上一篇: 5.ViewModel(五)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/5.ViewModel(%E4%BA%94).md) +[下一篇: 7.PagingLibrary(七)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/7.PagingLibrary(%E4%B8%83).md) + --- diff --git a/KotlinCourse/.idea/KotlinCourse.iml b/KotlinCourse/.idea/KotlinCourse.iml deleted file mode 100644 index c956989b..00000000 --- a/KotlinCourse/.idea/KotlinCourse.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/KotlinCourse/.idea/misc.xml b/KotlinCourse/.idea/misc.xml deleted file mode 100644 index 28a804d8..00000000 --- a/KotlinCourse/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/KotlinCourse/.idea/modules.xml b/KotlinCourse/.idea/modules.xml deleted file mode 100644 index 5871be5a..00000000 --- a/KotlinCourse/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/KotlinCourse/.idea/workspace.xml b/KotlinCourse/.idea/workspace.xml deleted file mode 100644 index 11086204..00000000 --- a/KotlinCourse/.idea/workspace.xml +++ /dev/null @@ -1,389 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - let - - - - - - - - - - true - DEFINITION_ORDER - - - - - - - - - - - - - - - - - - - - - - - -