2016년 12월 19일 월요일

Reqwest 소스 분석

https://github.com/seanmonstar/reqwest


RequestBuilder는 HTTP 요청에 가장 기본이 되는 structure로서 Request에 관련된 값들을 가지고 있는다.
// Arc: An atomically reference counted wrapper for shared state.
pub struct RequestBuilder {
  // client를 항상 새로 만들지 않고 재활용을 하기 위해 Arc를 사용한다.
  client: Arc<ClientRef>,
  method: Method,
  url: Result<Url, ::UrlError>,
  _version: HttpVersion,
  headers: Headers,
  body: Option<::Result<Body>>,
}

// Result와 Option은 rust에서 기본으로 제공한다.
enum Result<T, E> {
   Ok(T),
   Err(E),
}

pub enum Option<T> {
    None,
    Some(T),
}
Client는 실제 네트워크상으로 HTTP 요청을 하는 것에 관련되어 hyper에 관련된 정보(client, redirect_policy)를 내부에 가지고 있는다. Method는 어떤 메소드(Get, Post같은)를 사용할 것인지를 의미하고 HttpVersion은 HTTP/1.1, HTTP/2.0등 어떤 버전을 사용할 것인지를 의미한다. 현재 Reqwest는 HTTP/1.1만을 사용한다. RequestBuilder가 생성되고 나면 header와 body를 관련 함수를 사용하여 설정할 수 있게 한다. RequestBuilder를 자세히 살펴보기 전에 Client를 먼저 살펴보자.
pub struct Client {
  // 재활용이 가능하도록 하기 위해 Arc로 보호
  inner: Arc<ClientRef> // ::hyper::Client,
}

// Mutex: A mutual exclusion primitive useful for protecting shared data
struct ClientRef {
  hyper: ::hyper::client,
  // 값의 변경이 가능하도록 하기 위해 Mutex를 사용한다.
  redirect_policy: Mutex<RedirectPolicy>,
}
Client의 생성은 Client::new()를 호출함으로써 이루어진다.
impl Client {
  pub fn new() -> ::Result<Client> {
    let mut client = try!(new_hyper_client());
    client.set_redirect_policy(::hyper::client::RedirectPolicy::FollowNone);
    Ok(Client {
      inner: Arc::new(ClientRef {
        hyper:client,
        redirect_policy: Mutex::new(RedirectPolicy::default()),
      }),
    })
  }

  pub fn redirect(&mut self, policy: RedirectPolicy) {
    *self.inner.redirect_policy.lock().unwrap() = policy;
  }
}
try!는 Result가 error이면 바로 error를 리턴하는 매크로이다. new_hyper_client()를 사용하여 hyper의 Client를 생성한 후 redirect policy를 FollowNone으로 설정한다. 그리고, Client를 만들어서 리턴한다. 기본으로 제공하는 Result를 간단하게 사용하기 위해 아래의 type alias를 지정하여 사용한다. Error도 여기서 정의한 enum으로서 request시 에러가 발생했을 때 사용된다.
pub type Result<T> = ::std::result::Result<T, Error>;
new_hyper_client()를 살펴보자.
fn new_hyper_client() -> ::Result<::hyper::Client> {
  use tls:TlsClient;
  Ok(::hyper::Client::with_connector(
    ::hyper::client::Pool::with_connector(
      Default::default(),
      ::hyper::net::HttpsConnector::new(try!(TlsClient::new()))
    )
  ))
}
hyper의 Client를 만들어주기 위해 관련 함수들을 사용한다. 현재는 HTTPS만을 지원하는 것으로 한다. HTTPS 지원을 위하여 tls.rs에 있는 관련 함수들을 사용한다.
간단하게 get을 요청하는 경우에 사용하기 위한 간단한 함수를 만들어 보자.
impl Client {
  pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
    self.request(Method::Get, url)
  }

  pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
    let url = url.into_url();
    RequestBuilder {
      client: self.inner.clone(), // reference count가 증가한다.
      method: method,
      url: url,
      _version: HttpVersion::Http11,
      headers: Headers::new(),
      body: None
    }
  }
}
IntoUrl은 hyper에서 url에 사용하는 Url로의 변환을 해주는 trait이다. hyper에는 Url, str, String에 대해 정의되어 있다.
trait IntoUrl {
  fn into_url(self) -> Result<Url, UrlError>;
}
위와 같이 get을 만들면 마찬가지로 post와 head도 만들 수 있다.
impl Client {
  pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder {
    self.request(Method::Post, url)
  }

  pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
    self.request(Method::Head, url)
  }
}
RequestBuilder를 만들고 나면 send를 통해 서버에 요청을 할 수 있다.
impl RequestBuilder {
  pub fn send(mut self) -> ::Result<Response> {
    // user agent를 지정해주지 않았으면 기본값으로 지정한다.
    // Headers는 지정한 타입을 받을 수 있게 정의되어 있고 이 타입이 정의한 함수들로부터 필요한 값을 가져온다.
    if !self.headers.has::<UserAgent>() {
      self.headers.set(UserAgent(DEFAULT_USER_AGENT.to_owned()));
    }

    if !self.headers.has::<Accept>() {
      // 'Accept: */*'
      self.headers.set(Accept::star());
    }

    let client = self.client;
    let mut method = self.method;
    // Url로 변경
    let mut url = try!(self.url);
    let mut headers = self.headers;
    // Option<Result<Body>>를 Option<Body>로 변경
    let mut body = match self.body {
      Some(b) => Some(try!(b)),
      None => None,
    };

    // redirect되는 url들을 저장한다.
    let mut urls = Vec::new();

    loop {
      // 서버로부터 응답을 가져온다.
      let res = {
        let mut req = client.hyper.request(method.clone(), url.clone())
            .headers(headers.clone());

        if let Some(ref mut b) = body {
          // hyper에서 사용하는 Body로 변환
          let body = body::as_hyper_body(b);
          req = req.body(body);
        }

        try!(req.send())
      };

      // status를 확인하여 redirect가 필요한지를 본다.
      let should_redirect = match res.status {
        StatusCode::MovedPermanently |
        StatusCode::Found |
        StatusCode::SeeOther => {
          body = None;
          match Method {
            Method::Get | Method::Head => {},
            _ => {
              method = Method::Get;
            }
          }
          true
        },
        StatusCode::TemporaryRedirect |
        StatusCode::PermanentRedirect => {
          if let Some(mut body) = body {
            // redirect시 body를 다시 사용할 수 있는지 확인한다.
            body::can_reset(body)
          } else {
            true
          }
        },
        _ => false,
      };

      if should_redirect {
        // response로부터 Location 헤더를 가져온다.
        // Result가 된다.
        let loc = {
          // Option<Result>가 된다.
          let loc = res.headers.get::<Location>().map(|loc| url.join(loc));
          if let Some(loc) = loc {
            loc
          } else {
            // Location이 오지 않은 경우
            return Ok(Response {
              inner: res
            });
          }
        };

        url = match loc {
          Ok(loc) => {
            headers.set(Referer(url.to_string()));
            urls.push(url);
            // check_redirect 함수가 끝날때까지 redirect_policy의 lock을 잡고 있는다.
            if check_redirect(&client.redirect_policy.lock().unwrap(), &loc, &urls)? {
              loc
            } else {
              // redirect를 허락하지 않는다.
              return Ok(Response {
                inner: res
              });
            }
          },
          Err(e) => {
            // url.join이 실패한 경우
            return Ok(Response {
              inner: res
            });
          },
        };
        // 새 url로 다시 서버에 요청하도록 한다.
      } else {
        // redirect하지 않는 경우
        return Ok(Response {
          inner: res
        });
      }
    }
  }
}
기본 user agent를 사용할 때 to_owned를 통해 복사된 값을 사용한다(Cloning). DEFAULT_USER_AGENT는 아래와 같이 정의되어 있다.
static DEFAULT_USER_AGENT: &'static str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
send 함수가 리턴하는 Response는 다음과 같다.
pub struct Response {
  inner: ::hyper::client::Response,
}
Response로부터 사용할 수 있는 몇가지 함수들을 정의하여 쉽게 사용할 수 있도록 한다.
impl Response {
  // status code를 리턴한다.
  pub fn status(&self) -> &StatusCode {
    &self.inner.status
  }
  // headers를 리턴한다.
  pub fn headers(&self) -> &Headers {
    &self.inner.headers
  }
  // http version을 리턴한다.
  pub fn version(&self) -> &HttpVersion {
    &self.inner.version
  }
}
RequestBuilder에 header를 추가할 수 있게 하자.
imple RequestBuilder<'a> {
  // 헤더 하나를 추가한다.
  pub fn header<H: ::header::Header + ::header::HeaderFormat>(mut self, header: H) -> RequestBuilder<'a> {
    self.headers.set(header);
    self
  }

  // 헤더의 집합을 추가한다.
  pub fn header<H: >(mut self, headers: ::header::Headers) -> RequestBuilder<'a> {
    self.headers.extend(headers.iter());
    self
  }
}
RequestBuilder에 body를 추가할 수 있게 하자.
// 넣으려는 body는 Into<Body>를 구현하고 있어야 한다.
impl RequestBuilder {
  pub fn body<T: Into<Body>>(mut self, body: T) -> RequestBuilder {
    // Body로 변환한 후 Option<Result>로 저장한다.
    self.body = Some(Ok(body.into()));
    self
  }
}
현재까지 사용된 Body에 관련된 부분은 Into<Body>와 body::as_hyper_body이다. 이와 관련하여 Body의 구현을 살펴보자. Body의 구조는 아래와 같다.
pub struct Body {
  reader: Kind,
}

// Box: A pointer type for heap allocation.
// Read: The Read trait allows for reading bytes from a source.
// Send: Types that can be transferred across thread boundaries.
enum Kind {
  // Read가 구현된 것에 대한 처리. File을 읽는다던지 하는 것.
  Reader(Box<Read + Send>, Option<u64>),
  // byte에 대한 처리.
  Bytes(Vec<u8>),
}
Body를 위한 From구현은 Vec<u8>, String, [u8], &str, File의 5개의 타입에 대해 구현한다. From과 Into는 둘 중 어느 한쪽이 구현되면 나머지 한쪽도 자동으로 구현된다.
impl From<Vec<u8>> for Body {
  fn from(v: Vec<u8>) -> Body {
    Body {
      reader: Kind::Bytes(v),
    }
  }
}

