WebApp 安全風險與防護課堂開課了!

本文由葡萄城技術團隊於原創並首發

轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。

 

2018 網絡安全事故頻發,從數據泄露、信息竊取,到 DDOS 攻擊、勒索病毒,不僅威脅的總數在增加,威脅態勢也變得更加多樣化,攻擊者在不斷開發新的攻擊途徑的同時,也儘力在攻擊過程中掩蓋其蹤跡,使網絡安全防護變得越發棘手。未來是萬物互聯的時代,唯有把握住網絡信息安全,才能避免被降維打擊。

為了回饋社區,我們特邀葡萄城高級架構師、安全專家Carl作為分享嘉賓,於葡萄城技術公開課上,以 WebApp 安全防護為出發點,帶你了解更多意想不到的安全防護措施與黑客攻擊手段,助你提高網絡安全意識,最終學會如何規避風險隱患,避免遭受網絡安全攻擊。

本次課程共分三節,計劃講的內容如下:

第一節:開闊眼界 – 提升安全意識

提升網絡安全意識對項目團隊中的每一個角色、每一個流程都至關重要。同時,也只有具備了網絡安全意識,才願意為數據安全投入更多的時間和精力。下面,我將為您展示部分2018年發生的網絡安全事故,這些事故造成的損失,也許遠遠超出你的想象。

2018 網絡安全事故回顧

Facebook數據泄露事件:2018年9月,Facebook因安全系統漏洞而遭受黑客攻擊,導致約5000萬用戶信息泄露。

上市公司數據堂,涉嫌侵犯數百億條公民個人信息:大數據行業知名企業數據堂在短短8個月的時間內,日均泄露公民個人信息1.3億餘條,累計傳輸數據壓縮后約為4000GB。

圓通10億快遞信息泄露: 10億條用戶數據遭公開售賣,這些數據包括寄(收)件人姓名、電話、地址等隱私信息。

萬豪酒店5億用戶開房信息泄露:萬豪酒店客房預訂數據庫遭黑客入侵,約5億名客戶的信息可能被泄露。

更多數據泄露事件

  1. 國泰航空數據泄露,940萬乘客受影響
  2. MongoDB 數據庫被入侵, 1100 萬份郵件記錄遭泄露
  3. SHEIN 數據泄露影響 642 萬用戶
  4. GovPayNet憑證系統存在漏洞,1400萬交易記錄被曝光
  5. 小米有品平台泄露個人隱私 約2000萬用戶數據遭泄露
  6. …
  7. 美國亞特蘭大市政府受到勒索軟件攻擊
  8. 美國巴爾的摩市遭遇勒索軟件攻擊,導致911緊急調度服務的計算機輔助調度(CAD)功能掉線
  9. 台積電勒索病毒事件,約造成17.6 億元的營收損失,股票市值下跌78億
  10. 很多個人電腦和中小網站都曾遭受攻擊
  11. 平昌冬奧會開幕式服務器遭到身份不明的黑客入侵
  12. GitHub遭1.35T級流量攻擊
  13. CPU數據緩存機制漏洞
  14. iOS 平台WebView組件漏洞(UIWebView/ WKWebView)跨域訪問漏洞(CNNVD-201801-515)
  15. Oracle WebLogic Server WLS核心組件遠程代碼執行漏洞
  16. 微信支付SDKXXE漏洞
  17. Apache Struts2 S2-057安全漏洞

勒索病毒事件

DDoS 攻擊

年度重大漏洞盤點

第二節:知己知彼 – 黑客如何攻擊系統

一名黑客攻擊網站的典型步驟,主要分為以下5步:

  1. 信息收集和漏洞掃描
  2. 漏洞利用
  3. 上傳木馬
  4. 獲取服務器的控制權
  5. 清理痕迹

總結:

黑客不是手動測試系統漏洞的,而是有很多強大的工具可以自動化完成

黑客不是利用系統中的一個漏洞,而是要利用一系列,不同層次的漏洞

黑客經常批量攻擊一系列網站,選取其中漏洞較多,較好利用的重點突破

第三節:十大安全風險(OWASP Top 10)

不安全的軟件正在破壞着我們的金融、醫療、國防、能源和其他重要的基礎設施。隨着我們的軟件變得愈加龐大、複雜且相互關聯,實現應用程序安全的難度也呈指數級增長。而現代軟件開發過程的飛速發展,使得快速、準確地識別軟件安全風險變得愈發的重要,OWASP 組織也因此誕生。

OWASP,即開放式Web應用程序安全項目(Open Web Application Security Project),作為一個開源的、非盈利的全球性安全組織,它提供了有關計算機和互聯網應用程序的公正、實際、有成本效益的信息,其目的是協助個人、企業和機構來發現並使用可信賴的軟件。

OWASP Top 10是由OWASP組織公布,最具權威性的“10項最嚴重的Web應用程序安全風險預警”,其就安全問題從威脅性和脆弱性兩方面進行可能性分析,並結合技術和商業影響的分析結果,輸出公認的、最嚴重的十類Web應用安全風險排名。OWASP Top 10旨在針對上述風險,提出解決方案,幫助IT公司和開發團隊規範應用程序開發流程和測試流程,提高Web產品的安全性。

OWASP敦促所有公司在其組織內採用OWASP Top 10文檔,並確保其Web應用程序最大限度地降低這些風險,採用OWASP Top 10可能是將企業內的軟件開發文化轉變為生成安全代碼文化最行之有效的一步。

OWASP Top 10包括:

  1. 注入
  2. 失效的身份認證
  3. 敏感信息泄露
  4. XML外部實體(XXE)
  5. 失效的訪問控制
  6. 安全配置錯誤
  7. 跨站腳本(XSS)
  8. 不安全的反序列化
  9. 使用含有已知漏洞的組件

10. 不足的日誌記錄和監控

講師介紹:

Carl(陳慶),葡萄城高級架構師、安全專家、葡萄城技術公開課講師。擁有15年項目開發經驗,專註於產品架構、編程技術等領域,對網絡安全有着獨到見解,曾擔任微軟TechEd講師,樂於研究各種前沿技術並分享。

請點擊該地址報名觀看直播:http://live.vhall.com/137416596

錯過本場直播?沒關係,所有直播內容我們會存放在葡萄城公開課頁面,便於您隨時觀看、學習。後續我們也會將Carl老師講的內容整理成文章,發布在社區,敬請關注。

“賦能開發者”葡萄城除了為所有開發人員提供免費的開發技巧分享、項目實戰經驗外,還提供了眾多高水準、高品質的開發工具和開發者解決方案,可有效幫助開發人員提高效率,縮短項目周期,使開發人員能更專註於業務邏輯,順利完成高質量的項目交付,歡迎您深入了解。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

分享工作中一次優化程序的過程

 

程序應用場景:

年初從總公司交接了一個評分系統,系統大概情況是80w考生,每個考生105條作答數據,作答數據主要是客觀題(單選題,多選題,判斷題),評分時間大概40分鐘左右。

需求:優化代碼,提升評分效率,優化之後評分完成在20分鐘左右。

已有代碼優化邏輯:

1.程序方面:

多線程,通過計算評分的人數,得到需要的線程數量,開啟線程分別進行評分

2.查詢數據:

成績表建立自增長Id,查詢匹配需要評分的數據用where id> =Start and id< End

3.插入數據:

採用DataTable數據導入到Sql Server,提示評分完成時保存數據效率

詳細說一下第二點的邏輯

假設數據庫有100w條需要評分的數據,這時通過計算大概得到需要開啟25個線程去同時進行評分,也就是說第一個線程去評分數據庫Id在  1~40000考生的數據,也就是說去查詢數據時,大概查詢語句select * from A where Id>=1 and Id<40000這樣能夠保證查詢出來得效率是最高的;

但是這裡會有缺陷,如果數據存在刪除過在插入肯定數據就不是連續的,也就是

where Id>=1 and Id<40000不能保證查詢得到40000條數據,數據庫表中存在的Id不一定是從1開始,所以目前這種查詢方式是很理想化的。後續這裏還需要在這快研究和學習下。

本次優化細節

代碼片段1

優化前:

 

優化后:

 

 

總結:提取線程內查詢數據存儲到內存中,從而只會查詢一次。

代碼片段2

優化前:

 

優化后:

 

總結:修改List集合取一條數據的方式,Where修改成Find,如果還需要在這裏提升查詢效率可以修改成for循環,但會導致代碼可讀性會變差。

代碼片段3

優化前

 

優化后

 

 

總結:修改DataTable表Select查詢方式,先存儲到Dictionary中,在通過Key去取對應的數據。

最後通過測試全部完成評分時間大概20分鐘左右,也算成功的完成了這個任務,可能還需要在研究研究代碼,看能否有其他地方可以改善的。

存在的疑惑:

線程運算佔用的電腦Cpu的具體的值?

是否存在最佳線程數量?

電腦Cpu處理能力越強是否也能夠提升程序的評分效率?

總結:

1.線程里反覆查詢而且不變的基礎數據放到線程外查詢存儲到變量中

2.List集合的Where查詢修改成Find查詢,極高的提升查詢效率

3.DataTable的Select改用Dictionary<string,DataRow[]>,同樣極高的提升查詢效率

這次能夠站在前人的肩膀上完成這一次代碼的優化還有學習到了很多,瑾以寫在這片文章分享個人在平時工作中的解決的一些問題,希望這次能夠通過分享此篇文章讓自己更多的去記錄和分享工作中遇到以及解決的問題,提升自己的競爭力。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

監測元素resize

前言

近來有需求要做分頁,聽起來可能有點Low。 所以我要把Low的事情做得有點逼格。
分頁本身沒啥,但是數據量起來了,比如十萬。 要是不做點處理, 那你的頁面估計爽得很,機器也爽得很。 放心,我不會讓你這麼爽。

大量數據展現方案

比較流行的當然是虛擬滾動(無限滾動)。

  1. 始終展示的是有限的固定節點。
  2. 外層創建滾動層。

一句話,就是反覆利用固定節點展現數據。

其中還有兩個點

何時需要加載新的分頁數據

  1. scrollTop , clientHeight, scrollHeight
  2. IntersectionObserver (chrome 55+)

如何知道容器寬高變化
本文就圍繞着這個展開

因為使用的是react框架,使用了 react-window, react-window就是用來展現海量數據的react列表組件。
因為項目需要,還要不通尺寸一行展現不同數量的數據。 肯定有人就說,監聽window.resize。

沒錯,監聽resize一定程度,但是window.resize, 並不能讓我知道容器本身的尺寸,當

然可以通過getComputedStyle獲取。 要是window沒有resize的情況呢。

我就想實時的知道尺寸的變化。

尺寸變化監測方案

監測元素resize這裡有幾種方案的測試和源碼。

Cross-Browser, Event-based, Element Resize Detection

思路:

如果IE,直接註冊onresize(這個點贊啊)

否則: 創建 type為text/html的object

設置position為absolute, 高度100%, 寬度100% (這樣可以獲得父容器的寬高)

設置pointer-events:none,利用點擊穿透(讓object窗體變成幽靈)

object元素的高度變化后,通知訂閱者

resize事件節流

問題:

  1. 創建object
  2. 事件處理函數掛載了元素本身上

javascript-detect-element-resize

創建三個子元素,利用scroll事件來監測變化。

原理:

