Skip to content

Latest commit

 

History

History
executable file
·
291 lines (204 loc) · 16.9 KB

Лекция-09.md

File metadata and controls

executable file
·
291 lines (204 loc) · 16.9 KB

Лекция №8 (11.04.2019)

--

В тази лекция ще разгледаме вътрешната организация на паметта, когато пишем нашата програма, за да можем да спазваме законите, определени от Swift. Така приложенията ще могат да пестят системни ресурси и да работят по-бързо. Също така ще обърнем внимание на шабноните (функции, класове, структури и изброими типове) в Swift.

Кога можем да говорим за паметта?

Всяка програма използва памет. Образно можем да си мислим, че това е работната площадка на компютъра (процесора му) и той чете данни от там и ги записва там. Ние като потребители на тази умна система трябва да спазваме правилата, за да може да използваме всички ресурси, които тя ни предоставя. Ако разхищаваме и не следваме правилата, вероятността да стигнем до непредсказуемо състояние е голяма.

Променливите, които ползваме в нашите програми до сега, се пазят в паметта. В зависимост вида им, те се съхраняват в различни части от паметта. Стойностните типове се записват в програмния стек - това е памет, която се управлява от Swift автоматично. Ние нямаме контрол над нея. Приемаме, че Swift се грижи вместо нас. По-интересни са референтните типове, които се записват в хийп-а. Това е друга памет, която се използва за съхранение на големи обеми от данни. Можем да си мислим, че е неизчерпаема, понеже операционната система има механизми, чрез които може да я разширява, но все пак е с крайно голям обем.

Важно е да знаем, че класовете са референтни типове. Те се заделят в хийпа (този тип памет) и имаме референция (знание, къде се намират в паметта) към тях.

ARC (Automatic Reference Counting)

Това е механизмът, който се ползва от Swift за управление на паметта.

Една инстанция се пази в паметта докато има референции към нея. Ако няма повече референции, тогава тя бива деинициализирана. Стандартните референции са силни референции (strong), защото задържат паметта сочена от тях и тя няма да бъде деинициализирана.

Можем да говорим за ARC при класовете (т.е. референтните типове) и клоужърите. Типовете, които се предават по стойнонст като структури и изброими типове, не са част от ARC управлението на паметта. Те се управляват от друг различен механизъм, които приемаме за даденост.

Защо ни трябва автоматично управление?

Автоматичното управление на паметта ни позволява да се фокусриаме над истинските проблеми, а не над управлението на паметта в компютъра. Има различни механизми за управление на памет. Първият - най-базовият е ръчно управление на памет. Среща се в езиците като C, C++. Характерно е, че всяка динамично заделена инстанция заема памет и тази памет трябва експлицитно да бъде освободена след като няма да бъде използвана за напред. Второ ниво на автоматизиране е ARC (механизмът използван от Swift). Характерно за него е, че всяка истанция знае броя референции към нея. Т.е. ако имам две променливи, които сочат конкретна инстанция, то тази инстанция знае, че има поне две референции. Освобождаването на паметта настъпва автоматично, когато броят на референциите стане равен на нула и вече никой няма да използва обекта. Последното ниво на автоматизация (пълна автоматизация) е механизъм, който разчита на garbage collector (гарбидж-колектор). Това е алгоритъм, който се грижи за автоматичното разпознаване на ненужните обекти и освобождаването им. Предимството му е, че програмистът не трябва да се занимава с управлението на паметта - което не е напълно вярно за ARC. Непредсказуемостта му на изпълнение (кога ще се стартира) е основен недостатък.

За да илюстрираме освобождаването на памет, ще отпечатване текст при автоматичното извикване не deinit метода на класа.

class Car {
    private var name:String
    
    init(name:String) {
        self.name = name
        print("Initalize a car instance with name: \(name)")
    }
    
    deinit {
        print("Deinit a car instance with name \(self.name) ")
    }
}
    
var tesla:Car? = Car(name: "Tesla")
tesla = nil

Алокиране и деалокиране

Заделянето на памет настъпва когато инициализраме нова инстанция - обект от даден тип. Това става, когато неявно извикаме init метода на един клас.

Освобождаването на заетата памет става, когато нямаме повече референции към даден обект. Това е факт, когато занулим съответните променливи, както в примера по горе.

В общия случай ARC се справя с управлението на паметта, с изключения когато се получи цикъл от референции. Тогава броячите на всяка инстанция не стигат до 0 и паметта не може да бъде освободена.

Точно такива цикли са причината за memory leak-ове ("изтичане на памет"). Следва пример, който илюстрира цикъл от референции.

Сега ще дадем пример за референтен цикъл.

Нека да реализираме следните два класа:

  • Книга (има точно един автор)
  • Автор (има точно една книга)

Това е частен случай на реалността, но е напълно достатъчен да покаже проблема.

class Book {
    let title:String
    let author:Author
    var genre:String?
    var pages:Int = 0
    
    init(title:String, author:Author) {
        self.title = title
        self.author = author
    }
    
    deinit {
        print("Deinit a book instance with title \(self.title) ")
    }
}

class Author {
    let name:String
    //трябва да добавим weak пред пропъртито, за да
    //можем да прекъснем цикъла
    //weak var book:Book?
    var book:Book?
    var age:Int
    var isAlive:Bool
    
    init(name:String, age:Int, isAlive:Bool) {
        self.name = name
        self.age = age
        self.isAlive = isAlive
    }
    
    deinit {
        print("Deinit an Auhtor instance with name \(self.name) ")
    }
}


var author:Author? = Author(name: "Достоевски",age: 73, isAlive: false)