impl From<String> for Body {
  fn from(s: String) -> Body {
    s.into_bytes().into() // Vec<u8>로 변환 후 이의 into를 사용한다.
  }
}

impl<'a> From<&'a [u8]> for Body {
  fn from(s: &'a [u8]) -> Body {
    s.to_vec().into() // Vec<u8>로 변환 후 이의 into를 사용한다.
  }
}

impl<'a> From<&'a str> for Body {
  fn from(s: &'a str) -> Body {
    s.as_bytes().into() // Vec<u8>로 변환 후 이의 into를 사용한다.
  }
}

impl From<File> for Body {
  fn from(f: File) -> Body {
    // 파일의 크기를 가져온다.
    let len = f.metadata().map(|m| m.len()).ok();
    Body {
      reader: Kind::Reader(Box::new(f), len),
    }
  }
}
reader로부터 Body를 생성해주는 헬퍼함수로 Body::new()를 정의한다.
impl Body {
  pub fn new<R: Read + Send + 'static>(reader: R) -> Body {
    Body {
      reader: Kind::Reader(Box::new(reader), None),
    }
  }
}
이제 as_hyper_body를 정의하자.
// Body를 hyper에서 사용하는 Body로 변환한 후 이것을 리턴한다.
pub fn as_hyper_body<'a>(body: &'a mut Body) -> ::hyper::client::Body<'a> {
  match body.reader {
    Kind::Bytes(ref bytes) => {
      let len = bytes.len();
      ::hyper::client::Body::BufBody(bytes, len)
    },
    Kind::Reader(ref mut reader, len_opt) => {
      match len_opt {
        Some(len) => ::hyper::client::Body::SizedBody(reader, len),
        None => ::hyper::client::Body::ChunkedBody(reader),
      }
    },
  }
}
재활용이 가능한 Body인지 아닌지를 판별해주는 함수를 정의한다.
// File같은 경우 한번 읽으면 읽는 위치가 계속 증가하기 때문에 그대로 재활용할 수 없다.
pub fn can_reset(body: &Body) -> bool {
  match body.reader {
    Kind::Bytes(_) => true,
    Kind::Reader(..) => false,
  }
}
ClientRef에 redirect policy를 위한 redirect_policy가 있어서 설정을 변경할 수 있다. 이에 관련된 내용을 살펴보자.
pub struct RedirectPolicy {
  inner: Policy,
}

// Sync: Types for which it is safe to share references between threads.
enum Policy {
  // 사용자 지정 함수에 의해 redirect를 할지 말지를 정한다.
  Custom(Box<Fn(&Url, &[Url]) -> ::Result<bool> + Send + Sync + 'static>),
  Limit(usize), // redirect의 수에 제한을 둔다.
  None, // redirect를 하지 않는다.
}
위 enum의 값들을 생성하는 함수를 제공한다.
impl RedirectPolicy {
  pub fn limited(max: usize) -> RedirectPolicy {
    RedirectPolicy {
      inner: Policy::Limit(max),
    }
  }

  pub fn none() -> RedirectPolicy {
    RedirectPolicy {
      inner: Policy::None,
    }
  }

  pub fn custom<T>(policy: T) -> RedirectPolicy
  where T: Fn(&Url, &[Url]) -> ::Result<bool> + Send + Sync + 'static {
    RedirectPolicy {
      inner: Policy::Custom(Box::new(policy)),
    }
  }
}
Client 생성시 사용하는 default 함수는 다음과 같다.
// Default: A trait for giving a type a useful default value.
// default는 redirect를 10번까지 허용하는 것으로 지정한다.
impl Default for RedirectPolicy {
  fn default() -> RedirectPolicy {
    RedirectPolicy::limited(10)
  }
}
RequestBuilder의 send 함수에서는 redirect를 확인하기 위해 check_redirect 함수를 호출한다.
// Client에서 설정한 RedirectPolicy를 받아서 이의 redirect를 호출한다.
pub fn check_redirect(policy: &RedirectPolicy, next: &Url, previous: &[Url]) -> ::Result<bool> {
  policy.redirect(next, previous)
}
RedirectPolicy에 redirect를 구현한다.
impl RedirectPolicy {
  fn redirect(&self, next: &Url, previous: &[Url]) -> ::Result<bool> {
    match self.inner {
      Policy::Custom(ref custom) => custom(next, previous),
      Policy::Limit(max) => {
        if previous.len() == max { // 최대한 허용하는 redirect를 넘은 경우.
          Err(::Error::TooManyRedirects)
        } else if previous.contains(next) { // 이전에 이미 redirect한 url인 경우.
          Err(::Error::RedirectLoop)
        } else { // redirect를 허용하는 경우.
          Ok(true)
      },
      Policy::None => Ok(false),
    }
  }
}
앞에서 ::hyper::Client를 만들때 HttpsConnector를 만들기 위해 파라메터로 TlsClient::new()를 사용했다.
// 멤버 하나를 가지고 있는 tuple
pub struct TlsClient(TlsConnector);

impl TlsClient {
  pub fn new() -> ::Result<TlsClient> {
    // builder의 결과가 Ok이면 and_then의 내용을 실행하고 map을 통해 T를 U로 변경하고 map_err를 통해 E를 F로 변경한다.
    // and_then: Result가 Ok이면 실행.
    // map: Result가 Ok이면 Result<T, E>를 Result<U, E>로 변경.
    // map_err: Result가 Err이면 Result<T, E>를 Result<T, F>로 변경.
    TlsConnector::builder()
        .and_then(|c| c.build()) // c.build()의 리턴값은 Result<TlsConnector>이다.
        .map(TlsClient) // Ok(TlsClient(TlsConnector))가 된다.
        .map_err(|e| ::Error::Http(::hyper:Error:Ssl(Box::new(e))))
  }
}
TlsClient가 HttpsConnector에 사용되기 위해서는 SslClient를 구현해야 한다.
impl SslClient for TlsClient {
  type SslStream = TlsStream;

