Swift中Runtime簡(jiǎn)單了解以及項(xiàng)目中的運(yùn)用

最近沒(méi)什么任務(wù),想了解一下Swift中Runtime的一些知識(shí),所以網(wǎng)上找了不少相關(guān)文章看了一下,算是了解了一些Swift中Runtime的知識(shí),但是底層其實(shí)還是不太懂,只是了解一些Swift中Runtime的使用,所以想寫(xiě)一點(diǎn)東西,算是自己學(xué)習(xí)的總結(jié)。

Runtime能做什么呢?我自己總結(jié)了一下,主要功能有:

(一):網(wǎng)絡(luò)請(qǐng)求解析數(shù)據(jù)轉(zhuǎn)Model;自定義類型的歸檔,解檔;某些情況KVC之前的check保護(hù)。這塊主要用到的是獲取目標(biāo)類的屬性列表,然后通過(guò)屬性名進(jìn)行判斷,KVC等操作。
核心代碼是:

//獲取目標(biāo)類所有屬性
 let propertys = class_copyPropertyList(object, &count)

(二):可以在運(yùn)行時(shí),給目標(biāo)類添加一個(gè)方法。
核心代碼:

 let _ = class_addMethod(object_getClass(p),  #selector(ViewController.readBook), class_getMethodImplementation(object_getClass(self), #selector(ViewController.readBook)), method_getTypeEncoding(method))

(三):在一個(gè)類的extension里面,給其添加屬性。
核心代碼是:

var bookName: String? {
        set {
            objc_setAssociatedObject(self, ViewController.bookName, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
        get {
            return  objc_getAssociatedObject(self, ViewController.bookName) as? String
        }
    }

(四):在一個(gè)類的extension里面,對(duì)方法實(shí)現(xiàn)進(jìn)行替換。
核心代碼是:

let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod{
     class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
}else{   
     method_exchangeImplementations(originalMethod, swizzledMethod)
     }
第一部分:獲取目標(biāo)類的屬性,變量,方法,遵循代理列表

首先我創(chuàng)建了一個(gè)帶有一些屬性的People類,寫(xiě)了一些屬性,方法,定義了一個(gè)代理。

import UIKit

class People:NSObject,UITableViewDelegate {
    var name:String = ""
    var sex:String = ""
    var age:Int = 0
    var height:Float = 0.0
    var job:String = ""
    var native:String = ""
    var education:String = ""
     override init() {
        super.init()
    }
    var delegate:PeopleDelegate?
    func eat(){
        delegate?.peopleDelegateToWork()
    }
    func sleep(){
        
    }
    func work(){
        
    }
    
}
protocol PeopleDelegate {
    func peopleDelegateToWork()
}
(1)獲取目標(biāo)類的變量列表:

蘋(píng)果提供的方法是:

public func class_copyIvarList(_ cls: Swift.AnyClass!, _ outCount: UnsafeMutablePointer<UInt32>!) -> UnsafeMutablePointer<Ivar?>!

所以獲取變量名的代碼是:

        var count:UInt32 = 0
        let  ivars = class_copyIvarList(People.self, &count)
        for i in 0   ..< Int(count)  {
            let ivar = ivars?[i]
            if ivar != nil{
                let tempName = String(cString: ivar_getName(ivar!))
                print("變量名:\(tempName)")
            }
        }
        free(ivars)

控制臺(tái)輸出:

變量名.png

Ivar是objc_ivar的指針,包含變量名,變量類型等成員。
關(guān)于Ivar詳解看這里:Ivar詳解

(2)獲取目標(biāo)類的屬性列表:

蘋(píng)果提供的方法是:

public func class_copyPropertyList(_ cls: Swift.AnyClass!, _ outCount: UnsafeMutablePointer<UInt32>!) -> UnsafeMutablePointer<objc_property_t?>!

在OC中一個(gè)類有變量和屬性的區(qū)別,但是在swift中并不會(huì)有這種區(qū)分,所以其實(shí)class_copyIvarList和class_copyPropertyList我實(shí)驗(yàn)了一下,獲取到的東西基本一致,只是在class_copyIvarList里面會(huì)額外拿到自定義代理的Deleagte對(duì)象,其他還沒(méi)發(fā)現(xiàn)額外的區(qū)別。

獲取目標(biāo)類屬性名代碼是:

        var count:UInt32 = 0
        let properties = class_copyPropertyList(People.self, &count)
        for i in 0 ..< Int(count){
            let property = properties?[i];
            if property != nil{
                let propertyName = String(cString: property_getName(property!))
                print("屬性名:\(propertyName)")
            }
        }
        free(properties)

控制臺(tái)輸出:


屬性名.png

可以發(fā)現(xiàn)屬性列表名的輸出,就少了一個(gè)delegate,這個(gè)對(duì)象是我在People里面寫(xiě)的自定義代理對(duì)象。

(3)獲取目標(biāo)類的方法列表

蘋(píng)果提供的方法是:

public func class_copyMethodList(_ cls: Swift.AnyClass!, _ outCount: UnsafeMutablePointer<UInt32>!) -> UnsafeMutablePointer<Method?>!

所以獲取目標(biāo)類方法列表代碼:

        var count:UInt32 = 0
        let funcs = class_copyMethodList(People.self, &count)
        for i in 0 ..< Int(count){
            let sel = sel_getName(method_getName(funcs?[i]))
            let name = String.init(validatingUTF8: sel!)
            let argument = method_getNumberOfArguments(funcs?[i])
            print("方法名:\(name!)"+"參數(shù)個(gè)數(shù):\(Int(argument))" )
        }

控制臺(tái)輸出:


方法名.png
(4)獲取目標(biāo)類遵循代理列表

蘋(píng)果提供的方法是

public func class_copyProtocolList(_ cls: Swift.AnyClass!, _ outCount: UnsafeMutablePointer<UInt32>!) -> AutoreleasingUnsafeMutablePointer<Protocol?>!

獲取目標(biāo)類遵循代理主要代碼是:

        var count:UInt32 = 0
        let protocolArray = class_copyProtocolList(People.self, &count)
        for index in 0 ..< Int(count){
            let protocolTemp = protocol_getName(protocolArray?[index])
            let name = String.init(validatingUTF8: protocolTemp!)
            print("遵循的協(xié)議:\(name!)")
        }

測(cè)試代碼:上面代碼在這里

第二部分:通過(guò)Runtime給目標(biāo)類添加屬性,添加方法,替換方法的實(shí)現(xiàn)
(1)給目標(biāo)類添加屬性

主要方法是:

public func objc_setAssociatedObject(_ object: Any!, _ key: UnsafeRawPointer!, _ value: Any!, _ policy: objc_AssociationPolicy)

第一個(gè)參數(shù):目標(biāo)類對(duì)象
第二個(gè)參數(shù):所要添加的屬性名
第三個(gè)參數(shù):所要添加屬性的值
第四個(gè)參數(shù):采用的協(xié)議 objc_AssociationPolicy類型的值
首先我寫(xiě)了一個(gè)空的Person類,除了聲明它是個(gè)類,就沒(méi)進(jìn)行其他操作了

class Person : NSObject{
    
}

在extension里面添加的代碼是:

extension Person{
    var bookName: String? {
        set {
            objc_setAssociatedObject(self, ViewController.bookName, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
        get {
            return  objc_getAssociatedObject(self, ViewController.bookName) as? String
        }
    }
}

這樣我就添加了一個(gè)名為bookName的屬性。我在其他地方調(diào)用的時(shí)候可以直接進(jìn)行賦值和取值

        let onePerson = Person()
        onePerson.bookName = "鄧小平傳"
        print(onePerson.bookName!)

運(yùn)行后控制臺(tái)的輸出:


添加屬性.png
(2)給目標(biāo)類添加方法

還是上面那個(gè)空Person類,給Person添加方法的主要代碼是:
首先我在一個(gè)第三方類Viewcontroller里面寫(xiě)了一個(gè)readBook方法,里面進(jìn)行一句輸出

    func readBook(){
        print("I LOVE ReadBook")
    }

然后把這個(gè)readBook的實(shí)現(xiàn)添加到Person類里面

let onePerson = Person()
//這句話是我寫(xiě)的一個(gè)工具類,簡(jiǎn)單的輸出類存在的屬性,方法等
Swift_RunTimeTool.getMethodList(object: Person.self)

 //下面連個(gè)語(yǔ)句都是添加方法,第一個(gè)語(yǔ)句是添加能直接取到Sel的方法,后面的參數(shù)以及返回值是通過(guò)現(xiàn)有方法取出
let _ = class_addMethod(object_getClass(onePerson),  #selector(ViewController.readBook), class_getMethodImplementation(object_getClass(self), #selector(ViewController.readBook)), method_getTypeEncoding(method))
//這個(gè)語(yǔ)句是添加取不到Sel的方法,添加一個(gè)目前不存在的方法名的方法,后面方法參數(shù)返回值是直接字符串給出
let _ = class_addMethod(object_getClass(onePerson),  Selector(("findBook")), class_getMethodImplementation(object_getClass(self), #selector(ViewController.readBook)), "v@:")

Swift_RunTimeTool.getMethodList(object: Person.self)
//添加后的方法的調(diào)用只能是通過(guò)performSelector
onePerson.perform(Selector(("findBook")))

控制臺(tái)輸出:

添加方法.png

也許你會(huì)有一些疑問(wèn),你添加的方法明明沒(méi)有參數(shù),為什么輸出是兩個(gè)參數(shù),這兩個(gè)參數(shù)的type我看了一下一個(gè)是“@”,一個(gè)是“:”,至于為什么默認(rèn)帶這兩個(gè)參數(shù),我也不知道。。
關(guān)于performSelector詳解看這里:performSelector的詳解
測(cè)試代碼:添加屬性已經(jīng)添加方法代碼在這里

(3)替換一個(gè)類的方法的實(shí)現(xiàn)

交換兩個(gè)方法的IMP,這種方式也叫作Method Swizzling,Method Swizzling是iOS中AOP(面相切面編程)的一種實(shí)現(xiàn)方式。
方法Method的定義是:


Method的定義.png

Method Swizzling簡(jiǎn)略的過(guò)程就是如下面兩張圖:

Method Swizzling 1.png
Method Swizzling 2.png

其實(shí)就是把方法內(nèi)存放的實(shí)現(xiàn)地址給交換存儲(chǔ)了一下。
進(jìn)行交換的代碼是:

public func method_exchangeImplementations(_ m1: Method!, _ m2: Method!)

所以對(duì)于兩個(gè)方法具體的交換代碼是:

    /// 交換一個(gè)類的兩個(gè)方法實(shí)現(xiàn)
    ///
    /// - Parameters:
    ///   - cls: 目標(biāo)類
    ///   - originalSelector:被交換的方法
    ///   - swizzeSelector: 交換的方法
   class func methodSwizze(cls : AnyClass,originalSelector : Selector , swizzeSelector : Selector)  {
        let originalMethod = class_getInstanceMethod(cls, originalSelector)
        let swizzeMethod = class_getInstanceMethod(cls, swizzeSelector)
        
        let didAddMethod = class_addMethod(cls, originalSelector, method_getImplementation(swizzeMethod), method_getTypeEncoding(swizzeMethod))
        
        if didAddMethod {
            class_replaceMethod(cls, swizzeSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
        }else {
            method_exchangeImplementations(originalMethod, swizzeMethod)
        }
    }

這個(gè)方法被我寫(xiě)在了一個(gè)Swift_RunTimeTool.swift文件里面,自后會(huì)通過(guò)這個(gè)類直接去調(diào)用交換方法。
接下來(lái)我把UIViewController的ViewDidAppear方法和我自己寫(xiě)的一個(gè)jyViewDidAppear的方法交換一下,等于在ViewDidAppear加入一些自己的東西。


import Foundation
import UIKit
extension UIViewController{
    
    override open class func initialize(){
        super.initialize()
//在swift3.0中GCD的once方法被蘋(píng)果刪除了,所以我對(duì)DispatchQueue添加了一個(gè)once方法
        DispatchQueue.once(token: "com.besttone.jySwizzeMethod") {
        let primaryAppearSel = #selector(UIViewController.viewDidAppear(_:))
        let replaceAppearSel = #selector(UIViewController.jyViewDidAppear(_:))
        SwiftRunTimeTool.methodSwizze(cls: UIViewController.self, originalSelector: primaryAppearSel, swizzeSelector: replaceAppearSel)
    }
   func jyViewDidAppear(_ animation : Bool){
    //這樣調(diào)用不會(huì)導(dǎo)致遞歸,運(yùn)行后self.jyViewDidAppear(animation)真正調(diào)用的是ViewDidAppear方法,因?yàn)橄到y(tǒng)的方法里面會(huì)進(jìn)行一些必要的操作,不能忽略不調(diào)用。
          self.jyViewDidAppear(animation)
          print("jyViewDidAppear")
  }
   public extension DispatchQueue {
     private static var _onceTracker = [String]()
     public class func once(token: String, block:()->Void) {
        objc_sync_enter(self)
        defer { 
           objc_sync_exit(self) }
        
        if _onceTracker.contains(token) {
            return
        }
        
        _onceTracker.append(token)
        block()
    }
}

這樣代碼寫(xiě)過(guò)之后,當(dāng)VC里面的View出現(xiàn)之后,會(huì)調(diào)用jyViewDidAppear方法。
控制臺(tái)輸出:


jyViewDidAppear.png

Method Swizzling詳解看這里:iOS黑魔法-Method Swizzling

這篇文章介紹了一個(gè)挺有用的通過(guò)Runtime交換方法防止NSArray取值越界,但是在swift中Array變成結(jié)構(gòu)體了,不再是OC中的繼承NSObject類,所以O(shè)C中通過(guò)替換NSArray的objectAtIndex方法的行為就不可行了。

第三部分:Runtime在項(xiàng)目中的運(yùn)用

下面是我覺(jué)得Runtime在項(xiàng)目里面最有用的兩種運(yùn)用方式,可以更好更優(yōu)雅的實(shí)現(xiàn)項(xiàng)目中某些功能

(1)通過(guò)Runtime解耦合統(tǒng)計(jì)埋點(diǎn)

這個(gè)功能的實(shí)現(xiàn)簡(jiǎn)單點(diǎn)講就是通過(guò)替換UIViewController和UIControl中的系統(tǒng)方法,在系統(tǒng)調(diào)用這些方法時(shí),我們加入自己的統(tǒng)計(jì)代碼。

extension UIViewController{
    
    override open class func initialize(){
        super.initialize()
        DispatchQueue.once(token: "com.besttone.jyBesttone") {
            //交換viewDidAppear和viewWillDisappear方法,統(tǒng)計(jì)每個(gè)頁(yè)面進(jìn)入和離開(kāi),然后再通過(guò)plist文件直接取對(duì)應(yīng)的統(tǒng)計(jì)ID
            let primaryAppearSel = #selector(UIViewController.viewDidAppear(_:))
            let replaceAppearSel = #selector(UIViewController.jyViewDidAppear(_:))
            
            SwiftRunTimeTool.methodSwizze(cls: UIViewController.self, originalSelector: primaryAppearSel, swizzeSelector: replaceAppearSel)
            
            let primaryDisapperaSel = #selector(UIViewController.viewWillDisappear(_:))
            let replaceDisapperaSel = #selector(UIViewController.jyViewWillDisAppear(_:))
            
            SwiftRunTimeTool.methodSwizze(cls: UIViewController.self, originalSelector: primaryDisapperaSel, swizzeSelector: replaceDisapperaSel)
        }
    }
    
    func jyViewDidAppear(_ animation : Bool){
        self.jyViewDidAppear(animation)
          //從本地plist文件獲取一個(gè)頁(yè)面VC的統(tǒng)計(jì)ID,然后在這個(gè)方法里面進(jìn)行統(tǒng)計(jì)請(qǐng)求的發(fā)起
        if VCIDTool.getVCID(className: object_getClass(self), isEnter: true) != ""{
            print(VCIDTool.getVCID(className: object_getClass(self), isEnter: true))
        }
    }
    func jyViewWillDisAppear(_ animation : Bool){
        self.jyViewWillDisAppear(animation)
        if VCIDTool.getVCID(className: object_getClass(self), isEnter: false) != ""{
            print(VCIDTool.getVCID(className: object_getClass(self), isEnter: false))
        }
    }
}
class VCIDTool : NSObject{
    //從Plist文件取VC統(tǒng)計(jì)ID
    class func getVCID(className : AnyClass , isEnter : Bool) -> String{
        let filePath = Bundle.main.path(forResource: "ViewControllerIDList", ofType: "plist") ?? ""
        let vcDic : NSDictionary = NSDictionary(contentsOfFile: filePath) ?? NSDictionary()
        let vcName = "\(className)"
        if let vcSomeDic = vcDic[vcName] {
            return ((vcSomeDic as! NSDictionary)["PageEnentIDs"] as! NSDictionary)[isEnter ? "Enter" : "Leave"] as! String
        }else{
            return ""
        }
      }
   }
public extension DispatchQueue {
    
    private static var _onceTracker = [String]()
    public class func once(token: String, block:()->Void) {
        objc_sync_enter(self)
        defer { objc_sync_exit(self) }
        
        if _onceTracker.contains(token) {
            return
        }
        
        _onceTracker.append(token)
        block()
    }
}

這樣在每個(gè)VC進(jìn)入和離開(kāi)會(huì)調(diào)用到,我自己寫(xiě)的jyViewDidAppear和jyViewWillDisAppear,就可以進(jìn)行統(tǒng)計(jì)操作。其實(shí)這個(gè)方法是看別人的文章獲得的,感覺(jué)這種寫(xiě)法真的挺不錯(cuò),文章還寫(xiě)道了替換Control里面的事件方法,獲取Control子類,不同UIbutton的點(diǎn)擊事件,可以根據(jù)Button所在的文件和點(diǎn)擊綁定的事件來(lái)進(jìn)行區(qū)分,然后同樣的也是在事先設(shè)置好的plist文件去獲取到Control的統(tǒng)計(jì)ID。我是參考別人文章的思路,他的文章比我說(shuō)的細(xì)。
我參考的文章在這里:可復(fù)用而且高度解耦的用戶統(tǒng)計(jì)埋點(diǎn)實(shí)現(xiàn)
我最終寫(xiě)出來(lái)的代碼:代碼在這里

(2)push從任何一個(gè)界面跳轉(zhuǎn)任何一個(gè)界面

這個(gè)東西個(gè)人感覺(jué)還是很有用的,在和后臺(tái)對(duì)接好后,對(duì)于推送跳轉(zhuǎn)的寫(xiě)法就會(huì)方便很多。這塊最重要的東西其實(shí)不是Runtime的東西,Runtime更多的是做一個(gè)KVC之前的保護(hù),防止設(shè)置空key導(dǎo)致崩潰。
首先在AppDelegate里面寫(xiě)一個(gè)處理push信息的方法

    func conductPushParams(params : [String : AnyObject]){
        guard let className = params["class"] as? String else{
            print("參數(shù)類名不存在")
            return
        }
        //讀取本地Storyboard綁定VC的ID的plist文件
        let filePath = Bundle.main.path(forResource: "VCStoryboardID", ofType: "plist") ?? ""
        let vcDic : NSDictionary = NSDictionary(contentsOfFile: filePath) ?? NSDictionary()
        //判斷plist文件里面是否含有目標(biāo)VC的Storyboard ID
        if let vcID = vcDic[className] {
            //如果VC是與storyboard關(guān)聯(lián)的通過(guò)下面的語(yǔ)句創(chuàng)建VC對(duì)象,通過(guò)Runtime檢查VC對(duì)象是否含有對(duì)應(yīng)屬性,如果包含通過(guò)KVC給屬性賦值
            let clsVC = UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier:  vcID as! String)
            for key in (params["property"] as! [String : String]).keys{
                if SwiftRunTimeTool.checkClassPerporty(object: object_getClass(clsVC), propertyStr: key){
                    clsVC.setValue((params["property"] as! [String : String])[key]! as String, forKey: key)
                }
            }
            //如果App是Tabbar + Navigation + VC結(jié)構(gòu)的 如果不是這個(gè)結(jié)構(gòu)下面這句as! UITabBarController強(qiáng)轉(zhuǎn)你就會(huì)崩潰
            let tabVC = self.window?.rootViewController as! UITabBarController
            let navVC = tabVC.viewControllers?[tabVC.selectedIndex] as! UINavigationController
            clsVC.hidesBottomBarWhenPushed = true
            navVC.pushViewController(clsVC, animated: true)
        }else{
            //如果VC不與Storyboard關(guān)聯(lián),則通過(guò)Push傳過(guò)來(lái)的目標(biāo)VC名字創(chuàng)建VC對(duì)象,通過(guò)Runtime檢查VC對(duì)象是否含有對(duì)應(yīng)屬性,如果包含通過(guò)KVC給屬性賦值
            guard let clsName = Bundle.main.infoDictionary!["CFBundleExecutable"] else {
                print("命名空間不存在")
                return
            }
            //swift根據(jù)VC名字字符串創(chuàng)建相應(yīng)VC的對(duì)象,首先獲取到命名空間,然后NSClassFromString(命名空間.VC名字字符串),就能獲取到相應(yīng)的VC,class
            let classTemp : AnyClass? = NSClassFromString((clsName as! String) + "." + className)
            guard let clsType = classTemp as? UIViewController.Type else{
                print("無(wú)法轉(zhuǎn)換為UIViewController類型")
                return
            }
            let clsVC = clsType.init()
            let tabVC = self.window?.rootViewController as! UITabBarController
            let navVC = tabVC.viewControllers?[tabVC.selectedIndex] as! UINavigationController
            for key in (params["property"] as! [String : String]).keys{
                if SwiftRunTimeTool.checkClassPerporty(object: object_getClass(clsVC), propertyStr: key){
                    clsVC.setValue((params["property"] as! [String : String])[key]! as String, forKey: key)
                }
            }
            clsVC.hidesBottomBarWhenPushed = true
            navVC.pushViewController(clsVC, animated: true)
        }
    }



   class SwiftRunTimeTool: NSObject {
       class func checkClassPerporty(object : AnyClass , propertyStr: String) -> Bool{
          var count : UInt32 = 0
          let propertys = class_copyPropertyList(object, &count)
          for index in 0 ..< Int(count){
            let name = property_getName(propertys?[index])
            if let propertyName = String.init(validatingUTF8: name!){
                if propertyName == propertyStr{
                    return true
                }
            }
        }
        return false
    }
}

然后繼續(xù)在AppDelete寫(xiě)一個(gè)模擬推送信息的方法

func setPushParams() ->[String : AnyObject]{
        let params = ["class" : "TestViewController" , "property" : ["sourceText" : "推送跳轉(zhuǎn)過(guò)來(lái)"] ] as [String : Any]
        return params as [String : AnyObject]
    }

class:是你要跳轉(zhuǎn)的目的VC的名字,需要我們和后臺(tái)進(jìn)行對(duì)接好,不能隨便推,如果覺(jué)得不好維護(hù),也可以規(guī)定好ID,然后項(xiàng)目里面設(shè)置plist文件,從plist文件里面獲取到VC的名字字符串。
property:推送帶過(guò)來(lái)的參數(shù)信息

然后我們?cè)赿idFinishLaunchingWithOptions方法里面延時(shí)調(diào)用一下,模擬推送。

 func application(_ application: UIApplication, didFinishLaunchingWithOptions 
     DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) { 
            self.conductPushParams(params: self.setPushParams())
        }
}

這些代碼都是一個(gè)實(shí)驗(yàn)的東西,在實(shí)際項(xiàng)目里面不可能只傳一個(gè)字符串,肯定會(huì)帶有不少信息,這樣就需要傳一個(gè)model了,我們可以設(shè)置一個(gè)pushModel的東西,里面寫(xiě)上需要推送跳轉(zhuǎn)過(guò)去的VC的dataModel作為屬性,然后根據(jù)dataModel的具體名字創(chuàng)建對(duì)象,根據(jù)推送過(guò)來(lái)的key和值,kvc設(shè)置dataModel值,然后將dataModel通過(guò)kvc賦值給pushModel的相應(yīng)屬性,最后將pushModel的通過(guò)kvc賦值給需要跳轉(zhuǎn)的VC。
這個(gè)思路的代碼,接下來(lái)我會(huì)重新寫(xiě)一篇文章仔細(xì)介紹一下,就不在這里展開(kāi)了。
關(guān)于跳轉(zhuǎn)的改進(jìn)版文章:改進(jìn)版
前后一共寫(xiě)了兩天半,算是給自己前段時(shí)間學(xué)習(xí)的一個(gè)交代了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容