var book:Book? = Book(title: "53",author:author! )

author!.book = book
//не можем да го прекъснем
book = nil
author = nil

Можем да направим следните изводи:

  • ARC е добър, когато няма цикли.
  • Ако имаме цикли от референции, трябва да използваме съответните механизми, за да ги разрешим.
  • ARC има нужда от малка подсказка, за да може да реши проблема.

Да използваме weak референция

weak реферeцията е такава, която позволява на ARC да деинициализира променливата, сочена от референцията. В резултат на това, тази променлива има стойност nil. Не можем да направим константа и типът е винаги опционален (optional). Трябва да използваме такава референция, когато реферираният елемент може да бъде заменен.

Наблюдателите (observers) на пропъртита не се активират, когато ARC промени стойността на nil.

При езиците с гарбидж-колектор (алтернативен механизъм на ARC) weak референциите имат друго значение. Те често се ползват, когато се реализира кеш от обекти, който трябва да се освободи, само когато няма достатъчно памет. Освобождаването се извършва от гарбидж-колектора. При ARC weak се различава и не може да бъде ползвана по този начин, тъй като референциите(паметта) биват освободени веднага.

Да използваме unowned референция

unowned реферeнцията е такава, която позволява на ARC да деинициализира променливата, но тук интересното е, че 'дължината на живот' на тази променлива е същата или по-дълга. Т.е. няма да има случай в който тя да сочи към мястото в паметта, а то да е nil.

Трябва да се използва unowned когато сме сигурни, че референцията ще сочи инстанция, която няма да е деинициализирана. Ако се опитате да достъпите такава инстанция ще се получи грешка при изпълнение (runtime грешка). Ето и пример, който можем да разрешим с помощта на unowned модификатора.

Да се реализира примерна йерархия:

  • Студент (който има студентска книжка)
  • Студентска книжка (StudentCard, която има студент)
class Student {
    let name: String
    var age = 19
    var card:StudentCard?
    
    init(name:String, age:Int) {
        self.name = name
        self.age = age
        print("Init a student instance.")
    }
    
    func printStrudent() {
        print("Name: \(name) ")
        print("Age: \(age) ")
    }
    
    deinit {
        print("deInit a student instance")
    }
}

class StudentCard {
    let university: String
    let number: String
    
    
//    unowned(unsafe) let student:Student
//    unowned let student:Student
    let student:Student
    
    init(uni:String, number:String, student:Student) {
        university = uni
        self.number = number
        self.student = student
    }
    
    deinit {
        print("deInit a student-card instance - \(self.number)")
    }
}

var student:Student? = Student(name: "Г. Петров", age: 21)
var studentId:StudentCard? = StudentCard(uni: "СУ св. 'Климент Охридски'", number: "35123", student: student!)

student?.card = studentId

studentId = nil
student = nil

Можем да използваме варианта unowned(unsafe), където проверката дали паметта не е занулена, е изключена. Този вариант е по-бърз от стандартния, но носи рискове в случаите, когато инстанцията е деалокирана.

Референтни цикли можем да получим когато използваме и други референтни типове, примерно клоужъри.

Цикли от референции при клоужъри (closures)

Понеже тялото на клоужър (closure) запомня (capture) променливи и ако го използваме в клас - запомня self, тогава можем да стигнем до цикъл. Тъй като клоужърите и те са референтен тип, те могат да образуват цикъл от референции.

Сега ще разгледаме следния пример:

class DataType {
    let name:String
    var properties: Array<String> //[String]
    
    let prettyPrint = true
    
    init(name:String) {
        self.name = name
        properties = []
    }
    
    lazy var toSwift: () -> String = {
    //списъка с променливите в клоужъра ни
    //позволява да упражним допълнителен контрол
//        [unowned self, name = self.name] in

        var swiftCode:String = "class \(self.name) {"
        
        if self.prettyPrint {
            swiftCode += "\n"
        }
        
        for property in self.properties {
            if self.prettyPrint {
                swiftCode += "\t"
            }
            swiftCode += property
        }
        
        if self.prettyPrint {
            swiftCode += "\n"
        }
        
        swiftCode += "}"
        
        return swiftCode
    }
    
    deinit {
        print("Deinit dataType instance \(self.name)")
    }
}

var student:DataType? = DataType(name: "Student")
student?.properties.append("var name:String = \"Без име\"")
print(student!.toSwift())
student = nil

За да решим проблема трябва да използваме списъка с променливите, които клоужъра зaпомня и да ги определим като unowned или weak. Този списък се нарича capture list - или списък със запомнени променливи, които се използват в тялото на клоужъра. Той позволява добавяне на допълнителни модификатори към променливите и дори дефиниране на нови, които пази клоужъра.

Ето и един пример, който показва каква е разликата между клоужър с и без такъв списък.

var myA = 0
var myB = 0

let f: () -> () = { [myA] in
    print("A = \(myA)")
    print("B = \(myB)")
}
    
myA = 7
myB = 7

f()

//A = 0
//B = 7

Ето как изглежда примера за референтни типове.

var myA = Car(name: "Tesla A 1.0")
var myB = Car(name: "Tesla B 1.0")

let f: () -> () = { [weak myA] in
    print("A = \(myA?.name ?? "???")")
    print("B = \(String(describing: myB.name))")
}


myA.name = "Tesla A 2.0"
myB.name = "Tesla B 2.0"
//"Tesla A 2.0"
//"Tesla B 2.0"

На базата на примера можем да направим промяна, като добавим [unowned self] преди параметрите на клоужъра.