  // wrap a client stream with SSL.
  fn wrap_client(&self, stream: HttpStream, host: &str) -> ::hyper::Result<Self::Stream> {
    self.0.connect(host, stream).map(TlsStream).map_err(|e| {
      match e {
        HandshakeError::Failure(e) => ::hyper::Error::Ssl(Box::new(e)),
        HandshakeError::Interrupted(..) => {
          unreachable!("TlsClient::handshake interrupted")
        }
      }
    })
  }
}
TlsStream이 SslStream으로 사용되기 위해서 Read, Write, Clone, NetworkStream을 구현해야 한다. native_tls의 TlsStream이 이를 다 구현하고 있기 때문에 TlsStream은 이를 wrapping해서 필요한 구현을 delegate로 구현한다.
// 이름 중복을 피하기 위해 native_tls의 TlsStream을 NativeTlsStream로 이름을 바꿔서 사용한다.
pub struct TlsStream(NativeTlsStream<HttpStream>);

impl Read for TlsStream {
  fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
    self.0.read(buf)
  }
}

impl Write for TlsStream {
  fn write(&mut self, data: &[u8]) -> io::Result<usize> {
    self.0.write(data)
  }

  fn flush(&mut self) -> io::Result<()> {
    self.0.flush()
  }
}

impl Clone for TlsStream {
  fn clone(&self) -> TlsStream {
    unreachable!("TlsStream::clone is never used for the Client")
  }
}

impl NetworkStream for TlsStream {
  fn peer_addr(&mut self) -> io::Result<SocketAddr> {
    self.0.get_mut().peer_addr()
  }

  fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
    self.0.get_ref().set_read_timeout(dur)
  }

  fn set_write_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
    self.0.get_ref().set_write_timeout(dur)
  }
}

2016년 12월 13일 화요일

SlidingUpPanelLayout 소스 분석

지금은 없어졌지만 Umano에서 만든 앱에 적용된 sliding up panel의 구현 소스 분석

https://github.com/umano/AndroidSlidingUpPanel

패널을 위로 스와이프하는 경우 메인 영역 어둡게 처리하기

  • ViewGroup의 child를 그릴때 drawChild가 호출되므로 여기에서 처리하도록 한다.
  • slideable view가 있고 main view인 경우에 main view를 어둡게 처리한다.
  • 그림을 그리는 영역을 canvas.getClipBounds로 얻어온다.
    • 이 영역의 bottom을 slideable view의 top과 비교하여 작은 값으로 바꿔준다. -> slideable view는 어둡게 처리할 필요 없다.
  • 지정된 fade color로부터 alpha를 얻어온 후 slide offset의 비율에 따라 어두운 정도를 계산한다.
  • 계산된 alpha로 다시 color값을 만든 후 이 color를 canvas.drawRect에 지정한다.

Shadow를 slideable view 위에 표시하기

  • draw를 override하여 구현한다.
    • draw는 drawChild에 의해 호출된다.
  • View 소스를 보면 drawing step에 대해 아래와 같은 설명이 있다.
    • 1 Draw the background
    • 2 If necessary, save the canvas' layers to prepare for fading
    • 3 Draw view's content
    • 4 Draw children
    • 5 If necessary, draw the fading edges and restore layers
    • 6 Draw decorations (scrollbars for instance)
  • shadow drawable을 xml로 만든다: gradient
  • draw(Canvas c)를 override 하여 여기서 shadow drawable을 지정한 위치에 그린다.
  • slideable view의 top으로부터 shadow drawable을 놓을 위치(drawable의 top과 bottom)를 파악한다.

onSaveInstanceState

  • slide state 정보를 저장하여 onRestoreInstanceState 호출 시 다시 저장할 수 있도록 한다.

class LayoutParams extends ViewGroup.MarginLayoutParams

  • weight를 지정하여 percentage로 height를 정할 수 있도록 한다.
  • ViewGroup의 generateLayoutParams이 호출될 때 이 LayoutParams를 생성하여 리턴하여 이 layout이 사용되도록 한다.
  • checkLayoutParams가 false를 리턴하는 경우 generateLayoutParams이 호출된다.
  • onLayout에서 margin이 크기 계산에 사용될 수 있도록하기 위해 사용된다.