https://zhuanlan.zhihu.com/p/24887312
The scroll event is fired when the document view or an element has been scrolled.
當文檔視圖或者元素滾動的時候會觸發 scroll 事件。
也就是說元素滾動的時候會觸發這個事件,那麼什麼時候元素會滾動?當元素大於其父級元素,且父級元素允許其滾動的時候,該元素可以進行滾動。
換句話說,元素可以滾動意味着父子元素大小不一致,這是這個方法的核心。
那麼我們需要讓元素大小發生改變時,使得 scrollLeft 或者 scrollTop 發生改變,從而觸發 scroll 事件,進一步得知其大小發生了改變。

visibility: hidden; opacity: 0; position: absolute;讓自己變得虛無

addEventListener(“scroll”, scrollListener, true) 在捕捉階段攔截事件,使用false無效

div.expand-trigger 變大

div.expand-trigger 變小

animationstart來監聽显示,比如style.display = ‘none’然後style.display = ‘block’

問題:

  1. 額外創建四個元素節點以及一個style節點
  2. 事件都掛載了元素本身身上,

ResizeObserver

原生自帶的方案, 兼容性並不高, resize-observer-polyfill 基於resize和MutationOberver的polyfill實現了ResizeObserver。

 const resizeObserver = new ResizeObserver(entries => {
        for (let entry of entries) {
            
            console.log(entry.target.id, `height:${entry.contentRect.height}  width:${entry.contentRect.width}`);
        }
        });
        resizeObserver.observe(document.querySelector('#my_element'));
        resizeObserver.observe(document.querySelector('#my_element2'));

此外

當然,我覺得還

  1. 定時器 + getComputedStyle 也是很低成本的實現。
  2. resize + MutationOberver 也是很簡單的方案。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

百萬級日活 App 的屏幕錄製功能是如何實現的

Android 從 4.0 開始就提供了手機錄屏方法,但是需要 root 權限,比較麻煩不容易實現。但是從 5.0 開始,系統提供給了 App 錄製屏幕的一系列方法,不需要 root 權限,只需要用戶授權即可錄屏,相對來說較為簡單。

基本上根據 官方文檔 便可以寫出錄屏的相關代碼。

屏幕錄製的基本實現步驟

在 Manifest 中申明權限
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
獲取 MediaProjectionManager 並申請權限
private val mediaProjectionManager by lazy { activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as? MediaProjectionManager }
private var mediaProjection: MediaProjection? = null
if (mediaProjectionManager == null) {
    Log.d(TAG, "mediaProjectionManager == null,當前手機暫不支持錄屏")
    showToast(R.string.phone_not_support_screen_record)
    return
}
// 申請相關權限
PermissionUtils.permission(PermissionConstants.STORAGE, PermissionConstants.MICROPHONE)
        .callback(object : PermissionUtils.SimpleCallback {
            override fun onGranted() {
                Log.d(TAG, "start record")
                mediaProjectionManager?.apply {
                    // 申請相關權限成功后,要向用戶申請錄屏對話框
                    val intent = this.createScreenCaptureIntent()
                    if (activity.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
                        activity.startActivityForResult(intent, REQUEST_CODE)
                    } else {
                        showToast(R.string.phone_not_support_screen_record)
                    }
                }
            }
            override fun onDenied() {
                showToast(R.string.permission_denied)
            }
        })
        .request()
重寫 onActivityResult() 對用戶授權進行處理
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    if (requestCode == REQUEST_CODE) {
        if (resultCode == Activity.RESULT_OK) {
            mediaProjection = mediaProjectionManager!!.getMediaProjection(resultCode, data)
            // 實測,部分手機上錄製視頻的時候會有彈窗的出現,所以我們需要做一個 150ms 的延遲
            Handler().postDelayed({
                if (initRecorder()) {
                    mediaRecorder?.start()
                } else {
                    showToast(R.string.phone_not_support_screen_record)
                }
            }, 150)
        } else {
            showToast(R.string.phone_not_support_screen_record)
        }
    }
}

private fun initRecorder(): Boolean {
    Log.d(TAG, "initRecorder")
    var result = true
    // 創建文件夾
    val f = File(savePath)
    if (!f.exists()) {
        f.mkdirs()
    }
    // 錄屏保存的文件
    saveFile = File(savePath, "$saveName.tmp")
    saveFile?.apply {
        if (exists()) {
            delete()
        }
    }
    mediaRecorder = MediaRecorder()
    val width = Math.min(displayMetrics.widthPixels, 1080)
    val height = Math.min(displayMetrics.heightPixels, 1920)
    mediaRecorder?.apply {
        // 可以設置是否錄製音頻
        if (recordAudio) {
            setAudioSource(MediaRecorder.AudioSource.MIC)
        }
        setVideoSource(MediaRecorder.VideoSource.SURFACE)
        setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
        setVideoEncoder(MediaRecorder.VideoEncoder.H264)
        if (recordAudio){
            setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
        }
        setOutputFile(saveFile!!.absolutePath)
        setVideoSize(width, height)
        setVideoEncodingBitRate(8388608)
        setVideoFrameRate(VIDEO_FRAME_RATE)
        try {
            prepare()
            virtualDisplay = mediaProjection?.createVirtualDisplay("MainScreen", width, height, displayMetrics.densityDpi,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface, null, null)
            Log.d(TAG, "initRecorder 成功")
        } catch (e: Exception) {
            Log.e(TAG, "IllegalStateException preparing MediaRecorder: ${e.message}")
            e.printStackTrace()
            result = false
        }
    }
    return result
}   

上面可以看到,我們可以設置一系列參數,各種參數的意思就希望大家自己去觀摩官方文檔了。其中有一個比較重要的一點是我們通過 MediaProjectionManager 創建了一個 VirtualDisplay,這個 VirtualDisplay 可以理解為虛擬的呈現器,它可以捕獲屏幕上的內容,並將其捕獲的內容渲染到 Surface 上,MediaRecorder 再進一步把其封裝為 mp4 文件保存。

錄製完畢,調用 stop 方法保存數據

private fun stop() {
    if (isRecording) {
        isRecording = false
        try {
            mediaRecorder?.apply {
                setOnErrorListener(null)
                setOnInfoListener(null)
                setPreviewDisplay(null)
                stop()
                Log.d(TAG, "stop success")
            }
        } catch (e: Exception) {
            Log.e(TAG, "stopRecorder() error!${e.message}")
        } finally {
            mediaRecorder?.reset()
            virtualDisplay?.release()
            mediaProjection?.stop()
            listener?.onEndRecord()
        }
    }
}

/**
 * if you has parameters, the recordAudio will be invalid
 */
fun stopRecord(videoDuration: Long = 0, audioDuration: Long = 0, afdd: AssetFileDescriptor? = null) {
    stop()
    if (audioDuration != 0L && afdd != null) {
        syntheticAudio(videoDuration, audioDuration, afdd)
    } else {
        // saveFile
        if (saveFile != null) {
            val newFile = File(savePath, "$saveName.mp4")
            // 錄製結束后修改後綴為 mp4
            saveFile!!.renameTo(newFile)
            // 刷新到相冊
            val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
            intent.data = Uri.fromFile(newFile)
            activity.sendBroadcast(intent)
            showToast(R.string.save_to_album_success)
        }
        saveFile = null
    }

}

我們必須來看看 MediaRecorder 對 stop() 方法的註釋。

/**
 * Stops recording. Call this after start(). Once recording is stopped,
 * you will have to configure it again as if it has just been constructed.
 * Note that a RuntimeException is intentionally thrown to the
 * application, if no valid audio/video data has been received when stop()
 * is called. This happens if stop() is called immediately after
 * start(). The failure lets the application take action accordingly to
 * clean up the output file (delete the output file, for instance), since
 * the output file is not properly constructed when this happens.
 *
 * @throws IllegalStateException if it is called before start()
 */
public native void stop() throws IllegalStateException; 

根據官方文檔,stop() 如果在 prepare() 后立即調用會崩潰,但對其他情況下發生的錯誤卻沒有做過多提及,實際上,當你真正地使用 MediaRecorder 做屏幕錄製的時候,你會發現即使你沒有在 prepare() 后立即調用 stop(),也可能拋出 IllegalStateException 異常。所以,保險起見,我們最好是直接使用 try...catch... 語句塊進行包裹。

比如你 initRecorder 中某些參數設置有問題,也會出現 stop() 出錯,數據寫不進你的文件。

完畢后,釋放資源
fun clearAll() {
    mediaRecorder?.release()
    mediaRecorder = null
    virtualDisplay?.release()
    virtualDisplay = null
    mediaProjection?.stop()
    mediaProjection = null
}

無法繞過的環境聲音

上面基本對 Android 屏幕錄製做了簡單的代碼編寫,當然實際上,我們需要做的地方還不止上面這些,感興趣的可以移步到 ScreenRecordHelper 進行查看。

但這根本不是我們的重點,我們極其容易遇到這樣的情況,需要我們錄製音頻的時候錄製系統音量,但卻不允許我們把環境音量錄進去。

似乎我們前面初始化 MediaRecorder 的時候有個設置音頻源的地方,我們來看看這個 MediaRecorder.setAudioSource() 方法都支持設置哪些東西。

從官方文檔 可知,我們可以設置以下這些音頻源。由於官方註釋太多,這裏就簡單解釋一些我們支持的可以設置的音頻源。

//設定錄音來源於同方向的相機麥克風相同,若相機無內置相機或無法識別,則使用預設的麥克風
MediaRecorder.AudioSource.CAMCORDER 
//默認音頻源
MediaRecorder.AudioSource.DEFAULT  
//設定錄音來源為主麥克風
MediaRecorder.AudioSource.MIC
//設定錄音來源為語音撥出的語音與對方說話的聲音
MediaRecorder.AudioSource.VOICE_CALL
// 攝像頭旁邊的麥克風
MediaRecorder.AudioSource.VOICE_COMMUNICATION
//下行聲音
MediaRecorder.AudioSource.VOICE_DOWNLINK
//語音識別
MediaRecorder.AudioSource.VOICE_RECOGNITION
//上行聲音
MediaRecorder.AudioSource.VOICE_UPLINK

咋一看沒有我們想要的選項,實際上你逐個進行測試,你也會發現,確實如此。我們想要媒體播放的音樂,總是無法擺脫環境聲音的限制。

奇怪的是,我們使用華為部分手機的系統錄屏的時候,卻可以做到,這就感嘆於 ROM 的定製性更改的神奇,當然,千奇百怪的第三方 ROM 也一直讓我們 Android 適配困難重重。

曲線救國剝離環境聲音

既然我們通過調用系統的 API 始終無法實現我們的需求:錄製屏幕,並同時播放背景音樂,錄製好保存的視頻需要只有背景音樂而沒有環境音量,我們只好另闢蹊徑。

不難想到,我們完全可以在錄製視頻的時候不設置音頻源,這樣得到的視頻就是一個沒有任何聲音的視頻,如果此時我們再把音樂強行剪輯進去,這樣就可以完美解決用戶的需要了。

對於音視頻的混合編輯,想必大多數人都能想到的是大名鼎鼎的 FFmpeg ,但如果要自己去編譯優化得到一個穩定可使用的 FFmpge 庫的話,需要花上不少時間。更重要的是,我們為一個如此簡單的功能大大的增大我們 APK 的體積,那是萬萬不可的。所以我們需要把目光轉移到官方的 MediaExtractor 上。