class DragHelperCallback extends ViewDragHelper.Callback

  • tryCaptureView
    • dragging을 허용하는 slideable view를 리턴하도록 한다.
  • getViewVerticalDragRange
    • slide range를 리턴한다.
  • onViewDragStateChanged
    • drag state가 바뀌면 slide offset을 계산한 후 이 값에 따른 state를 저장한다.
  • onViewPositionChanged
    • slideable view의 위치에 따라 main view의 크기를 조절한다.
      • 보통은 현재 상태 그대로 유지.
      • collapsed 상태이고 overlay가 아닐 때 height를 match_parent로 변경한다.
  • onViewReleased
    • dragging하다가 손을 뗀 경우 호출된다.
    • slideable view를 최종으로 위치시킬 top 위치를 계산한 후 settleCapturedViewAt를 사용하여 적용한다.
  • clampViewPositionVertical
    • collapsed와 expanded 사이로 해서 top의 위치를 리턴한다. 이 값으로 slideable view의 위치를 이동한다.

mFirstLayout

  • layout을 다시 정하는 경우 사용된다.
  • true로 설정
    • anchor point를 새로 설정할 때
    • onAttachedToWindow와 onDetachedFromWindow가 호출될 때
    • onSizeChanged가 호출될 때 height가 변경되었을 때
  • false로 설정
    • onLayout이 끝난 경우
  • onLayout
    • slide offset 새로 계산
    • slideable view의 경우 새로 계산된 slide offset 값에 따라 새로 위치가 계산되어야 함

setWillNotDraw(false)

  • 직접 draw를 그린다는 것을 의미한다. onDraw가 호출되도록 한다.

onMeasure

  • child로 두개만 가지는지를 확인한다. 첫번째가 main view이고 두번째가 slideable view이다.
  • main view와 slideable view의 width와 height를 계산한다.
  • main view
    • overlay가 아닌 경우 height에서 panel의 height를 뺀다.
  • slideable view
    • slide range를 지정한다: height - panel height

onLayout

  • slide state에 따라 slide offset(0 ~ 1)을 계산한다.
  • slide offset에 따라 slideable view의 top을 계산한 후 이를 적용한다.
  • parallax가 지정된 경우 main view의 위치를 ViewCompat.setTranslationY함수를 사용해서 slide offset의 비율만큼 옮겨준다.

computeScroll

  • 스크롤시 호출되는 함수
  • 보통 특별한 작업이 필요한 것이 아니면 여기서 사용되는 것과 같이 쓰면 된다: continueSettling와 ViewCompat.postInvalidateOnAnimation의 조합

requestLayout과 invalidate의 사용

  • layout부터 다시 그려야 하는 경우는 requestLayout을 호출하고 view의 내용만 다시 그려야 하는 경우는 invalidate를 호출해준다.

터치 이벤트 처리

  • 뷰의 터치 이벤트 관련 함수에서 ViewDragHelper의 관련 함수를 호출해준다.
  • dispatchTouchEvent
    • dragging중일때는 바로 onTouchEvent를 호출한다.
  • onInterceptTouchEvent
    • mDragHelper.shouldInterceptTouchEvent(ev)를 호출해준다.
    • true를 리턴하면 자기 자신의 onTouchEvent를 호출하고 false를 리턴하면 자식의 onTouchEvent가 호출된다.
    • 터치 포인트가 slideable view 영역 안이 아니면 mDragHelper.cancel()를 호출하고 false를 리턴한다.
  • onTouchEvent
    • mDragHelper.processTouchEvent(ev)를 호출해준다.

터치 영역이 특정 View에서 이루어진 것인지 확인하기

  • 확인을 원하는 View의 getLocationOnScreen함수를 호출하여 스크린 기준 기준점의 위치를 알아낸다. -> viewLocation
  • 자기 자신의 getLocationOnScreen함수를 호출하여 스크린 기준 기준점의 위치를 알아낸다. 여기에 터치 포인트를 더하여 스크린의 어디에서 터치가 일어났는지 알아낸다. -> screenX, screenY
  • 앞에서 찾은 값의 비교로 터치 포인트가 View안에서 일어난 것인지 확인 가능

2016년 11월 12일 토요일

안드로이드에서 제공하는 머터리얼 스타일 프로그레스 바 만드는 방법

아래의 github code에서 제공하는 CircularProgressView에 대한 소스 분석

https://github.com/rahatarmanahmed/CircularProgressView

View를 상속한 CircularProgressView를 만든다.

public class CircularProgressView extends View

View를 상속하면 최대 4개의 constructor를 구현해야 한다.(보통 여기서 xml에 넣어준 attribute들을 가져와서 설정을 한다.)
constructor에서 attribute들에 대한 값들을 설정하고, 이 값들을 가지고 paint도 설정을 해준다.
(나중에 onDraw에서 그림 그릴때 이렇게 설정된 paint가 사용된다.)

onMeasure와 onSizeChanged로부터 width와 height를 파악해서 작은쪽의 값을 size에 저장하고 bound에는 (left: paddingLeft + thickness, top: paddingTop + thickness, right: size - paddingLeft - thickness, bottom: size - paddingTop - thickness)를 저장한다.

onDraw에서 아래의 코드로 호를 그린다.

canvas.drawArc(bounds, startAngle + indeterminateRotateOffset, indeterminateSweep, false, paint);

호를 startAngle + indeterminateRotateOffset 각도에서 시작해서 indeterminateSweep 크기만큼의 각도로 그린다. 이 세 값은 animator가 업데이트 되어 invalidate()를 호출하기 전에 업데이트 된다.

AnimatorSet을 사용해서 animation set을 만들어 이 set을 실행한다. AnimatorSet의 play()를 통해 animation의 선후관계를 만든다.

이제 가장 중요한 animation을 만들어 보자. 총 4개의 animation을 만든다.
첫번째는 호를 그리는 animation으로 indeterminateSweep(계속 증가)을 변경한다.
두번째는 호를 이동하는 animation으로 indeterminateRotateOffset을 변경한다.
세번째는 호를 줄이는 animation으로 startAngle(증가)과 indeterminateSweep(계속 감소)을 변경한다.
네번째는 두번째와 마찬가지로 호를 이동하는 animation으로 indeterminateRotateOffset을 변경한다.

첫번째를 다시 코드로 자세히 보면, ValueAnimator.ofFloat(start, sweep)을 통해 호를 그리게 될 각도의 interpolation 영역을 지정하고 이의 duration, interpolator도 지정해준다. 그리고 나서 리스너를 달아 값이 나오면 이를 indeterminateSweep에 넣고 invalidate()를 호출한다. 이렇게 함으로서 onDraw에서는 변경된 indeterminateSweep을 가지고 점점 커지는 호를 그리게 된다.

ValueAnimator frontEndExtend = ValueAnimator.ofFloat(INDETERMINANT_MIN_SWEEP, maxSweep);
frontEndExtend.setDuration(animDuration/animSteps/2);
frontEndExtend.setInterpolator(new DecelerateInterpolator(1));
frontEndExtend.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override    public void onAnimationUpdate(ValueAnimator animation) {
        indeterminateSweep = (Float) animation.getAnimatedValue();
        invalidate();
    }
});

첫번째와 두번째 animation을 같이 동작시키고, 그 이후에 세번째와 네번째를 같이 동작시킴으로서 안드로이드에서의 material progress bar와 비슷한 형태의 progress bar가 나오게 된다.




2016년 11월 8일 화요일

WebTorrent 소스 분석

https://github.com/feross/webtorrent