從 官方文檔 來看,能夠支持到 m4a 和 aac 格式的音頻文件合成到視頻文件中,根據相關文檔我們就不難寫出這樣的代碼。

/**
 * https://stackoverflow.com/questions/31572067/android-how-to-mux-audio-file-and-video-file
 */
private fun syntheticAudio(audioDuration: Long, videoDuration: Long, afdd: AssetFileDescriptor) {
    Log.d(TAG, "start syntheticAudio")
    val newFile = File(savePath, "$saveName.mp4")
    if (newFile.exists()) {
        newFile.delete()
    }
    try {
        newFile.createNewFile()
        val videoExtractor = MediaExtractor()
        videoExtractor.setDataSource(saveFile!!.absolutePath)
        val audioExtractor = MediaExtractor()
        afdd.apply {
            audioExtractor.setDataSource(fileDescriptor, startOffset, length * videoDuration / audioDuration)
        }
        val muxer = MediaMuxer(newFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        videoExtractor.selectTrack(0)
        val videoFormat = videoExtractor.getTrackFormat(0)
        val videoTrack = muxer.addTrack(videoFormat)

        audioExtractor.selectTrack(0)
        val audioFormat = audioExtractor.getTrackFormat(0)
        val audioTrack = muxer.addTrack(audioFormat)

        var sawEOS = false
        var frameCount = 0
        val offset = 100
        val sampleSize = 1000 * 1024
        val videoBuf = ByteBuffer.allocate(sampleSize)
        val audioBuf = ByteBuffer.allocate(sampleSize)
        val videoBufferInfo = MediaCodec.BufferInfo()
        val audioBufferInfo = MediaCodec.BufferInfo()

        videoExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
        audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)

        muxer.start()

        // 每秒多少幀
        // 實測 OPPO R9em 垃圾手機,拿出來的沒有 MediaFormat.KEY_FRAME_RATE
        val frameRate = if (videoFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) {
            videoFormat.getInteger(MediaFormat.KEY_FRAME_RATE)
        } else {
            31
        }
        // 得出平均每一幀間隔多少微妙
        val videoSampleTime = 1000 * 1000 / frameRate
        while (!sawEOS) {
            videoBufferInfo.offset = offset
            videoBufferInfo.size = videoExtractor.readSampleData(videoBuf, offset)
            if (videoBufferInfo.size < 0) {
                sawEOS = true
                videoBufferInfo.size = 0
            } else {
                videoBufferInfo.presentationTimeUs += videoSampleTime
                videoBufferInfo.flags = videoExtractor.sampleFlags
                muxer.writeSampleData(videoTrack, videoBuf, videoBufferInfo)
                videoExtractor.advance()
                frameCount++
            }
        }
        var sawEOS2 = false
        var frameCount2 = 0
        while (!sawEOS2) {
            frameCount2++
            audioBufferInfo.offset = offset
            audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, offset)

            if (audioBufferInfo.size < 0) {
                sawEOS2 = true
                audioBufferInfo.size = 0
            } else {
                audioBufferInfo.presentationTimeUs = audioExtractor.sampleTime
                audioBufferInfo.flags = audioExtractor.sampleFlags
                muxer.writeSampleData(audioTrack, audioBuf, audioBufferInfo)
                audioExtractor.advance()
            }
        }
        muxer.stop()
        muxer.release()
        videoExtractor.release()
        audioExtractor.release()

        // 刪除無聲視頻文件
        saveFile?.delete()
    } catch (e: Exception) {
        Log.e(TAG, "Mixer Error:${e.message}")
        // 視頻添加音頻合成失敗,直接保存視頻
        saveFile?.renameTo(newFile)

    } finally {
        afdd.close()
        Handler().post {
            refreshVideo(newFile)
            saveFile = null
        }
    }
}

於是成就了錄屏幫助類 ScreenRecordHelper

經過各種兼容性測試,目前在 DAU 超過 100 萬的 APP 中穩定運行了兩個版本,於是抽出了一個工具類庫分享給大家,使用非常簡單,代碼註釋比較全面,感興趣的可以直接點擊鏈接進行訪問:https://github.com/nanchen2251/ScreenRecordHelper

使用就非常簡單了,直接把 [README] (https://github.com/nanchen2251/ScreenRecordHelper/blob/master/README.md) 貼過來吧。

Step 1. Add it in your root build.gradle at the end of repositories:
allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}       
Step 2. Add the dependency
dependencies {
    implementation 'com.github.nanchen2251:ScreenRecordHelper:1.0.2'
}
Step 3. Just use it in your project
// start screen record
if (screenRecordHelper == null) {
    screenRecordHelper = ScreenRecordHelper(this, null, PathUtils.getExternalStoragePath() + "/nanchen")
}
screenRecordHelper?.apply {
    if (!isRecording) {
        // if you want to record the audio,you can set the recordAudio as true
        screenRecordHelper?.startRecord()
    }
}

// You must rewrite the onActivityResult
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && data != null) {
        screenRecordHelper?.onActivityResult(requestCode, resultCode, data)
    }
}
    
// just stop screen record
screenRecordHelper?.apply {
    if (isRecording) {
        stopRecord()     
    }
}
Step 4. if you want to mix the audio into your video,you just should do
// parameter1 -> The last video length you want
// parameter2 -> the audio's duration
// parameter2 -> assets resource
stopRecord(duration, audioDuration, afdd)
Step 5. If you still don’t understand, please refer to the demo

由於個人水平有限,雖然目前抗住了公司產品的考驗,但肯定還有很多地方沒有支持全面,希望有知道的大佬不嗇賜教,有任何兼容性問題請直接提 issues,Thx。

參考文章:http://lastwarmth.win/2018/11/23/media-mix/
https://juejin.im/post/5afaee7df265da0ba2672608

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

不吹不黑,關於 Java 類加載器的這一點,市面上沒有任何一本圖書講到,實戰分析Tomcat的類加載器結構(使用Eclipse MAT驗證),還是Tomcat,關於類加載器的趣味實驗,重寫類加載器,實現簡單的熱替換,@Java Web 程序員,我們一起給程序開個後門吧:讓你在保留現場,服務不重啟的情況下,執行我們的調試代碼,@Java web程序員,在保留現場,服務不重啟的情況下,執行我們的調試代碼(JSP 方式)

類加載器第7彈:

實戰分析Tomcat的類加載器結構(使用Eclipse MAT驗證)

還是Tomcat,關於類加載器的趣味實驗

了不得,我可能發現了Jar 包衝突的秘密

重寫類加載器,實現簡單的熱替換

@Java Web 程序員,我們一起給程序開個後門吧:讓你在保留現場,服務不重啟的情況下,執行我們的調試代碼

@Java web程序員,在保留現場,服務不重啟的情況下,執行我們的調試代碼(JSP 方式)

 

 

一、一個程序員的思考

大家都知道,Tomcat 處理業務,靠什麼?最終是靠我們自己編寫的 Servlet。你可能說你不寫 servlet,你用 spring MVC,那也是人家幫你寫好了,你只需要配置就行。在這裏,有一個邊界,Tomcat 算容器,容器的相關 jar 包都放在它自己的 安裝目錄的 lib 下面; 我們呢,算是業務,算是webapp,我們的 servlet ,不管是自定義的,還是 spring mvc 的DispatcherServlet,都是放在我們的 war 包裏面 WEB-INF/lib下。 看過前面文章的同學是曉得的, 這二者是由不同的類加載器加載的。在 Tomcat 的實現中,會委託 webappclassloader 去加載WAR 包中的 servlet ,然後 反射生成對應的 servlet。後續有請求來了,調用生成的 servlet 的 service 方法即可。

在 org.apache.catalina.core.StandardWrapper#loadServlet 中,即負責 生成 servlet:

 

 org.apache.catalina.core.DefaultInstanceManager#newInstance(java.lang.String)
@Override
public Object newInstance(String className) throws IllegalAccessException, InvocationTargetException, NamingException, InstantiationException, ClassNotFoundException { Class<?> clazz = loadClassMaybePrivileged(className, classLoader); return newInstance(clazz.newInstance(), clazz); }

 

在上圖中,會利用 instanceManager 根據參數中指定的 servletClass 去生成 servlet 實例。newInstance 代碼如下,主要就是用 當前 context 的classloader 去加載 該 servlet,然後 反射生成 servlet 對象。

我們重點關注的是那個紅框圈出的強轉:為什麼由 webappclassloader 加載的對象,可以轉換 為 Tomcat common classloader 加載的 Servlet 呢? 按理說,兩個不同的類加載器加載的類都是互相隔離的啊,不應該拋一個 ClassCastException 嗎?說真的,我翻了不少書,從來沒提到這個,就連網上也很含糊。

 