bittorrent-dht

  • 처음 시작시 find_node로 node들을 찾는다. lookup으로 내가 원하는 info_hash를 가지고 있는 node들을 찾는다. announce로 node들에게 내가 info_hash를 다운받고 있음을 알린다. ping으로 node가 살아있는지 확인한다. 에러가 발생하면 error를 응답한다.
  • 'KRPC Protocol'에 관련된 부분은 k-rpc 모듈에 의해 이루어진다.
  • id는 k-rpc 생성시 만들어진다.
  • _tables: info_hash를 key로 하여 찾은 node 정보를 저장한다.
  • _values: arbitrary payload를 저장한다.
  • _peers: 나에게 announce 쿼리를 보낸 peer 정보를 저장한다.
  • 5분마다 secret 값을 변경한다. secret는 token 생성에 사용된다.
  • routing table에 있는 값들의 저장을 위해 toJSON 함수를 제공한다.
    • toJSON 함수를 사용하여 뽑아낸 node와 value의 정보를 디스크등에 저장해 놓았다가 다시 시작시 addNode를 통해 routing table에 추가할 수 있다.
  • find_node
    • query: { "a": { "id": "<querying nodes id>", "target": "<id of target node>" }
    • 이 작업이 이루어짐으로서 서버로부터 관련 node들의 정보가 routing table에 저장된다.
    • k-rpc에서 기본으로 사용하는 서버들(BOOTSTRAP_NODES)이 있다: router.bittorrent.com:6881, router.utorrent.com:6881, dht.transmissionbt.com:6881
  • lookup
    • query: { "q": "get-peers", "a": { "id": "<querying nodes id>", "info_hash": "<20-byte infohash of target torrent>" } }
    • 응답이 오면 k-bucket에 넣고(id, host, port, token) 'peer' event를 emit한다. peer는 {host, port}이다.
  • announce
    • table에서 info_hash에 연결되어 있는 k-bucket을 찾는다. 여기에는 info_hash를 가지고 있는 node들의 정보가 들어있다. 여기서 info_hash에 closest인 node들을 찾아 이들에 announce를 한다.
    • query: { "q": "announce_peer", "a": { "id": "<querying nodes id>", "token": "<response to a previous get_peers query>", "info_hash": "<20-byte infohash of target torrent>", "port": "port", "implied_port": "0 or 1" } }
  • ping
    • query: { "q": "ping" }
    • response: { "r": "<querying nodes id>" }
  • error
    • response: { "e":[201, "A Generic Error Ocurred"] }

webtorrent

  • handshake를 보낸다. -> (bitfield를 받는다. -> request를 보낸다. -> piece를 받는다.) -> keep-alive를 60s마다 받는다. -> (interested를 받는다. -> unchoked를 보낸다.) -> peer가 DHT를 지원하는 경우 자신의 DHT 포트를 알리는 port를 받는다. -> 모든 piece를 다 받은 경우 본인을 seeder로 설정하고 remote peer에 choked를 보낸다.

BitTorrent Protocol

  • handshake: <pstrlen><pstr><reserved><info_hash><peer_id>
    • 상대 client와 연결시 첫번째로 보내는 메시지.
    • version 1.0의 경우 pstrlen = 19이고 pstr = "BitTorrent protocol" 이다.
  • message format: <length prefix><message ID><payload>
  • port: <len=0003><id=9><listen-port>
    • DHT를 구현하고 있는 경우 remote peer에게 보내는 메시지.
  • keep-alive: <len=0000>
    • 연결이 끊기지 않도록 하기 위해 보내는 메시지. 보통 2분마다 보낸다(webtorrent는 1분마다 보낸다).
  • bitfield: <len=0001+X><id=5><bitfield>
    • 어떤 piece를 가지고 있고 안가지고 있는지를 알리는 메시지.
    • bit에서 cleared는 missing piece를 set은 downloaded를 의미한다.
    • lazy bitfield: 모든 bit를 cleared 하여 받은 부분이 없다고 알린 후, have 메시지로 가지고 있는 부분을 알리는 방법. ISP filtering을 막는 방법이라고 알려져 있다.
  • have: <len=0005><id=4><piece index>
    • 가지고 있는 piece의 index를 알리는 메시지
  • request: <len=0013><id=6><index><begin><length>
    • 지정한 piece를 요청한다.
  • piece: <len=0009+X><id=7><index><begin><block>
    • 요청받은 piece를 응답한다.
  • state information: interested and choked
    • interested는 remote peer가 block을 요청할 것이라는 것을 의미하고 choked는 remote peer가 요청에 응답을 주지 않을 것이라는 것을 의미한다.
    • interested: <len=0001><id=2>
    • not interested: <len=0001><id=3>
    • choke: <len=0001><id=0>
    • unchoke: <len=0001><id=1>

2016년 11월 4일 금요일

Swift 3.1에 추가될 사항 살펴보기

## 0045 Add prefix(while:) and drop(while:) to the stdlib

https://github.com/apple/swift-evolution/blob/master/proposals/0045-scan-takewhile-dropwhile.md

Collection과 LazySequenceProtocol과 LazyCollectionProtocol에 새로운 2개의 함수 prefix와 drop을 추가한다.

## 0141 Availability by Swift version

https://github.com/apple/swift-evolution/blob/master/proposals/0141-available-by-swift-version.md

@available(...) attribute에 swift version을 추가한다.
기존에 platform이나 os version으로 사용하던 것과 같이 사용하면 된다.


@available(swift, obsoleted: 3.1)
class Foo {
  //...
}

하지만, 아래와 같이 platform availability abbreviation list에 swift를 추가하는 것은 허락하지 않는다.

  • @available(swift 3, *)
  • @available(swift 3, iOS 10, *)

## 0145 - Package Manager Version Pinning

## 0082 - Package Manager Editable Packages

## 0080 - Failable Numeric Conversion Initialisers

새로운 종류의 conversion initialiser를 더한다.
//  Conversions from all integer types.
init?(exactly value: Int8)
init?(exactly value: Int16)
init?(exactly value: Int32)
init?(exactly value: Int64)
init?(exactly value: Int)
init?(exactly value: UInt8)
init?(exactly value: UInt16)
init?(exactly value: UInt32)
init?(exactly value: UInt64)
init?(exactly value: UInt)

//  Conversions from all floating-point types.
init?(exactly value: Float)
init?(exactly value: Double)
#if arch(i386) || arch(x86_64)
init?(exactly value: Float80)
#endif

## 0147 - Move UnsafeMutablePointer.initialize(from:) to UnsafeMutableBufferPointer

UnsafeMutablePointer.initialize(from:)는 deprecated 시키고 UnsafeMutableBufferPointer를 사용한다.
UnsafeMutableRawPointer.initializeMemory(as:from:)는 deprecated 시키고 UnsafeMutableRawBufferPointer.initialize(as:from:)를 사용한다.

기존에 Collection을 취하던 것이 Sequence를 취하는 것으로 변경된다.

Array와 ArraySlice와 ContiguousArray에서의 +=와 append<C : Collection>(contentsOf newElements: C)는 더이상 필요없기 때문에 삭제된다(다른 방식으로 효율적으로 구현할 수 있게 된다.).

## 0151 - Package Manager Swift Language Compatibility Version

## 0152 - Package Manager Tools Version


Rust 변경사항 살펴보기 :

## RFC 1624 : loop-break-value

https://github.com/rust-lang/rfcs/blob/master/text/1624-loop-break-value.md

break가 value를 가질 수 있게 하고 loop가 리턴값을 가질 수 있게 한다.

break는 아래의 4가지가 가능해진다.(label은 loop의 이름이고 EXPR은 expression을 의미)

1. break;
2. break 'label;
3. break EXPR;
4. break 'label EXPR;

loop의 리턴 타입

1. break가 없으면 리턴하지 않는다는 것을 의미하는 !가 된다.
2. break가 있으면 ()가 리턴값이 된다.
3. break EXPR이 있으면 EXPR의 타입이 리턴타입이 된다.

위의 내용을 적용함으로서 코드가 아래처럼 간결해 질 수 있다.

// without loop-break-value:
let x = {
    let temp_bar;
    loop {
        ...
        if ... {
            temp_bar = bar;
            break;
        }
    }
    foo(temp_bar)
};

// with loop-break-value:
let x = foo(loop {
        ...
        if ... { break bar; }
    });

## RFC 1682 : field-init-shorthand

https://github.com/rust-lang/rfcs/blob/master/text/1682-field-init-shorthand.md

named field를 가지는 struct와 union과 enum에서 초기화 시 field가 같은 이름을 가지는 경우 'field: field' 대신 'field'를 사용할 수 있도록 한다.

위의 내용을 적용함으로서 코드가 아래처럼 간결해 질 수 있다.

struct SomeStruct { field1: ComplexType, field2: AnotherType }

impl SomeStruct {
    fn new() -> Self {
        let field1 = {
            // Various initialization code
        };
        let field2 = {
            // More initialization code
        };
        SomeStruct { field1, field2 }
    }
}

## RFC 1665 : windows-subsystem

https://github.com/rust-lang/rfcs/blob/dd3ba8ec65f02c7742c75c7a25d5ce2c49fa5012/text/1665-windows-subsystem.md

현재의 rust program은 Windows에서 실행될 때 항상 console을 띄운다. 이것은 Windows가 CONSOLE subsystem은 main을 entry point로 하고 WINDOWS subsytem은 WinMain을 entry point로 하는데, rust program은 항상 main이 entry point이기 때문이다.

이 문제의 해결을 위해 아래와 같은 attribute를 새로 추가한다.

#![windows_subsystem = "windows"]

여기서 가능한 값은 {windows, console} 이고 나중에 더 추가될 수도 있다.

위 attribute에서 subsystem이 "windows" 이면 "/ENTRY:mainCRTStartup" 을 linker option에 추가함으로서 WINDOWS subsystem의 경우에 main을 entry point로 사용하면서 console이 뜨지 않도록 한다.

## RFC 1725 : unaligned access

https://github.com/rust-lang/rfcs/blob/master/text/1725-unaligned-access.md

unaligned pointer에서 reading/writing을 할 수 있는 ptr::read_unaligned와 ptr::write_unaligned를 추가한다.

사실 위 두 함수의 구현은 ptr::copy_nonoverlapping
의 wrapper이다. 따라서, ptr::copy_nonoverlapping를 직접 사용해도 된다. 그래도 위의 두 함수가 사용하기 더 편리하기도 하고 좀 더 직관적이다.

## RFC 1566 : procedural macros

## RFC 1647 : allow self in where clauses

타입이 trait의 implementations의 어떤 위치에서든 사용이 가능하도록 한다.
impl SomeTrait for SomeType where Self: SomeOtherTrait { }

impl SomeTrait<Self> for SomeType { }

impl SomeTrait for SomeType where SomeOtherType<Self>: SomeTrait { }

impl SomeTrait for SomeType where Self::AssocType: SomeOtherTrait {
    AssocType = SomeOtherType;
}

하지만 아래는 안된다.
// The error here is because this would be Vec<Vec<Self>>, Vec<Vec<Vec<Self>>>, ...
impl SomeTrait for Vec<Self> { }

## RFC 1414 : rvalue static promotion

constexpr rvalues를 static memory에 values로 promote한다.

function body's block에서 아래의 조건이 맞으면
* constexpr rvalue에의 shared reference를 취하는 경우(&<constexpr>)
* constexpr이 UnsafeCell { ... } 생성자를 포함하고 있지 않은 경우
* constexpr이 UnsafeCell을 포함하는 타입을 리턴하는 const fn call을 포함하고 있지 않은 경우
rvalue를 static memory location으로 translate 하고 resulting reference 'static lifetime을 준다.

예는 아래와 같다.
// OK:
let a: &'static u32 = &32;
let b: &'static Option<UnsafeCell<u32>> = &None;
let c: &'static Fn() -> u32 = &|| 42;

let h: &'static u32 = &(32 + 64);

fn generic<T>() -> &'static Option<T> {
    &None::<T>
}

// BAD:
let f: &'static Option<UnsafeCell<u32>> = &Some(UnsafeCell { data: 32 });
let g: &'static Cell<u32> = &Cell::new(); // assuming conf fn new()

## RFC 1651 : movecell

Cell이  non-Copy타입하고도 잘 동작하도록 확장한다.

이에 따라 get 함수는 Copy 타입에만 동작하도록 변경되고, 추가되는 함수들이 있다.
impl<T> Cell<T> {
    fn set(&self, val: T);
    fn replace(&self, val: T) -> T;
    fn into_inner(self) -> T;
}

impl<T: Copy> Cell<T> {
    fn get(&self) -> T;
}

impl<T: Default> Cell<T> {
    fn take(&self) -> T;
}

## RFC 1584 : macros

Declarative macros 2.0. macro_rules!에의 교체

// Syntax (TBA)

macro foo($a: ident) => {
    return $a + 1;
}

## RFC 1558 : closure to fn coercion


Generic interfaces 요점

 https://go.dev/blog/generic-interfaces  Generic interface를 정의할 때 최소한의 제약만을 정의하고 실제 구현체들이 자신만의 필요한 제약을 추가할 수 있도록 하는 것이 좋다. pointer receiver를...