再來一個,關於SPI的問題。  在 SPI 中(有興趣的同學可以自行查詢,網上很多,我隨便找了一篇:https://www.jianshu.com/p/46b42f7f593c),主要是由 java 社區指定規範,比如 JDBC,廠家有那麼多,mysql,oracle,postgre,大家都有自己的 jar包,要是沒有 JDBC 規範,我們估計就得針對各個廠家的實現類編程了,那遷移就麻煩了,你針對 mysql 數據庫寫的代碼,換成 oracle 的話,代碼不改是肯定不能跑的。所以, JCP組織制定了 JDBC 規範,JDBC 規範中指定了一堆的 接口,我們平時開發,只需要針對接口來編程,而實現怎麼辦,交給各廠家唄,由廠家來實現 JDBC 規範。這裏以代碼舉例,oracle.jdbc.OracleDriver 實現了 java.sql.Driver,同時,在 oracle.jdbc.OracleDriver 的 static 初始化塊中,有下面的代碼:

 

    static { try { if (defaultDriver == null) { defaultDriver = new oracle.jdbc.OracleDriver();  DriverManager.registerDriver(defaultDriver);  } // 省略
    }

其中,標紅這句,就是 Oracle Driver 要向 JDBC 接口註冊自己,java.sql.DriverManager#registerDriver(java.sql.Driver)的實現如下:

java.sql.DriverManager#registerDriver(java.sql.Driver) 

public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException { registerDriver(driver, null); }

 

可以看到,registerDriver(java.sql.Driver)  方法的參數為 java.sql.Driver,而我們傳的參數為 oracle.jdbc.OracleDriver 類型,這兩個類型,分別由不同的類加載器加載(java.sql.Driver 由 jdk 的 啟動類加載器加載,而 oracle.jdbc.OracleDriver ,如果為 web應用,則為 tomcat 的 webappclassloader 來加載,不管怎麼說,反正不是由 jdk 加載的),這樣的兩個類型,連 類加載器都不一樣,怎麼就能正常轉換呢,為啥不拋 ClassCastException?

 

 二、不同類加載器加載的類,可以轉換的關鍵

經過上面兩個例子的觀察,不知道大家發現沒, 我們都是把一個實現,轉換為一個接口。也許,這就是問題的關鍵。我們可以大膽地推測,基於類的雙親委派機制,在 加載 實現類的時候,jvm 遇到 實現類中引用到的其他類,也會觸發加載,加載的過程中,會觸發 loadClass,比如,加載 webappclassloader 在 加載 oracle.jdbc.OracleDriver 時,觸發加載 java.sql.Driver,但是 webappclassloader 明顯是不能去加載 java.sql.Driver 的,於是會委託給 jdk 的類加載,所以,最終,oracle.jdbc.OracleDriver 中 引用的 java.sql.Driver ,其實就是由 jdk 的類加載器去加載的。 而  registerDriver(java.sql.Driver driver) 中的 driver 參數的類型 java.sql.Driver 也是由 jdk 的類加載器去加載的,二者相同,所以自然可以相互轉換。

 

這裏總結一句(不一定對),在同時滿足以下幾個條件的情況下:

前置條件1、接口 jar包 中,定義一個接口 Test

前置條件2、實現 jar 包中,定義 Test 的實現類,比如 TestImpl。(但是不要在該類中包含該 接口,你說沒法編譯,那就把接口 jar包放到 classpath)

前置條件3、接口 jar 包由 interface_classLoader 加載,實現 jar 包 由 impl_classloader 加載,其中 impl_classloader 會在自己無法加載時,委派給 interface_classLoader 

 

則,定義在 實現jar 中的Test 接口的實現類,反射生成的對象,可以轉換為 Test 類型。

 

猜測說完了,就是求證過程。

 

三、求證

1、定義接口 jar 

D:\classloader_interface\ITestSample.java  

/**
* desc: * * @author : * creat_date: 2019/6/16 0016 * creat_time: 19:28 **/ public interface ITestSample { }

 

cmd下,執行:

D:\classloader_interface>javac ITestSample.java
D:\classloader_interface>jar cvf interface.jar ITestSample.class 已添加清單 正在添加: ITestSample.class(輸入 = 103) (輸出 = 86)(壓縮了 16%)

 

此時,即可在當前目錄下,生成 名為 interface.jar 的接口jar包。

 

2、定義接口的實現 jar

在不同目錄下,新建了一個實現類。

D:\classloader_impl\TestSampleImpl.java /**
 * Created by Administrator on 2019/6/25. */ public class TestSampleImpl implements ITestSample{ }

編譯,打包:

1 D:\classloader_impl>javac -cp D:\classloader_interface\interface.jar TestSampleI 2 mpl.java 3 
4 D:\classloader_impl>jar -cvf impl.jar TestSampleImpl.class 5 已添加清單 6 正在添加: TestSampleImpl.class(輸入 = 221) (輸出 = 176)(壓縮了 20%)

 

請注意上面的標紅行,不加編譯不過。

 

3、測試

測試的思路是,用一個urlclassloader 去加載 interface.jar 中的 ITestSample,用另外一個 URLClassLoader 去加載 impl.jar 中的 TestSampleImpl ,然後用java.lang.Class#isAssignableFrom 判斷後者是否能轉成前者。

 

 1 import java.lang.reflect.Method;  2 import java.net.URL;  3 import java.net.URLClassLoader;  4 
 5 /**
 6  * desc:  7  *  8  * @author : caokunliang  9  * creat_date: 2019/6/14 0014 10  * creat_time: 17:04 11  **/
12 public class MainTest { 13 
14 
15     public static void testInterfaceByOneAndImplByAnother()throws Exception{ 16         URL url = new URL("file:D:\\classloader_interface\\interface.jar"); 17         URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url}); 18         Class<?> iTestSampleClass = urlClassLoader.loadClass("ITestSample"); 19 
20 
21         URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar"); 22         URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader); 23         Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl"); 24 
25 
26         System.out.println("實現類能轉否?:"  + iTestSampleClass.isAssignableFrom(testSampleImplClass)); 27 
28  } 29 
30     public static void main(String[] args) throws Exception { 31  testInterfaceByOneAndImplByAnother(); 32  } 33 
34 }

 

打印如下:

 

4、延伸測試1

如果我們做如下改動,你猜會怎樣? 這裏的主要差別是:

改之前,urlClassloader 作為 parentClassloader: 

        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader);

改之後,不傳,默認會以 jdk 的應用類加載器作為 parent:

        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl});

 

打印結果是:

Exception in thread "main" java.lang.NoClassDefFoundError: ITestSample at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:760) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at java.net.URLClassLoader.defineClass(URLClassLoader.java:455) at java.net.URLClassLoader.access$100(URLClassLoader.java:73) at java.net.URLClassLoader$1.run(URLClassLoader.java:367) at java.net.URLClassLoader$1.run(URLClassLoader.java:361) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:360) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at MainTest.testInterfaceByOneAndImplByAnother(MainTest.java:23) at MainTest.main(MainTest.java:33) Caused by: java.lang.ClassNotFoundException: ITestSample at java.net.URLClassLoader$1.run(URLClassLoader.java:372) at java.net.URLClassLoader$1.run(URLClassLoader.java:361) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:360) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ... 13 more

 

 結果就是,第23行,Class<?> testSampleImplClass = implUrlClassLoader.loadClass(“TestSampleImpl”); 這裏報錯了,提示找不到 ITestSample。

這就是因為,在加載了 implUrlClassLoader 后,觸發了對 ITestSample 的隱式加載,這個隱式加載會用哪個加載器去加載呢,沒有默認指明的情況下,就是用當前的類加載器,而當前類加載器就是 implUrlClassLoader ,但是這個類加載器開始加載 ITestSample,它是遵循雙親委派的,它的parent 加載器 即為 appclassloader,(jdk的默認應用類加載器),但appclassloader 根本不能加載 ITestSample,於是還是還給 implUrlClassLoader ,但是 implUrlClassLoader  也不能加載,於是拋出異常。

 

5、延伸測試2

我們再做一個改動, 改動處和上一個測試一樣,只是這次,我們傳入了一個特別的類加載器,作為其 parentClassLoader。 它的特殊之處在於,almostSameUrlClassLoader 和 前面加載 interface.jar 的類加載器一模一樣,只是是一個新的實例。

 URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url});  URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, almostSameUrlClassLoader);

 

這次,看看結果吧,也許你猜到了?

 

這次沒報錯了,畢竟 almostSameUrlClassLoader  知道去哪裡加載 ITestSample,但是,最後的結果显示,實現類的 class 並不能 轉成 ITestSample。

 

6、延伸測試3

說實話,有些同學可能對 java.lang.Class#isAssignableFrom 不是很熟悉,我們換個你更不熟悉的,如何?

        URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar"); URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url}); URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, almostSameUrlClassLoader); Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl"); Object o = testSampleImplClass.newInstance(); Object cast = iTestSampleClass.cast(o); // 將 o 轉成 接口的那個類 System.out.println(cast);

結果:

 

如果換成下面這樣,就沒啥問題:

        URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar"); URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url}); URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader); Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl"); Object o = testSampleImplClass.newInstance(); Object cast = iTestSampleClass.cast(o); System.out.println(cast);

 

執行:

 

四、總結

大家將就看吧,第三章的測試如果仔細看下來,基本就能理解了。 其實,除了 接口這種方式,貌似 繼承 的方式也是可以的,改天再試驗下。 這一塊,不知道為啥,我是真的在網上書上沒找到,但其實很重要,改天找找虛擬機層面的實現代碼吧。 大家如果覺得有幫助,麻煩點個推薦,對於寫作的人來說,這莫過於最大的獎勵了。

 

參考:

深入探討 Java 類加載器

 

 

 

 

 

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

一次向linux開源社區提交補丁的經歷

背景

在開發過程中,偶然發現了spinand驅動的一個bug,滿懷欣喜地往社區提補丁。這是怎麼樣的一個bug呢?

static int spinand_mtd_read(struct mtd_info *mtd, loff_t from,
                            struct mtd_oob_ops *ops)
{
        ......
        nanddev_io_for_each_page(nand, from, ops, &iter) {
                ......
                ret = spinand_read_page(spinand, &iter.req, enable_ecc);
                if (ret < 0 && ret != -EBADMSG)     /* 讀取數據出錯 */
                        break;

                if (ret == -EBADMSG) {
                        /* -EBADMSG 返回表示壞塊 */
                        ecc_failed = true;
                        mtd->ecc_stats.failed++;
                        ret = 0;
                } else {
                        /* 出現位翻轉或者讀取正常,則記錄歷史位翻轉最大值 */
                        mtd->ecc_stats.corrected += ret;
                        max_bitflips = max_t(unsigned int, max_bitflips, ret);
                }

                ops->retlen += iter.req.datalen; 
                ops->oobretlen += iter.req.ooblen;
        }

        if (ecc_failed && !ret)
                ret = -EBADMSG;

        return ret ? ret : max_bitflips;
}

代碼邏輯如下:

  1. 遍歷讀取每一個page
  2. 如果讀出錯則直接返回
  3. 如果出現壞塊,則置位ecc_failed,在函數最後會檢查此標誌
  4. 如果出現位翻轉,則暫存最大位翻轉的bit位數量
  5. 全部讀取完后,如果有置位ecc_failed,則返回壞塊錯誤碼;如果出現位翻轉,則返回最大位翻轉;否則返回0,表示正常

問題出在於,如果剛好最後一次讀取出現位翻轉,此時ret != 0就直接退出循環,此時會導致壞塊標識無效,且返回最後的位翻轉量而非歷史位翻轉最大值。這是代碼不嚴謹的地方。

第一次提交

修改補丁如下,補丁邏輯不再解釋。

In function spinand_mtd_read, if the last page to read occurs bitflip,
this function will return error value because veriable ret not equal to 0.

Signed-off-by: liaoweixiong <liaoweixiong@allwinnertech.com>
---
 drivers/mtd/nand/spi/core.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/mtd/nand/spi/core.c b/drivers/mtd/nand/spi/core.c
index 556bfdb..6b9388d 100644
--- a/drivers/mtd/nand/spi/core.c
+++ b/drivers/mtd/nand/spi/core.c
@@ -511,12 +511,12 @@ static int spinand_mtd_read(struct mtd_info *mtd, loff_t from,
        if (ret == -EBADMSG) {
            ecc_failed = true;
            mtd->ecc_stats.failed++;
-           ret = 0;
        } else {
            mtd->ecc_stats.corrected += ret;
            max_bitflips = max_t(unsigned int, max_bitflips, ret);
        }
 
+       ret = 0;
        ops->retlen += iter.req.datalen;
        ops->oobretlen += iter.req.ooblen;
    }

21:13分發出的郵件,21:45分陸續收到兩個回復:

<maintainer A>:

Actually, that's exactly what the MTD core expects (see [1]), so you're
the one introducing a regression here.
<maintainer B>:

To me it looks like the patch description is somewhat incorrect, but the 
fix itself looks okay, unless I'm getting it wrong.

In case of the last page containing bitflips (ret > 0), 
spinand_mtd_read() will return that number of bitflips for the last 
page. But to me it looks like it should instead return max_bitflips like 
it does when the last page read returns with 0.

以及隔天回復

<maintainer A>:

Oh, you're right. liaoweixiong, can you adjust the commit message
accordingly?

好吧,問題出在與我沒把問題描述清楚,改改再提交

第二次提交

只改了comment和補丁標題:

Subject: [PATCH v2] mtd: spinand: read return badly if the last page has bitflips

In case of the last page containing bitflips (ret > 0), 
spinand_mtd_read() will return that number of bitflips for the last 
page. But to me it looks like it should instead return max_bitflips like 
it does when the last page read returns with 0.

然後嘩啦啦收到兩個Reviewed-by,附帶一個建議:

Reviewed-by: <maintainer B>

This should probably be resent with the following tags:

Cc: stable@vger.kernel.org
Fixes: 7529df465248 ("mtd: nand: Add core infrastructure to support SPI 
NANDs")

得,再提交一次吧

第三次提交

此時的我提交補丁到社區經驗並不多,Maintainer讓我resend,我就忐忑開始胡思亂想了:

版本號需要累加么?該怎麼標記是重新發送?有兩個maintainer已經”認可”了我的補丁(reviewed-by),我改怎麼體現到新的郵件中?

仔細想想內容並沒改,因此不需要累加版本號;查詢前人提交,在郵件標題可以加上RESEND字樣;搜索含RESEND字樣的前人郵件,剛好找到一個在maintainer reviewed后resend為acked,寫在signed-off-by區。

OK,確定下來就重新發吧

Subject: [RESEND PATCH v2] mtd: spinand: read return badly if the last page has bitflips

......
Signed-off-by: liaoweixiong <liaoweixiong@allwinnertech.com>
Acked-by: <maintainer A>
Acked-by: <maintainer B>
Fixes: 7529df465248 ("mtd: nand: Add core infrastructure to support SPI NANDs")

很快,就挨批了…

第四次提交

晚上10點多,收到回復:

<maintainer B>

Why did you change our Reviewed-by tags to Acked-by tags?

額…我也是看別人這麼做我才這麼做的,大佬生氣了!趕緊補救

......
Reviewed-by: <maintainer A>
Reviewed-by: <maintainer B>
Fixes: 7529df465248 ("mtd: nand: Add core infrastructure to support SPI NANDs")

第五次提交

埋下的坑終究是要踩的,很快,再次挨批了

<maintainer C>

This is not the correct way to submit patches for inclusion in the
stable kernel tree.  Please read:
    https://www.kernel.org/doc/html/latest/process/stable-kernel-rules.html
for how to do this properly.
<maintainer A>

FYI, you should not send the patch to stable@vger.kernel.org, but 
instead, as I said in my other reply, add the tag "Cc: 
stable@vger.kernel.org". See "Option 1" in the document Greg referred to.

小白趕緊狠補基礎操作規範…

第六次提交

......
Reviewed-by: <maintainer A>
Reviewed-by: <maintainer B>
Cc: stable@vger.kernel.org
Fixes: 7529df465248 ("mtd: nand: Add core infrastructure to support SPI NANDs")

總結

哎,我只是挪了一行代碼的位置而已啊,Maintainer嚴審下,我竟然提交了6次!6次!突然感覺心好累。

累歸累,問題總結還是需要的

  1. 新手不具備提交代碼的一些常識,包括 a) 提交中各個tag的含義,在什麼時候加這些tag,例如Reviewed-by和Acked-by的差別 b) 提交補丁到stable的注意事項
  2. 對補丁的問題描述不夠仔細清楚,導致 無法理解,幸好 幫我澄清了

解決方法:

  1. linux提交有規範文檔的,抽時間擼一遍,並翻譯發博客
  2. 在發補丁之前,讓身邊的人幫忙看一下補丁說明是否足夠清晰

希望我的經歷能幫助到正在或者準備向Linux內核開源社區的小夥伴

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

【面試】如果把線程當作一個人來對待,所有問題都瞬間明白了

多線程的問題都曾經困擾過每個開發人員,今天將從全新視角來解說,希望讀者都能明白。

強烈建議去運行下文章中的示例代碼,自己體會下。



問題究竟出在哪裡?

一個線程執行,固然是安全的,但是有時太慢了,怎麼辦?

老祖宗告訴我們,“一方有難,八方支援”,那不就是多叫幾個線程來幫忙嘛,好辦呀,多new幾個不就行了,又不要錢。這樣能管用嗎?繼續往下看。

俗話說,“在家靠父母,出門靠朋友”。有了朋友的幫助,就會事半功倍。是這樣的嗎?

不一定,如果朋友“不靠譜”,結果竟是在“添亂”。於是就演變為,“不怕神一樣的對手,就怕豬一樣的隊友”。可見“人多力量大”縱然是對的,但也要配合好才能成事。

人和人是朋友,那線程和線程也是“朋友”,如果多線程之間不能配合好的話,最終也會變為“豬一樣的隊友”。事實證明,這也不是一件易事。且容我慢慢道來。

開發是一門技術,管理是一門藝術。也許你正想帶着兄弟們大幹一場,可偏偏就有人要辭職。或者你付出了這麼多,但別人從來沒有感動過。為什麼會這樣呢?

因為你面對的是人。每個人都是獨立的個體,有思想,有靈魂,有情感,有三觀。能夠接受外界的“輸入”,經過“處理”后,能夠產生“輸出”。

說白了就是會自主的分析問題,並做出決定。這叫什麼呢?答案就是,主觀能動性。

擁有主觀能動性的物體(比如人),你需要和它協商着或配合著來共同完成一件事情,而不能“強迫”它去做什麼,因為這樣往往不會有好的結果。

費了這麼多口舌,就是希望把問題盡量的簡單化。終於可以回到程序了,那線程的情況是不是類似的呢?答案是肯定的。

一個線程準備好后,經過CPU的調度,就可以自主的運行了。此時它儼然成了一個獨立的個體,且具有主觀能動性。

這本是一件好事,但卻也有不好的一面,那就是你對它的“掌控”能力變弱了,頗有一種“將在外,君命有所不受”的感覺。

可能你不同意這種看法,說我可以“強迫”它停止運行,調用Thread類的stop()方法來直接把它“掐死”,不好意思,該方法已廢棄。

因為線程可能在運行一些“關鍵”代碼(比如轉賬),此刻不能被終止。Thread類還有一些其它的方法也都廢棄了,大抵原因其實都差不多。

講了這麼多,相信你已經明白了,簡單總結一下:

事情起因:線程可以獨立自主的運行,可以認為它具有主觀能動性。

造成結果:對它的掌控能力變弱了,而且又不能直接把它“幹掉”。

解決方案:凡事商量着來,互相配合著把事情完成。

作者觀點:其實就是把線程當作人來對待。



小試牛刀一下

一旦把線程當成人,就來到了人類的世界,這我們太熟悉了,所以很多問題都會變得非常簡單明了。一起來看看吧。

場景一,停止

“大胖,大胖,12點了,該去吃飯了,別寫了”

“好的,好的,稍等片刻,把這幾行代碼寫完就走”

要點:把停止的信號傳達給別人,別人處理完手頭的事情就自己主動停止了。

 static void stopByFlag() {
    ARunnable ar = new ARunnable();
    new Thread(ar).start();
    ar.tellToStop();
  }

  static class ARunnable implements Runnable {

    volatile boolean stop;

    void tellToStop() {
      stop = true;
    }

    @Override
    public void run() {
      println("進入不可停止區域 1。。。");
      doingLongTime(5);
      println("退出不可停止區域 1。。。");
      println("檢測標誌stop = %s", String.valueOf(stop));
      if (stop) {
        println("停止執行");
        return;
      }
      println("進入不可停止區域 2。。。");
      doingLongTime(5);
      println("退出不可停止區域 2。。。");
    }

  }

 

解說:線程在預設的地點檢測flag,來決定是否停止。


場景二,暫停/恢復

“大胖,大胖,先別發請求了,對方服務器快掛了”

“好的,好的,等這個執行完就不發了”

過了一會

“大胖,大胖,可以重新發請求了”

“好的,好的”

要點:把暫停的信號傳達給別人,別人處理完手頭的事情就自己主動暫停了。但是恢復是無法自主進行的,只能由操作系統來恢複線程的執行。

 

static void pauseByFlag() {
    BRunnable br = new BRunnable();
    new Thread(br).start();
    br.tellToPause();
    sleep(8);
    br.tellToResume();
  }

  static class BRunnable implements Runnable {

    volatile boolean pause;

    void tellToPause() {
      pause = true;
    }

    void tellToResume() {
      synchronized (this) {
        this.notify();
      }
    }

    @Override
    public void run() {
      println("進入不可暫停區域 1。。。");
      doingLongTime(5);
      println("退出不可暫停區域 1。。。");
      println("檢測標誌pause = %s", String.valueOf(pause));
      if (pause) {
        println("暫停執行");
        try {
          synchronized (this) {
            this.wait();
          }
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        println("恢復執行");
      }
      println("進入不可暫停區域 2。。。");
      doingLongTime(5);
      println("退出不可暫停區域 2。。。");
    }

  }

解說:還是在預設的地點檢測flag。然後就是wait/notify配合使用。


場景三,插隊

“大胖,大胖,讓我站到你前面,不想排隊了”

“好吧”

要點:別人插隊到你前面,必須等他完事後才輪到你。

static void jqByJoin() {
    CRunnable cr = new CRunnable();
    Thread t = new Thread(cr);
    t.start();
    sleep(1);
    try {
      t.join();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    println("終於輪到我了");
  }

  static class CRunnable implements Runnable {

    @Override
    public void run() {
      println("進入不可暫停區域 1。。。");
      doingLongTime(5);
      println("退出不可暫停區域 1。。。");
    }

  }

 

解說:join方法可以讓某個線程插到自己前面,等它執行完,自己才會繼續執行。


場景四,叫醒

“大胖,大胖,醒醒,醒醒,看誰來了”

“誰啊,我去”

要點:要把別人從睡夢中叫醒,一定要採取稍微暴力一點的手段。

static void stopByInterrupt() {
    DRunnable dr = new DRunnable();
    Thread t = new Thread(dr);
    t.start();
    sleep(2);
    t.interrupt();
  }

  static class DRunnable implements Runnable {

    @Override
    public void run() {
      println("進入暫停。。。");
      try {
        sleep2(5);
      } catch (InterruptedException e) {
        println("收到中斷異常。。。");
        println("做一些相關處理。。。");
      }
      println("繼續執行或選擇退出。。。");
    }

  }

 

解說:線程在sleep或wait時,是處於無法交互的狀態的,此時只能使用interrupt方法中斷它,線程會被激活並收到中斷異常。



常見的協作配合

上面那些場景,其實都是對一個線程的操作,下面來看多線程間的一些配合。

事件一,考試

假設今天考試,20個學生,1個監考老師。規定學生可以提前交卷,即把卷子留下,直接走人就行了。

但老師必須等到所有的學生都走後,才可以收卷子,然後裝訂打包。

如果把學生和老師都看作線程,就是1個線程和20個線程的配合問題,即等20個線程都結束了,這1個線程才開始。

比如20個線程分別在計算數據,等它們都結束后得到20个中間結果,最後這1個線程再進行後續匯總、處理等。

  static final int COUNT = 20;
  static CountDownLatch cdl = new CountDownLatch(COUNT);

  public static void main(String[] args) throws Exception {
    new Thread(new Teacher(cdl)).start();
    sleep(1);
    for (int i = 0; i < COUNT; i++) {
      new Thread(new Student(i, cdl)).start();
    }
    synchronized (ThreadCo1.class) {
      ThreadCo1.class.wait();
    }
  }

  static class Teacher implements Runnable {

    CountDownLatch cdl;

    Teacher(CountDownLatch cdl) {
      this.cdl = cdl;
    }

    @Override
    public void run() {
      println("老師髮捲子。。。");
      try {
        cdl.await();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      println("老師收卷子。。。");
    }

  }

  static class Student implements Runnable {

    CountDownLatch cdl;
    int num;

    Student(int num, CountDownLatch cdl) {
      this.num = num;
      this.cdl = cdl;
    }

    @Override
    public void run() {
      println("學生(%d)寫卷子。。。", num);
      doingLongTime();
      println("學生(%d)交卷子。。。", num);
      cdl.countDown();
    }

  }

 

解說:每完成一個線程,計數器減1,當減到0時,被阻塞的線程自動執行。


事件二,旅遊

最近景色宜人,公司組織去登山,大夥都來到了山腳下,登山過程自由進行。

但為了在特定的地點拍集體照,規定1個小時后在半山腰集合,誰最後到的,要給大家表演一個節目。

然後繼續登山,在2個小時后,在山頂集合拍照,還是誰最後到的表演節目。

接着開始下山了,在2個小時后在山腳下集合,點名回家,最後到的照例表演節目。

  static final int COUNT = 5;
  static CyclicBarrier cb = new CyclicBarrier(COUNT, new Singer());

  public static void main(String[] args) throws Exception {
    for (int i = 0; i < COUNT; i++) {
      new Thread(new Staff(i, cb)).start();
    }
    synchronized (ThreadCo2.class) {
      ThreadCo2.class.wait();
    }
  }

  static class Singer implements Runnable {

    @Override
    public void run() {
      println("為大家唱歌。。。");
    }

  }

  static class Staff implements Runnable {

    CyclicBarrier cb;
    int num;

    Staff(int num, CyclicBarrier cb) {
      this.num = num;
      this.cb = cb;
    }

    @Override
    public void run() {
      println("員工(%d)出發。。。", num);
      doingLongTime();
      println("員工(%d)到達地點一。。。", num);
      try {
        cb.await();
      } catch (Exception e) {
        e.printStackTrace();
      }
      println("員工(%d)再出發。。。", num);
      doingLongTime();
      println("員工(%d)到達地點二。。。", num);
      try {
        cb.await();
      } catch (Exception e) {
        e.printStackTrace();
      }
      println("員工(%d)再出發。。。", num);
      doingLongTime();
      println("員工(%d)到達地點三。。。", num);
      try {
        cb.await();
      } catch (Exception e) {
        e.printStackTrace();
      }
      println("員工(%d)結束。。。", num);
    }

  }

 

解說:某個線程到達預設點時就在此等待,等所有的線程都到達時,大家再一起向下個預設點出發。如此循環反覆下去。


事件三,勞動

大胖和小白去了創業公司,公司為了節約開支,沒有請專門的保潔人員。讓員工自己掃地和擦桌。

大胖覺得擦桌輕鬆,就讓小白去掃地。可小白覺得掃地太累,也想擦桌。

為了公平起見,於是決定,每人先干一半,然後交換工具,再接着干對方剩下的那一個半。

  static Exchanger<Tool> ex = new Exchanger<>();

  public static void main(String[] args) throws Exception {
    new Thread(new Staff("大胖", new Tool("笤帚", "掃地"), ex)).start();
    new Thread(new Staff("小白", new Tool("抹布", "擦桌"), ex)).start();
    synchronized (ThreadCo3.class) {
      ThreadCo3.class.wait();
    }
  }

  static class Staff implements Runnable {

    String name;
    Tool tool;
    Exchanger<Tool> ex;

    Staff(String name, Tool tool, Exchanger<Tool> ex) {
      this.name = name;
      this.tool = tool;
      this.ex = ex;
    }

    @Override
    public void run() {
      println("%s拿的工具是[%s],他開始[%s]。。。", name, tool.name, tool.work);
      doingLongTime();
      println("%s開始交換工具。。。", name);
      try {
        tool = ex.exchange(tool);
      } catch (Exception e) {
        e.printStackTrace();
      }

      println("%s的工具變為[%s],他開始[%s]。。。", name, tool.name, tool.work);
    }

  }

  static class Tool {

    String name;
    String work;

    Tool(String name, String work) {
      this.name = name;
      this.work = work;
    }

  }

 

解說:兩個線程在預設點交換變量,先到達的等待對方。


事件四,魔性遊戲

這是一個充滿魔性的小遊戲,由一個團隊一起參加。所有人每隔5秒鐘抽一次簽,每個人有50%的概率留下來或被淘汰。

留下來的人下次抽籤時同樣有50%的概率被淘汰。被淘汰的人下次抽籤時同樣有50%的概率復活。

團隊所有成員都被淘汰完,為挑戰失敗,團隊所有成員都回到遊戲中(除剛開始外),為挑戰成功。

比如一開始10人參與遊戲,第一輪抽籤后,6人留下,4人淘汰。

第二輪抽籤后,留下的6人中4人被淘汰,淘汰的4人中2人復活,那麼目前是4人在遊戲中,6人被淘汰。

一直如此繼續下去,直到10人全部被淘汰,或全部回到遊戲中。

可見,人數越多,全部被淘汰的概率越小,但全部回到遊戲中的概率也越小。

反之,人數越少,全部回到遊戲中的概率越大,但全部被淘汰的概率也越大。

是不是很有魔性啊。哈哈。

  static final int COUNT = 6;
  static Phaser ph = new Phaser() {
    protected boolean onAdvance(int phase, int registeredParties) {
      println2("第(%d)局,剩餘[%d]人", phase, registeredParties);
      return registeredParties == 0 ||
          (phase != 0 && registeredParties == COUNT);
    };
  };

  public static void main(String[] args) throws Exception {
    new Thread(new Challenger("張三")).start();
    new Thread(new Challenger("李四")).start();
    new Thread(new Challenger("王五")).start();
    new Thread(new Challenger("趙六")).start();
    new Thread(new Challenger("大胖")).start();
    new Thread(new Challenger("小白")).start();
    synchronized (ThreadCo4.class) {
      ThreadCo4.class.wait();
    }
  }

  static class Challenger implements Runnable {

    String name;
    int state;

    Challenger(String name) {
      this.name = name;
      this.state = 0;
    }

    @Override
    public void run() {
      println("[%s]開始挑戰。。。", name);
      ph.register();
      int phase = 0;
      int h;
      while (!ph.isTerminated() && phase < 100) {
        doingLongTime(5);
        if (state == 0) {
          if (Decide.goon()) {
            h = ph.arriveAndAwaitAdvance();
            if (h < 0)
              println("No%d.[%s]繼續,但已勝利。。。", phase, name);
            else
              println("No%d.[%s]繼續at(%d)。。。", phase, name, h);
          } else {
            state = -1;
            h = ph.arriveAndDeregister();
            println("No%d.[%s]退出at(%d)。。。", phase, name, h);
          }
        } else {
          if (Decide.revive()) {
            state = 0;
            h = ph.register();
            if (h < 0)
              println("No%d.[%s]復活,但已失敗。。。", phase, name);
            else
              println("No%d.[%s]復活at(%d)。。。", phase, name, h);
          } else {
            println("No%d.[%s]沒有復活。。。", phase, name);
          }
        }
        phase++;
      }
      if (state == 0) {
        ph.arriveAndDeregister();
      }
      println("[%s]結束。。。", name);
    }

  }

  static class Decide {

    static boolean goon() {
      return random(9) > 4;
    }

    static boolean revive() {
      return random(9) < 5;
    }
  }

 

解說:某個線程到達預設點后,可以選擇等待同伴或自己退出,等大家都到達后,再一起向下一個預設點出發,隨時都可以有新的線程加入,退出的也可以再次加入。



生產與銷售的問題

在現實中,工廠生產出來的產品會先放到倉庫存儲,銷售人員簽了單子后,會從倉庫把產品發給客戶。

如果生產的過快,倉庫里產品越堆越多,直到把倉庫堆滿,那就必須停止生產,因為沒地方放了。

此時只能讓銷售人員趕緊出去簽單子,把產品發出去,倉庫就有了空間,可以恢復生產了。

如果銷售的過快,倉庫里產品越來越少,直到把倉庫清空,那就必須停止銷售,因為沒產品了。

此時只能讓生產人員趕緊生產產品,把產品放到倉庫里,倉庫里就有了產品,可以恢復銷售了。

可能會有人問,為什麼不讓生產和銷售直接掛鈎呢,把倉庫這個環節去掉?

這樣會造成兩種不好的情況:

一是突然來了很多單子,生產人員累成死Dog也生產不出來。

二是很長時間沒有單子,生產人員閑成廢Dog也無事可做。

用稍微“專業”點的術語就是此時的生產和銷售是一種強耦合的關係,銷售的波動對生產影響太大。

倉庫就是一個緩衝區,能有效的吸收波動,很大程度上減少波動的傳遞,起到一種解耦作用,由強耦合變成一種鬆散耦合。

這其實就對應計算機里經典的生產者和消費者問題。

經典的生產者和消費者

一到多個線程充當生產者,生產元素。一到多個線程充當消費者,消費元素。

在兩者之間插入一個隊列(Queue)充當緩衝區,建立起生產者和消費者的鬆散耦合。

正常情況下,即生產元素的速度和消費元素的速度差不多時,生產者和消費者其實是不需要去關注對方的。

生產者可以一直生產,因為隊列里總是有空間。消費者可以一直消費,因為隊列里總是有元素。即達到一個動態的平衡。

但在特殊情況下,比如生產元素的速度很快,隊列里沒有了空間,此時生產者必須自我“ba工”,開始“睡大覺”。

一旦消費者消費了元素之後,隊列里才會有空間,生產者才可以重啟生產,所以,消費者在消費完元素後有義務去叫醒生產者復工。

更準確的說法應該是,只有在生產者“睡大覺”時,消費者消費完元素后才需要去叫醒生產者。否則,其實可以不用叫醒,因為人家本來就沒睡。

反之,如果消費元素的速度很快,隊列里沒有了元素,只需把上述情況顛倒過來即可。

但這樣的話就會引入一個新的問題,就是要能夠準備的判斷出對方有沒有在睡大覺,為此就必須定義一個狀態變量,在自己即將開始睡大覺時,自己設置下這個變量。

對方通過檢測這個變量,來決定是否進行叫醒操作。當自己被叫醒后,首先要做的就是清除一下這個變量,表明我已經醒來複工了。

這樣就需要多維護一個變量和多了一部分判斷邏輯。可能有些人會覺得可以通過判斷隊列的“空”或“滿”(即隊列中的元素數目)來決定是否進行叫醒操作。

在高併發下,可能剛剛判斷隊列不為空,瞬間之後隊列可能已經變為空的了,這樣會導致邏輯出錯。線程可能永遠無法被叫醒。

因此,綜合所有,生產者每生產一個元素后,都會通知消費者,“現在有元素的,你可以消費”。

同樣,消費者每消費一個元素后,也會通知生產者,“現在有空間的,你可以生產”。

很明顯,這些通知很多時候(即對方沒有睡大覺時)是沒有真正意義的,不過無所謂,只要忽略它們就行了。

就是“寧可錯殺一千,也不放過一個”。首先要保證是正確的,然後才有資格去BB別的。

  public static void main(String[] args) {
    Queue queue = new Queue();
    new Thread(new Producer(queue)).start();
    new Thread(new Producer(queue)).start();
    new Thread(new Consumer(queue)).start();
  }

  static class Producer implements Runnable {

    Queue queue;

    Producer(Queue queue) {
      this.queue = queue;
    }

    @Override
    public void run() 
{
      try {
        for (int i = 0; i < 10000; i++) {
          doingLongTime();
          queue.putEle(random(10000));
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    }

  }

  static class Consumer implements Runnable {

    Queue queue;

    Consumer(Queue queue) {
      this.queue = queue;
    }

    @Override
    public void run() 
{
      try {
        for (int i = 0; i < 10000; i++) {
          doingLongTime();
          queue.takeEle();
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    }

  }

  static class Queue {
    Lock lock = new ReentrantLock();
    Condition prodCond  = lock.newCondition();
    Condition consCond = lock.newCondition();

    final int CAPACITY = 10;
    Object[] container = new Object[CAPACITY];
    int count = 0;
    int putIndex = 0;
    int takeIndex = 0;

    public void putEle(Object ele) throws InterruptedException {
      try {
        lock.lock();
        while (count == CAPACITY) {
          println("隊列已滿:%d,生產者開始睡大覺。。。", count);
          prodCond.await();
        }
        container[putIndex] = ele;
        println("生產元素:%d", ele);
        putIndex++;
        if (putIndex >= CAPACITY) {
          putIndex = 0;
        }
        count++;
        println("通知消費者去消費。。。");
        consCond.signalAll();
      } finally {
        lock.unlock();
      }
    }

    public Object takeEle() throws InterruptedException {
      try {
        lock.lock();
        while (count == 0) {
          println("隊列已空:%d,消費者開始睡大覺。。。", count);
          consCond.await();
        }
        Object ele = container[takeIndex];
        println("消費元素:%d", ele);
        takeIndex++;
        if (takeIndex >= CAPACITY) {
          takeIndex = 0;
        }
        count--;
        println("通知生產者去生產。。。");
        prodCond.signalAll();
        return ele;
      } finally {
        lock.unlock();
      }
    }
  }

 

解說:其實就是對await/signalAll的應用,幾乎面試必問。


源代碼:

https://github.com/coding-new-talking/java-code-demo.git

 

 

(END)

 

作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號和知識星球的二維碼,歡迎關注!

       

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

Python深入淺出property特性屬性

導語

在Java中,通常在類中定義的成員變量為私有變量,在類的實例中不能直接通過對象.屬性直接操作,而是要通過getter和setter來操作私有變量。
而在Python中,因為有property這個概念,所以不需要寫getter和setter一堆重複的代碼來操作私有變量。Python“私有變量”通常在變量前加上“_”或者“__”,例如_attr或者__attr,這是約定俗成的規範。

把私有屬性變成只讀特性

class MyClass:

    def __init__(self, x):
        self._x = x

這裏定義了一個MyClass類,它有一個實例變量_x,綁定了用戶傳來的x值。_x是私有變量,通過obj._x獲取私有變量不符合語言規範,進而我們要使_x變成property(特性),通過obj.x直接訪問。

改造后的代碼如下:

class MyClass:

    def __init__(self, x):
        self._x = x

    @property
    def x(self):
        return self._x
    
>>> obj = MyClass(10)
>>> obj.x
10

我們把_x變成了property特性,以只讀的方式獲取x的值。

我們現在想為x賦值該怎樣做呢?

>>> obj.x = 999
Traceback (most recent call last):
  File "xxx", line 14, in <module>
    obj.x = 23
AttributeError: can't set attribute

可以看到,拋出了AttributeError: can’t set attribute。顯然,只讀方法不支持賦值。

把私有變量變成可賦值的特性

我們只需要在上述代碼改造成:

class MyClass:

    def __init__(self, x):
        self._x = x

    @property
    def x(self):
        return self._x
    
    @x.setter
    def x(self, value):
        self._x = value

>>> obj = MyClass(10)
>>> obj.x = 999
>>> obj.x
999

可以看到,我們為x添加了setter,可以直接為obj.x賦值操作

property屬性能夠遮蓋實例屬性

繼續上面的代碼,我們看看以下的操作:

>>> obj = MyClass(10)
>>> obj.__dict__
{'_x': 999}  #此時實例變量中有_x的值
>>> obj.__dict__['x'] = 99999  #設置obj的實例變量有x值,跟property屬性重名!
>>> obj.__dict__
{'_x': 999, 'x': 99999}  #此時實例變量中有_x和x的值

>>> obj.x     #結果是obj的實例變量還是property屬性?
10

如上代碼所示,obj對象有一個_x實例變量和一個x的property屬性,我們又強行為obj增加了一個x實例變量,這個實例變量x和property屬性x同名!
通過obj.x我們得知,返回的是property屬性,說明property屬性會遮蓋實例屬性!也可以理解為property屬性的優先級更大!

property類解析

我們通常使用內置的@property裝飾器。但其實property是一個類,python中類和函數的調用方式都差不多,他們都是可調用對象
property的構造方法如下:

class property(object):
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        """"""

它最大接受4個參數,都可以為空。
第一個為getter,第二個為setter,第三個為delete函數,第四個為文檔。

上述代碼的另一種寫法

class MyClass:

    def __init__(self, x):
        self._x = x

    def get_x(self):
        return self._x

    def set_x(self, value):
        self._x = value

    x = property(get_x, set_x)

>>> obj = MyClass(10)
>>> obj.x
10

如上,x是property的實例,設置了getter和setter,作為類變量放在MyClass類中。

以上就是property屬性的解析。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

docker (2)—存儲、網絡(利用docker容器上線靜態網站)

一、docker底層依賴的核心技術

1、命名空間 (Namespaces)

2、控制組 (Control Groups)

3、聯合文件系統 (Union File System)

4、Linux 虛擬網絡支持:本地和容器內創建虛擬接口

(1) 命名空間(Namespaces):

實現了容器間資源的隔離,每個容器擁有自己獨立的命名空間 , 運行其中的應用就像是運行在獨立的操作系統中一樣 , 我們都可以看到文件系統,網卡等資源保證了容器之間互不影響,namesaces管理進程號 , 每個進程命名空間有一套自己的進程號管理方法 , 進

程命名空間是一個父子關係的結構 , 子空間中的進程對於父空間是可見的。

(2) 控制組 (Control Groups) :

  控制組 (Control groups)–CGroups 是 Linux 內核的一個特性 ,主要用來對共享資源進行隔離、限制、審計等 。cgroups 允許對於進程或進程組公平( 不公平 ) 的分配 CPU 時間、內存分配和 I/O 帶寬。

容器通過 cgroups 來得到所能夠管理資源的分配和使用。因此容器所獲得資源僅為所有系統資源的一個部分

1、資源限制 : 內存子系統為進程組設置內存使用上限,內存達到上限后再申請內存,就會發出 Out of Memory

2、 優先級 : 通過優先級讓一些組得到更多 CPU 等資源

3、 資源審計 : 用來統計系統上實際把多少資源用到適合的目的上 , 可以使用 cpuacct 子系統記錄某個進程組使用的 CPU 時間

4、 隔離 : 為組隔離名字空間 , 這樣一個組不會看到其他組的進程 .網絡連接和文件系統

5、 控制 : 掛起 . 恢復和啟動等操作

(3)聯合文件系統 (Union File System) :

  docker 中使用AUFS(another Union File System 或 v2 版本以後的Advanced multi-layered Unification File System) 控製為每一個成員目錄設定只讀 / 讀寫 / 寫出權限 , 同時 AUFS 有一個類似分層的概念 , 對只讀權限的分支可以邏輯上進行增量的修改.

二、docker的存儲

docker兩種存儲資源類型:

1、Data Volume (數據卷)

2、Data Volume Dontainers — 數據卷容器

(1) Data Volume (數據卷):

Data Volume 本質上是 Docker Host 文件系統中的目錄或文件,使用類似與 Linux 下對目錄或者文件進行 mount 操作。數據卷可以在容器之間共享和重用,對數據卷的更改會立馬生效,對數據卷的更新不會影響鏡像,卷會一直存在,直到沒有容器使用。

Data Volume(數據卷)的特點:

1、Data Volume 是目錄或文件,而非沒有格式化的磁盤(塊設備)。

2、容器可以讀寫 volume 中的數據。

3、volume 數據可以被永久的保存,即使使用它的容器已經銷毀。

 Data Volume 的使用:

1、在宿主機根目錄下創建一個目錄(數據卷)

 

2、啟動一個容器並將數據卷掛載到容器的目錄下

 

3、驗證  ( 持久化的需要映射目錄)

 

#

 

(2)Data Volume Dontainers — 數據卷容器

數據卷容器就是一個普通的容器,只不過是專門用它提供數據卷供其他容器掛載使用

Data Volume Dontainers使用:

1、創建一個名為 dbdata 的數據卷,並在其中創建一個數據卷掛載到 /dbdata

docker run -dti -v /dbdata --name dbser centos:latest

2、再啟動兩個容器,並使用數據卷容器

docker run -dti --volumes-from dbser --name db1 centos:latest

#

3、驗證

 

#

容器 db1 和 db2 同時掛載了同一個數據卷到本地相同 /dbdata目錄。三個容器任何一個目錄下的寫入,都可以時時同步到另外兩個

三、docker 三種網絡

  docker 網絡從覆蓋範圍可分為單個 host 上的容器網絡和跨多個 host 的網絡,docker 目前提供了映射容器端口到宿主主機和容器互聯機制來為容器提供網絡服務,在啟動容器的時候,如果不指定參數,在容器外部是沒有辦法通過網絡來訪問容器內部的網絡應用和服務的

docker 安裝時會自動在host上創建三個網絡

docker   network  ls   (查看docker  網絡)

(1) docker–none網絡

none 網絡就是什麼都沒有的網絡。掛在這個網絡下的容器除了 lo,沒有其他 任何網卡。容器創建時,可以通過 –network=none 指定使用 none 網絡

 

none網絡的應用

封閉的網絡意味着隔離,一些對安全性要求高並且不需要聯網的應用可以使用 none 網絡。

(2)docker–host網絡

連接到 host 網絡的容器,共享 docker host 的網絡棧,容器的網絡配置與host 完全一樣。可以通過 –network=host 指定使用 host 網絡

 

host 網絡的應用

  直接使用 Docker host 的網絡最大的好處就是性能,如果容器對網絡傳輸效率有較高要求,就可以選擇 host 網絡。當然不便之處就是犧牲一些靈活性,比如要考慮端口衝突問題,Docker host上已經使用的端口就不能再用了。

Docker host 的另一個用途是讓容器可以直接配置 host 網路。比如某些跨host 的網絡解決方案,其本身也是以容器方式運行的,這些方案需要對網絡進行配置,比如管理 iptables

(3) docker–bridge 網絡

docker 安裝時會創建一個 命名為 docker0 的 linux bridge。如果不指定–network,創建的容器默認都會掛到 docker0 上

 

#

 

eth0@if29      與 veth04c5851 是一對 veth pair

#

  veth pair 是一種成對出現的特殊網絡設備,可以把它們想象成由一根虛擬網線連接起來的一對網卡,網卡的一頭(eth0@if29)在容器中,另一頭( veth04c5851)掛在網橋 docker0 上,其效果就是將 eth0@if29也掛在了docker0 上。

# 查看網絡配置信息 ( 設置容器ip 網段、網關)

docker network inspect bridge

 

#

注:容器創建時,docker 會自動從 172.17.0.0/16 中分配一個 IP,這裏 16 位的掩碼保證有足夠多的 IP 可以供容器。

四、創建 user-defined網絡 (自定義網絡)

通過 bridge 驅動創建類似前面默認的 bridge 網絡

1、利用bridge驅動創建名為my-net2網橋(docker會自動分配網段)

docker network create --driver bridge my-net2

# 查看網絡配置信息

 

# 查看網橋

 

2、利用bridge驅動創建名為my-net3網橋(user-defined (自定義)網段及網關)

docker network create --driver bridge --subnet 172.33.1.0/24 --gateway 172.33.1.1 my-net3

# 查看網絡配置信息

 

# 查看網橋

 

3、啟動容器使用新建的my-net3網絡

docker run -it  --network=my-net3  busybox:latest

4、啟動容器使用my-net3網絡並指定ip(只有使用 –subnet 創建的網絡才能指定靜態 IP,如果是docker自動分配的網段不可以指定ip)

docker run -it --network=my-net3  --ip 172.33.1.100  busybox:latest

 

5、讓已啟動不同vlan的busybox容器,可以連接到my-net2(其實在busybox中新建了my-net2的網卡)(添加網卡。訪問不同的網段)

 

# #docker network connect my-net3  08493ae30117   ( 連接)

6、使用–name指定啟動容器名字,可以使用docker自帶DNS通信,但只能工作在user-defined 網絡,默認的 bridge 網絡是無法使用 DNS 的。

#docker run -it --network=my-net3 --name=bbox1 busybox:latest

#docker run -it --network=my-net3 --name=bbox2 busybox:latest

7、容器之間的網絡互聯

&1、創建一個 db 容器

docker run -dti --name db centos:latest

&2、創建一個 web 容器,並使其連接到 容器db

docker run -dti --name web --link db:dblink centos:latest /bin/bash

–link db:dblink 實際是連接對端的名字和這個鏈接的名字,也就是和 db 容器建立一個叫做 dblink 的鏈接

 

# 測試  

 

注:此鏈接通信是單向的

8、容器端口映射

在啟動容器的時候,如果不指定參數,在容器外部是沒有辦法通過網絡來訪問容器內部的網絡應用和服務的,當容器需要通信時,我們可以使用 -P (大) &&-p (小)來指定端口映射

(1)   -P : Docker 會隨機映射一個 49000 ~ 49900 的端口到容器內部開放的網絡端口

(2)   -p :則可以指定要映射的端口,並且在一個指定的端口上只可以綁定一個容器。

支持的格式

 IP : HostPort : ContainerPort

 IP : : ContainerPort

 IP : HostPort :

&1、 查看映射

docker port   CONTAINER ID/NAMES

&2、映射所有接口地址,此時綁定本地所有接口上的 5200 到容器的 5200 接口,訪問任何一個本地接口的 5000 ,都會直接訪問到容器內部

docker run -dti -p 5200:5200 centos:latest  /bin/bash

&3、多次使用可以實現多個接口的映射

docker run -dti -p 5400:5400  -p 5300:5300 centos:latest  /bin/bash

&4、映射到指定地址的指定接口

此時會綁定本地 192.168.226.147 接口上的 5100 到容器的 5100 接口

docker run -dti -p 192.168.226.147:5100:5100 centos:latest /bin/bash

 

&5、映射到指定地址的任意接口

此時會綁定本地 192.168.226.147 接口上的任意一個接口到容器的 5500 接口

docker run -dti -p 192.168.226.147::5500 centos:latest /bin/bash

實驗、通過端口映射實現訪問本地的 IP:PORT 可以訪問到容器內的 web

1、將容器80端口映射到主機8090端口

docker run -itd -p 192.168.226.147:8090:80 --name http-test httpd:latest

2、查看剛運行docker容器

docker  ps

 

3、進入 容器

 

4、容器內部編輯網頁文件 index.html

 

5、到宿主機上打開瀏覽器輸入 IP:PORT 訪問驗證

http://192.168.226.147:8090/

 

6、宿主機上傳靜態網站測試文件

 

7、解壓

 

8、把解壓的目錄上傳至容器下的網站根目錄

docker cp jd 67b3daf15a40:/usr/local/apache2/htdocs

9、進入容器,刪除原來的index.html 文件

 

10、展開目錄

 

11、web 訪問

http://192.168.226.147:8090/

 

 

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

Git的使用 — 用git玩翻github,結尾有驚喜!有驚喜!有驚喜!林妙妙看了說:牛呲呼啦帶閃電 (三)(超詳解)

簡介

      上一篇主要講解的是Git安裝及配置,這一篇就詳細的從無到有的來用Git玩翻github。

一、什麼是Github

Github是全球最大的社交編程及代碼託管網站(https://github.com/)。

Github可以託管各種git庫,並提供一個web界面(用戶名.github.io/倉庫名)

二、Github和Git是什麼關係 

Git是版本控制軟件

Github是項目代碼託管的平台,藉助git來管理項目代碼

1、 使用Github

目的:藉助github託管項目代碼

2、基本概念

a、倉庫(Repository)

 倉庫的意思,即你的項目,你想在 GitHub 上開源一個項目,那就必須要新建一個 Repository ,如果你開源的項目多了,你就擁有了多個 Repositories 。

 倉庫用來存放項目代碼,每個項目對應一個倉庫,多個開源項目則有多個倉庫。

b、收藏(Star)

倉庫主頁star按鈕,意思為收藏項目的人數,收藏項目,方便下次查看,在 GitHub 上如果你有一個項目獲得100個star都算很不容易了!

【如何收藏】

 操作:打開對應項目主頁,點擊右上角  star 按鈕即可收藏

 情景:張三無意訪問到李四的開源項目感覺不錯並進行收藏

 

【如何查看自己得收藏】

 

c、複製項目(Fork)派生

這個不好翻譯,如果實在要翻譯我把他翻譯成分叉,什麼意思呢?你開源了一個項目,別人想在你這個項目的基礎上做些改進,然後應用到自己的項目中,這個時候他就可以 Fork 你的項目(打開項目主頁點擊右上角的fork按鈕即可),然後他的 GitHub 主頁上就多了一個項目,只不過這個項目是基於你的項目基礎(本質上是在原有項目的基礎上新建了一個分支),他就可以隨心所欲的去改進,但是絲毫不會影響原有項目的代碼與結構。

注意:該fork的項目時獨立存在的

比如:張三fork了李四的項目,相當於張三複制了李四的項目,所以自己也單獨有了一個一樣名稱的倉庫(注:該倉庫會聲明來自於李四,但是獨立存在)

d、發起請求(Pull Request)

發起請求,這個其實是基於 Fork 的,還是上面那個例子,如果別人在你基礎上做了改進,後來覺得改進的很不錯,應該要把這些改進讓更多的人收益,於是就想把自己的改進合併到原有項目里,這個時候他就可以發起一個 Pull Request(簡稱PR) ,原有項目創建人,也就是你,就可以收到這個請求,這個時候你會仔細review他的代碼,並且測試覺得OK了,就會接受他的PR,這個時候他做的改進原有項目就會擁有了。

e、關注(Watch)

這個也好理解就是觀察,如果你 Watch 了某個項目,那麼以後只要這個項目有任何更新,你都會第一時間收到關於這個項目的通知提醒。

f、問題(Issue)

發現代碼BUG,但是目前沒有成型代碼,需要討論時用; 問題的意思,舉個例子,就是你開源了一個項目,別人發現你的項目中有bug,或者哪些地方做的不夠好,他就可以給你提個 Issue ,即問題,提的問題多了,也就是 Issues ,然後你看到了這些問題就可以去逐個修復,修復ok了就可以一個個的 Close 掉。

g、Github主頁

賬號創建成功或點擊網址導航欄github圖標都可進入github主頁:該頁左側主要显示用戶動態以及關注用戶或關注倉庫的動態;右側显示所有的git庫

h、倉庫主頁

倉庫主頁主要显示項目的信息,如:項目代碼,版本,收藏/關注/fork情況等

i、個人主頁

個人信息:頭像,個人簡介,關注我的人,我關注的人,我關注的git庫,我的開源項目,我貢獻的開源項目等信息

3、註冊github賬號

官方網址:https://github.com

注意:

a、因為github在國外服務器所以訪問較慢或者無法訪問,需要FQ(***)

b、私有倉庫只能自己或者指定的朋友才有權限操作(私有倉庫是收費的)

c、新註冊的用戶必須驗證郵箱后才可以創建git庫倉庫

4、創建倉庫/創建新項目

說明:一個git庫(倉庫)對應一個開源項目。通過git管理git庫

a、創建倉庫

1)點擊【Start a project】創建一個倉庫

2)問題:點擊【Start a project】創建一個倉庫,后出現該頁面

2)原因:未驗證郵箱,點擊下圖框框中的鏈接進行驗證

 

3)點擊【resend】發送郵件驗證郵箱

 

 4)點擊【verify email address】驗證郵箱

   說明:驗證成功後會自動跳轉github主頁,重新點擊【Start a project】即可創建倉庫

 

5) 驗證郵箱后,點擊【Start a project】進入下圖界面

b、倉庫主頁說明

 

 

 注意:qq郵箱需要設置白名單才可以收到郵件

設置QQ郵箱白名單

1、打開QQ郵箱、點擊【設置】

2、點擊【反垃圾】

3、點擊【設置域名白名單】

4、在新頁面的input框中輸入【github.com】添加即可

5、倉庫管理

a、新建文件:倉庫主頁,點擊【create new file】創建倉庫文件

 

 

 

   

b、編輯文件:倉庫主頁,點擊【需要修改的文件】進入文件詳情頁

 

    

c、刪除文件

 

d、被刪除文件如何查看信息

答案:點擊commits按鈕查看

e、上傳文件

 

f、搜索倉庫文件:快捷鍵(t)

6、下載/檢出項目

7、Github Issues

作用:發現代碼BUG,但是目前沒有成型代碼,需要討論時用;或者使用開源項目出現問題時使用

情景:張三發現李四開源git庫,則發提交了一個issue;李四隔天登錄在github主頁看到通知並和張三交流,最後關閉issue

三、基本概念(實戰操作)

1、Github主頁

 

2、個人主頁

 

四、開源項目貢獻流程

1、新建Issue

提交使用問題或者建議或者想法

2、Pull Request

步驟:

a、 fork項目

b、 修改自己倉庫的項目代碼

c、 新建 pull request

d、 等待作者操作審核

五、下面就是驚喜:Github  Pages搭建網站

1、個人站點

訪問:https://用戶名.github.io

搭建步驟:

a、創建個人站點-》新建倉庫(倉庫名必須是【用戶名.github.io】)

 

b、在倉庫下新建index.html的文件即可

注意:a、Github Page僅支持靜態頁面

   b、倉庫裏面只能是.html文件

   c、個人主頁也可以設置主題

2、Project Pages 項目站點

訪問:https://用戶名.github.io/倉庫名

原理:gh-pages 用於構建和發布

搭建步驟

a、進入項目主頁,點擊settings

b、在settings頁面,點擊【Choose a theme 】來自動生成主題頁面

c、新建站點基礎信息設置

d、選擇主題

e、發布網頁(publish page)

六、小結

Clone和Fork的區別:

fork(派生):將別人的倉庫複製一份到自己的倉庫。
clone(克隆):將倉庫克隆到自己本地電腦中。

Fork的主要應用場景:
1.在A的倉庫中fork項目B (此時我們自己的github就有一個一模一樣的倉庫B,但是URL不同)
2.將我們修改的代碼push到自己github中的倉庫B中
3.pull request ,主人就會收到請求,並決定要不要接受你的代碼

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!