woshidan's blog

あいとゆうきとITと、とっておきの話。

AndroidのImageViewのscaleTypeについて(コード読んだメモ編)

前の記事の検証する前に、いまいち動作がはっきりしないところがあったのでコード読んでいたときのメモです。

力尽きたので雑なのですが。。

どこでscaleTypeの設定を適用しているかを調べるために、 https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java をscaleTypeなどで検索するところから始めました。

読んでいるソースコードがMarshmallowのものである理由は、1/20時点でダッシュボードにて確認したら一番シェアが高かったためです。

目次

  • 関連しそうなソースコード
  • configureBounds()内で処理されるscaleTypeについて
  • mDrawMatrixがどこで使われるかについて
  • scaleTypeごとのコードリーディング
    • scaleType: CENTER
    • scaleType: CENTER_CROP
    • scaleType: CENTER_INSIDE
    • scaleType: FIT_XY
    • scaleType: MATRIX
    • FIT_CENTER, FIT_END, FIT_START
    • ScaleTypeがMATRIXの場合以外、サイズが合っていたら何もしない

関連しそうなソースコード

さて、初期化以外でscaleTypeが出てくるのは下記です。

https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java#L1093-L1176

// ImageView.java
    private void configureBounds() {
        if (mDrawable == null || !mHaveFrame) {
            return;
        }

        int dwidth = mDrawableWidth;
        int dheight = mDrawableHeight;

        int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
        int vheight = getHeight() - mPaddingTop - mPaddingBottom;

        boolean fits = (dwidth < 0 || vwidth == dwidth) &&
                       (dheight < 0 || vheight == dheight);

        if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
            /* If the drawable has no intrinsic size, or we're told to
                scaletofit, then we just fill our entire view.
            */
            mDrawable.setBounds(0, 0, vwidth, vheight);
            mDrawMatrix = null;
        } else {
            // We need to do the scaling ourself, so have the drawable
            // use its native size.
            mDrawable.setBounds(0, 0, dwidth, dheight);

            if (ScaleType.MATRIX == mScaleType) {
                // Use the specified matrix as-is.
                if (mMatrix.isIdentity()) {
                    mDrawMatrix = null;
                } else {
                    mDrawMatrix = mMatrix;
                }
            } else if (fits) {
                // The bitmap fits exactly, no transform needed.
                mDrawMatrix = null;
            } else if (ScaleType.CENTER == mScaleType) {
                // Center bitmap in view, no scaling.
                mDrawMatrix = mMatrix;
                mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f),
                                         Math.round((vheight - dheight) * 0.5f));
            } else if (ScaleType.CENTER_CROP == mScaleType) {
                mDrawMatrix = mMatrix;

                float scale;
                float dx = 0, dy = 0;

                if (dwidth * vheight > vwidth * dheight) {
                    scale = (float) vheight / (float) dheight; 
                    dx = (vwidth - dwidth * scale) * 0.5f;
                } else {
                    scale = (float) vwidth / (float) dwidth;
                    dy = (vheight - dheight * scale) * 0.5f;
                }

                mDrawMatrix.setScale(scale, scale);
                mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));
            } else if (ScaleType.CENTER_INSIDE == mScaleType) {
                mDrawMatrix = mMatrix;
                float scale;
                float dx;
                float dy;
                
                if (dwidth <= vwidth && dheight <= vheight) {
                    scale = 1.0f;
                } else {
                    scale = Math.min((float) vwidth / (float) dwidth,
                            (float) vheight / (float) dheight);
                }
                
                dx = Math.round((vwidth - dwidth * scale) * 0.5f);
                dy = Math.round((vheight - dheight * scale) * 0.5f);

                mDrawMatrix.setScale(scale, scale);
                mDrawMatrix.postTranslate(dx, dy);
            } else {
                // Generate the required transform.
                mTempSrc.set(0, 0, dwidth, dheight);
                mTempDst.set(0, 0, vwidth, vheight);
                
                mDrawMatrix = mMatrix;
                mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
            }
        }
    }

configureBounds()内で処理されるscaleTypeについて

全部のscaleTypeがこのメソッドの中の分岐に登場しているわけではなかったので、登場してるかなどざっくり分けると、

scaleType configureBounds()内での扱い
CENTER, CENTER_CROP, CENTER_INSIDE, MATRIX, FIT_XY (+ 画像サイズが取得できなかった場合) 処理あり
FIT_START, FIT_END, FIT_CENTER 個別に処理なし

となっていて、だいたいMatrix.ScaleToFitと同じ内容の処理はここに書いていない、という感じでした。

mDrawMatrixがどこで使われるかについて

ここから、各scaleTypeの処理について、さっきのconfiguraBounds()メソッドの分岐の中を覗いてみて、mDrawMatrixの値がどう設定されているかを見ていくことで、scaleTypeごとにどんな処理がされているかを見ていこうと思います。

といきなり書きましたが、mDrawMatrixの値がどういう風にImageViewの中のDrawableの見え方に影響するかちょっとわかりにくいですね。

このmDrawMatrixがどこで使われているかというと、主にImageViewonDrawメソッド内部で実行されるCanvas.concat(Matrix matrix)メソッドで、Drawableが持ってるCanvasインスタンスに対して拡大縮小などの処理をしています。(細かいところは拡大縮小じゃなくて画像のメモリ管理の話になるので適当にはしょってます)

ImageView onDraw 全体: https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java#L1214-L1247

// 右下方向の余白のために切り抜く
if (mCropToPadding) {
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
            scrollX + mRight - mLeft - mPaddingRight,
            scrollY + mBottom - mTop - mPaddingBottom);
}
// 余白の分だけ左上からずらす
canvas.translate(mPaddingLeft, mPaddingTop);

// Matrixの設定があったら、上記の余白の適用前に、
// configureBoundsで設定したMatrixの設定を適用させる
if (mDrawMatrix != null) {
    canvas.concat(mDrawMatrix);
}
// DrawableにMatrixなどが適用されたCanvasの中身を書き込み
mDrawable.draw(canvas);

scaleTypeごとのコードリーディング

scaleType: CENTER

vwidth, vheight: ImageViewのwidth, heightからpadding引いた値(pixel), mDrawableWidth, mDrawableWidth(Drawableの初期化の際に設定されるDrawableの画像の元々の大きさ(pixel))

// Drawableの大きさを元の画像の大きさにセットしておく
mDrawable.setBounds(0, 0, dwidth, dheight);

...

// Center bitmap in view, no scaling.
mDrawMatrix = mMatrix;
mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f),
                         Math.round((vheight - dheight) * 0.5f));

https://developer.android.com/reference/android/graphics/Matrix.html#setTranslate(float, float)

Matrix.setTranslate Set the matrix to translate by (dx, dy).とあるので、

説明文から見ても、Drawbleの画像の大きさを変えずに画像を中央に移動させています。

scaleType: CENTER_CROP

vwidth, vheight: ImageViewのwidth, heightからpadding引いた値(pixel), mDrawableWidth, mDrawableWidth(Drawableの初期化の際に設定されるDrawableの画像の元々の大きさ(pixel))

mDrawable.setBounds(0, 0, dwidth, dheight);

mDrawMatrix = mMatrix;

float scale;
float dx = 0, dy = 0;

if (dwidth * vheight > vwidth * dheight) {
    scale = (float) vheight / (float) dheight; 
    dx = (vwidth - dwidth * scale) * 0.5f;
} else {
    scale = (float) vwidth / (float) dwidth;
    dy = (vheight - dheight * scale) * 0.5f;
}

mDrawMatrix.setScale(scale, scale);
mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));

dwidth * vheight > vwidth * dheightがよくわからないんですけど、 d -> Orig(元画像), v -> Space(画像が入るスペース)と一旦添え字を変えて、

OrigWidth / OrigHeight > SpaceWidth / SpaceHeightと書き換えると、 アスペクト比を見て、元画像の方が画像が入るスペースより横長の場合は、となるので、

if (元画像の方が画像が入るスペースよりアスペクト比が横長の場合) {
    scale = (float) vheight / (float) dheight; 
    dx = (vwidth - dwidth * scale) * 0.5f;
} else {
  scale = (float) vwidth / (float) dwidth;
    dy = (vheight - dheight * scale) * 0.5f;
}

となって、scaleについては、元画像の方が画像が入るスペースよりアスペクト比が横長の場合(cf. 縦長のスペースに横長の画像を突っ込んだとか)は、高さを合わせるようにscaleを求めて、その大きさに拡大した画像が横方向に中心に来るように移動する距離(dx)を求めています。

一言で言うと、画像側の短い方の辺の長さを画像が入るスペースに揃えるように拡大縮小したら、はみ出した分を切り取る(ようにcanvas上で画像の位置を移動させている)となります。

scaleType: CENTER_INSIDE

vwidth, vheight: ImageViewのwidth, heightからpadding引いた値(pixel), mDrawableWidth, mDrawableWidth(Drawableの初期化の際に設定されるDrawableの画像の元々の大きさ(pixel))

mDrawable.setBounds(0, 0, dwidth, dheight);

mDrawMatrix = mMatrix;
float scale;
float dx;
float dy;

if (dwidth <= vwidth && dheight <= vheight) {
// 画像の縦横が、両方画像を入れるスペースの縦横より小さかったら縮小しない
    scale = 1.0f;
} else {
// 画像の縦横が、画像入れるスペースの縦横と比べて大きい方に合わせて拡大縮小する
// cf. 縦長のスペースに横長の画像を入れるとしたら、画像の横幅が縦長のスペースに合うように拡大縮小する
    scale = Math.min((float) vwidth / (float) dwidth,
            (float) vheight / (float) dheight);
}

// このとき、なるべく画像がスペースの中心に来るように移動させる
dx = Math.round((vwidth - dwidth * scale) * 0.5f);
dy = Math.round((vheight - dheight * scale) * 0.5f);

mDrawMatrix.setScale(scale, scale);
mDrawMatrix.postTranslate(dx, dy);

scaleType: FIT_XY

vwidth, vheight: ImageViewのwidth, heightからpadding引いた値(pixel), mDrawableWidth, mDrawableWidth(Drawableの初期化の際に設定されるDrawableの画像の元々の大きさ(pixel))

余白分あけてスペースいっぱい引伸ばす。アスペクト比なんてなかった。元画像のサイズがうまく取得できなかった場合も余白分あけてスペースいっぱい引伸ばす。

if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
    /* If the drawable has no intrinsic size, or we're told to
        scaletofit, then we just fill our entire view.
    */
    mDrawable.setBounds(0, 0, vwidth, vheight);
    // onDrawで再度拡大縮小はしない
    mDrawMatrix = null;
}

scaleType: MATRIX

mDrawable.setBounds(0, 0, dwidth, dheight);

// Use the specified matrix as-is.
if (mMatrix.isIdentity()) {
// 単位行列が設定されている場合は、余白などの設定以外何も差せない
// (scaleも移動もしない)
    mDrawMatrix = null;
} else {
// 設定されているmatrixを適用
    mDrawMatrix = mMatrix;
}

FIT_CENTER, FIT_END, FIT_START

https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java#L107-L109

// Avoid allocations...
private RectF mTempSrc = new RectF();
private RectF mTempDst = new RectF();

mDrawable.setBounds(0, 0, dwidth, dheight);

// Generate the required transform.
mTempSrc.set(0, 0, dwidth, dheight);
mTempDst.set(0, 0, vwidth, vheight);

mDrawMatrix = mMatrix;
// scaleTypeToScaleToFitでScaleTypeをMatrixの方のScaleTypeに変換して、
// Matrix.setRectToRectでそのScaleTypeの効果を適用させるようにしている
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));

ここにあるコードブロックではそうなんですが、適用されている効果が何なのかわからないのでもう少し追います。(Cだったらやめます)

https://github.com/android/platform_frameworks_base/blob/marshmallow-release/graphics/java/android/graphics/Matrix.java#L848

    private static native void native_setScale(long native_object,
                                        float sx, float sy, float px, float py);

Cだったので止めます。

ドキュメントから引用。

Type Value description
ImageView.ScaleType FIT_CENTER Scale the image using CENTER.
Matrix.ScaleToFit CENTER Compute a scale that will maintain the original src aspect ratio, but will also ensure that src fits entirely inside dst.
ImageView.ScaleType FIT_START Scale the image using START.
Matrix.ScaleToFit START Compute a scale that will maintain the original src aspect ratio, but will also ensure that src fits entirely inside dst.
ImageView.ScaleType FIT_END Scale the image using END.
Matrix.ScaleToFit END Compute a scale that will maintain the original src aspect ratio, but will also ensure that src fits entirely inside dst.

Matrix.ScaleToFit.CENTER, Matrix.ScaleToFit.START, Matrix.ScaleToFit.ENDの3つ、説明文が全く同じ気がしますが、

  • Matrix.ScaleToFit.CENTER 中央寄せ
  • Matrix.ScaleToFit.START 文頭寄せ
  • Matrix.ScaleToFit.END 文末寄せ

の違いがあります。詳しくはこちら

ScaleTypeがMATRIXの場合以外、サイズが合っていたら何もしない

        boolean fits = (dwidth < 0 || vwidth == dwidth) &&
                       (dheight < 0 || vheight == dheight);
       // 中略

            } else if (fits) {
                // The bitmap fits exactly, no transform needed.
                mDrawMatrix = null;

AndroidのImageViewのscaleTypeについて(検証結果スクショ編)

AndroidのImageViewのscaleTypeについて試したのでまとめました。

検証結果について

検証用画像

landscape_sample potrait_sample square_sample

検証結果スクショ

次の記事であるコード読んだときのメモのところで軽く触れているのですが、各種ScaleTypeはサイズがぴったり合っていたら何もしません。そのため、挙動を見る為には、

  • 横長のImageViewに縦長の画像(画像の縦幅がImageViewの縦幅より大きい/小さい)
  • 縦長のImageViewに横長の画像(画像の横幅がImageViewの横幅より大きい/小さい)
  • ImageViewと画像の縦横比は同じだけどサイズが大きい/小さい

といった条件の画像とImageViewを用意してみるとよさそうです、ということで用意して突っ込んだ結果が下表です。

ScaleType 縦長の画像 in 横長のImageView 横長の画像 in 縦長のImageView 正方形 in 正方形
指定なし(FIT_CENTER) 2017-01-19 2 14 21 2017-01-19 2 15 13 2017-01-19 2 15 45
CENTER 2017-01-19 2 17 41 2017-01-19 2 18 08 2017-01-19 2 18 25
CENTER_CROP 2017-01-19 2 19 49 2017-01-19 2 20 14 2017-01-19 2 20 36
CENTER_INSIDE 2017-01-19 15 54 18 2017-01-19 15 54 25 2017-01-19 15 54 38
FIT_START 2017-01-19 15 58 16 2017-01-19 15 58 24 2017-01-19 15 58 32
FIT_END 2017-01-19 15 56 21 2017-01-19 15 56 31 2017-01-19 15 56 38
FIT_XY 2017-01-19 16 01 31 2017-01-19 16 02 11 2017-01-19 16 02 16

検証用レイアウト

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.woshidan.imageapplication.MainActivity">

    <!-- scaleTypeを指定して色々試す -->
    <ImageView
        android:id="@+id/landscape_small_space"
        android:src="@drawable/potrait_sample"
        android:layout_width="100dp"
        android:layout_height="50dp"
        android:background="#333"
        />

    <ImageView
        android:id="@+id/landscape_large_space"
        android:src="@drawable/potrait_sample"
        android:layout_width="300dp"
        android:layout_height="150dp"
        android:background="#333"
        />

    <!--<ImageView-->
        <!--android:id="@+id/portrait_small_space"-->
        <!--android:src="@drawable/landscape_sample"-->
        <!--android:layout_width="50dp"-->
        <!--android:layout_height="100dp"-->
        <!--android:background="#333"-->
        <!--/>-->
    <!--<ImageView-->
        <!--android:id="@+id/portrait_large_space"-->
        <!--android:src="@drawable/landscape_sample"-->
        <!--android:layout_width="150dp"-->
        <!--android:layout_height="300dp"-->
        <!--android:background="#333" />-->
    <!---->

    <!--<ImageView-->
    <!--android:id="@+id/square_small_space"-->
    <!--android:src="@drawable/square_sample"-->
    <!--android:layout_width="50dp"-->
    <!--android:layout_height="50dp"-->
    <!--android:background="#333"-->
    <!--/>-->
    <!--<ImageView-->
    <!--android:id="@+id/square_large_space"-->
    <!--android:src="@drawable/square_sample"-->
    <!--android:layout_width="300dp"-->
    <!--android:layout_height="300dp"-->
    <!--android:background="#333" />-->
    <!---->
</LinearLayout>

build.gradleでminifyEnabledをtrueにしてもActivityはminifyされない

まとめ

  • Activityはgradleの設定でminifyをかけてもminifyされない
  • minifyされているかの確認をしたかったらJavaだけのクラスを仕込んで見るのがお手軽

検証1

gradleminifyの設定をした時、うまくいっていたらActivityのクラス名がaなどになっているだろうと下記のようなコード(検証用コード1)を書いてリリースビルドしたapkをインストールしたところ、ログには

01-19 22:43:13.181 9091-9091/? D/MainActivity: com.example.woshidan.minifytest.MainActivity

とminifyされていないクラス名が表示されていました。

検証用コード1

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.example.woshidan.minifytest"
        minSdkVersion 16
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
// proguard-rules.pro
// minifyされてるの見るテストなのでコメントのみ
package com.example.woshidan.minifytest;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.d("MainActivity", MainActivity.class.getName());
            }
        });
    }

検証2

先輩に相談したら、「Activityminifyできないとか?」という話の流れになって、「Lanucherに指定されているActivityでは?」「とりあえずAndroidフレームワークと関係ないJavaのクラスを入れてみては」という話になったので、次は下記のようなコード(検証用コード2)を書いてみたら、ログでminifyが確認できたのはJavaのクラスだけだったのでActivityminifyできないようでした。

01-19 22:57:26.532 19165-19165/? D/MainActivity: com.example.woshidan.minifytest.MainActivity
01-19 22:57:26.766 19165-19165/? D/SubActivity: com.example.woshidan.minifytest.SubActivity
01-19 22:57:26.768 19165-19165/? D/MinifyTester: com.example.woshidan.minifytest.a

検証用コード2

build.gradleproguard-rules.proは検証用コード1と同じ。

package com.example.woshidan.minifytest;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.d("MainActivity", MainActivity.class.getName());
                Intent intent = new Intent();
                intent.setClass(MainActivity.this, SubActivity.class);
                startActivity(intent);
            }
        });
    }
package com.example.woshidan.minifytest;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class SubActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sub);
        Log.d("SubActivity", SubActivity.class.getName());
        MinifyTester.testClassName();
    }
}
package com.example.woshidan.minifytest;

class MinifyTester {
    static void testClassName() {
        Log.d("MinifyTester", MinifyTester.class.getName());
    }
}

おなじくAndroidのシステムから呼ばれるコンポーネントServiceBroadcastReceiverも似たようなことがありそうな気がしますが、調べる機会があったら追記します。

Objective-Cのクラス変数について

C言語Objective-Cでstatic修飾子をつけて変数を宣言したときの挙動

Javaを書いてからObjective-Cのコードを見ると驚くのですが、Objective-Cの元となっているC言語、およびObjective-Cでは関数やメソッド*1の中で有効であり、特にそう書いてない場合は初期化され直さずに存在し続けます。

// test.c
int countUp(void) {
  static int count;
  count++:
}

int main(void) {
  countUp();
  countUp();
  countUp();
}
// countUpが呼び出されるたびにcountの値が増えていく。

こうして定義された関数/メソッド内のローカル変数は、プログラムの実行開始から終了まで存在し続ける変数となります。Javaのクラス変数がクラスの初呼び出し時に定義されることと比べると不思議な感じがしますね。

Objective-Cのクラス変数をどう表現するか

ところで、Objective-Cには、クラスオブジェクトに付随する変数、いわゆるクラス変数を表現するための構文上の仕組みがありません。

さて、先ほど関数/メソッド内のstatic 修飾子をつけて宣言した変数の話をしましたが、関数/メソッド外で宣言した場合はどうなるのでしょうか。

C言語では、関数、および関数の外部で定義された変数は、自動的にプログラム全体で共有されコード上のどこからでも参照できるようになります。これにstatic指定子をつけると、自身が定義されたソースファイル上とヘッダファイルで存在を教えたソースファイル上に参照範囲を限定できます。

Objective-Cでも同様の振る舞いをするため、関数/メソッドの外部で定義された static 修飾子をつけて宣言した変数をクラス変数の代わりとして利用します。

static 修飾子をつけてメソッド外部で宣言した変数は、そのままだとJavaでいうprivate修飾子をつけて宣言したクラス変数と似たような働きをし、そのクラスのクラスメソッドとインスタンスメソッドから参照できるようになります。

ただし、この方法で定義されたクラス変数は、サブクラスに継承されません。このため、サブクラスでクラス変数を扱いたい場合は、スーパークラスでアクセサメソッドを定義して、それを継承させるようにします。

参考

C言語のstatic指定子について

Objective-C Mac OS Xプログラミング

Objective-C Mac OS Xプログラミング

苦しんで覚えるC言語

苦しんで覚えるC言語

*1:Objective-Cでは、関数の記法、呼び出し方によって、C言語の関数とObjective-Cオブジェクトのメソッドに分けられ、C言語のようにfuncName(arg1, arg2)で呼び出すものを関数、[Obj funcName:arg1 WithArg:arg2]で呼び出すものをメソッドと言います。ランタイムでコードブロックの管理するときの扱いも若干異なるみたいです

プログラマが知るべき97のことを読みました

今月全然ブログ書いてなかったので書きます。

最近はObjectiveCやC言語の勉強をやっていてそちらを書けばいいじゃない、という気がしたのですが、 少し疲れてしまった。だいたい最初に詳解ObjectiveCから読んだせい(自業自得)。

その後少しずつObjectiveCプログラミングを読んでいるのですが、既視感と逆だヨォォォという気持ちでいっぱいです。 春まではObjectiveC, SwiftとiOSの勉強してるんじゃないかな...。

さて、きのこ本を読み終わったので印象に残った章をメモしておきます。来年とかに振り返って変わっていたら楽しいので。

誰にとっての利便性か

最近、プロジェクト外の人が使うコードを書くということを意識していて、機能追加の時、つい一つのメソッドで色々できるようにしないか考えそうになりますが、呼び出し側にとっては読みにくくなっていないか、ということをいつも頭に置いておきたいな、と思いました。

言語だけでなく文化も学ぶ & プログラミング言語は複数習得すべき

ObjectiveCやC言語といったいままで使っていたJavaRubyとは異なる毛色の言語を学んで色々思うところがあったので。 現状は、ObjectiveCかわいいよ、ObjectiveC(ぐったり)という気持ちなのですが、また来年書きます。

「魔法」に頼りすぎてはいけない

チームの雰囲気や文化が良いとかそういうのも魔法なんですよね、誰かがちょっと声をかけたりとか、そういうので成り立っていて、それはほんの少しの気遣いによるもので、ただではない、みたいな、そんな気持ち。

プロセス間通信とアプリケーションの応答時間の関係 & 並行処理に有効なメッセージパッシング

とりあえず並列処理をプロセスで書くというErlangを触ってみたいなぁ、という気持ち。

ポリモーフィズムの利用機会を見逃さない & リンカは魔法のプログラムではない

本体の文章もそうだけど、用語の説明がとてもわかりやすくてありがたかった。 こういう説明をできるようになりたいですね。

育ちの良いコード

このコードはいいのか悪いのか、がよくわからない時、他のコードをいじる必要があるかをパッチの単位から確認するやり方はいいな、と思いました。

JavaScriptでファイルのバイナリを送信するときと文字列のパラメータのみを送信するときに利用するリクエストボディの構成が全然違う

WebWorkerからサーバーへ通信を行いたくて、SafariだとWebWorkerではまだFormDataが使えない*1ので手動であれこれ頑張って調べていて*2面白かったことがあったので、メモ。

調査していた趣旨としては、FileAPIのFileReaderAPIのreadAsBinaryString()*3やreadAsArrayBuffer()でファイルの内容を読み込んだものをRailsのサーバへPOSTする、そのときよきにファイルとして扱ってもらう*4ためにうまいことリクエストパラメータを組み立てる、という話だったのですが、リクエストボディの文字列の構成からがらっと変える必要があったようです。

参考

https://developer.mozilla.org/ja/docs/Web/Guide/HTML/Forms/Sending_forms_through_JavaScript#Building_an_XMLHttpRequest_manually

http://api.rubyonrails.org/classes/ActionDispatch/Request.html

普通の文字列を送信するフォームの場合のリクエストボディ

<form method="post" action="/texts/upload">
  <input type="text" name="string_param" value="文字だよ" />
  <input type="hidden" name="authenticity_token" id="authenticity_token" value="<%= form_authenticity_token %>"/>
  <input type='submit' value='送信' />
</form>

上記のようなフォームの送信ボタンからサーバーへリクエストを送った場合、ActionDispatch::Request#raw_postメソッドで取得できるリクエストボディは下記のようにURIエンコードされたキーと値の組を&でつないだものとなります。

string_param=%E6%96%87%E5%AD%97%E3%81%A0%E3%82%88&authenticity_token=ymyqd1fC0%2FbOWLdEO546DPd%2BOUm9ljC1Qu0rYvErwBVtRJjUG4O%2BRsH7%2FZemkU8yIR3s7MLuujtiAex89NqkUA%3D%3D

ファイルなどのバイナリを送信するフォームの場合のリクエストボディ

var formData = new FormData();
formData.append("string_params", "文字だよ"); // バイナリデータ以外のパラメータがある場合の実験用
formData.append("file", file); // fileはFileクラスのインスタンス
$.ajax(
    {
    url: "/files/upload",
    type: 'POST',
    contentType: false,
    processData: false,
    headers: {
        'X-CSRF-Token' : $('#authenticity_token').val()
        },
    data: formData,
    dataType: 'json',
    success: function(response) {
        console.log(response);
    },
    error: function(error) {
      console.log(error);
    }
});

上のように、適当にフォームからFileインスタンスを取得してFormDataを利用してPOSTした場合のリクエストボディは下のようになります。

# パラメータごとに
# ------boundary(WebKitFormBoundar乱数文字列)------\r\n
# Content-Disposition: form-data; name="パラメータ名"\r\n\r\n
# 値(バイナリ?)\r\n------
# という構成になっている
------WebKitFormBoundarypUNpzkXNsa1wjYql\r\nContent-Disposition: form-data; name=\"string_params\"\r\n\r\n\xE6\x96\x87\xE5\xAD\x97\xE3\x81\xA0\xE3\x82\x88\r\n------WebKitFormBoundarypUNpzkXNsa1wjYql\r\nContent-Disposition: form-data; name=\"file\"; filename=\"UPLOAD_TEST.JPG\"\r\nContent-Type: image/jpeg\r\n\r\n\xFF\xD8\xFF\xE1\x00TExif\x00\x00MM\x00*\x00\x00\x00\b\x00\x03\x012\x00\x02\x00\x00\x00\x14\x00\x00\x002\x87i\x00\x04\x00\x00\x00\x01\x00\x00\x00F\x01\x12\x00\x03\x00\x00\x00\x01\x00\x01\x00...
(中略)
...xD1\x7F\x90\xA2\x85{\x14\x7F\xFF\xD9\r\n------WebKitFormBoundarypUNpzkXNsa1wjYql--\r\n

全然違いますね。文章力がなくてあれなんですけど、これ気づいたときすごい面白くてはしゃいで走り回りそうでした。

普段はFormDataオブジェクトを使うか、Androidやってるときもライブラリを使うので意識したことがなかったので、なるほどーという感じでした。

仕事の進め方としては、途中でしばらくhexdumpでバイナリの最初の方を読んでたけど、どう考えても一気に下のレイヤーまで遡り過ぎであり、先にHTTPのリクエストボディを読んだ方が筋が良かったので反省...。

*1:https://developer.mozilla.org/ja/docs/Web/API/FormData

*2:結局締め切り直前でアップロードキューに当たる変数へのアクセスを排他制御したかっただけなので、当分ほぼ1ファイル100行くらいで完結するスクリプトだったこともあり、キューいじる部分はUIスレッドでいいやん、と方針切り替えて一時間くらいで書いてた...

*3:これから廃止されていくので新規実装で使ってはダメ

*4:request.paramsしたらパラメータの値が、ActionDispatch::Http::UploadedFileインスタンスになってる、みたいな感じ

irbの実行結果の出力が非常に長い場合、途中からカットしたい

最近しばしば行う作業のログが、おそらく人間の目には確認できないほど長く、また件数的におそらくログから確認を行うことはしない*1ということもあり、じゃあ使わないログはある程度短くしたいですね、ということで調べました。

具体的には、属性がたくさんあるクラスについてのログで、最初の方のフィールドしか確認する必要がない場合、後半は出さなくてもいいのでは...というケースを対象としています。

# この例だとuser_idあたりまでがあれば業務上十分
irb(main):045:0> Memo.new
=> #<Memo:0x007fcf43045500 @id=123456, @user_id=12, @category_id=3456, @sub_category_id=nil, @reminded_at=2016-10-22 10:00:00 +0900, @created_at=2016-10-21 10:00:00 +0900, @updated_at=2016-10-21 10:00:00 +0900>

動作確認用の雑なクラス定義はこちらのgistにあります。

irbの実行結果の出力形式を変更する

IRB::Inspector.def_inspectorメソッドで、irbの実行結果の出力形式を独自に設定することができます。具体的には、引数に設定の名前としてのkeyとブロックを与えることで、ブロックの返り値を出力として利用できます。

なお、このメソッドは .irbrcというファイルに記述します。

# ~/.irbrc
# たとえば出力を2回表示するように設定してみます
IRB::Inspector.def_inspector([:test]){|v| v.to_s * 2 }

.irbrcに書いた上記の設定を適用するには、起動時に--inspectオプションで指定するかirb起動後にIRB.conf[:INSPECT_MODE]変数から設定します。

$ irb --inspect test
irb(main):001:0> abc = "def"
=> defdef

この要領で、出力結果の表示を50文字までにするような設定を書いてみます。

IRB::Inspector.def_inspector([:test2]){|v| v.to_s.slice(0..50) }
$ irb --inspect test2
# (クラス定義中...)
irb(main):015:0> Memo.new
=> id: 123456 user_id: 12 category_id: 3456 sub_catego

これでいいかなと一瞬思ったのですが、この方法の場合、出力に使う返り値のクラスが適切にto_sメソッドを実装している前提なので、いちいち各クラスのto_sメソッドを確認したり定義したりする必要があって大変そうです。

また、ppメソッドなどの出力結果を利用することも考えたのですが、ppメソッドの戻り値は、出力対象のクラスなので思っていたより簡単にはいきません。

irb(main):015:0> require 'pp'
=> true
irb(main):017:0> res = Memo.new
=> #<Memo:0x007fefe5068ad8 @id=123456, @user_id=12, @category_id=3456, @sub_category_id=nil, @reminded_at=2016-10-22 10:00:00 +0900, @created_at=2016-10-21 10:00:00 +0900, @updated_at=2016-10-21 10:00:00 +0900>
irb(main):018:0> res
=> #<Memo:0x007fefe5068ad8 @id=123456, @user_id=12, @category_id=3456, @sub_category_id=nil, @reminded_at=2016-10-22 10:00:00 +0900, @created_at=2016-10-21 10:00:00 +0900, @updated_at=2016-10-21 10:00:00 +0900>
irb(main):019:0> res.class
=> Memo

また、IRB::Inspector.def_inspectorメソッドで紹介したirbの出力方式は当然独自方式だけではなく、yaml, ppなどいくつかのオプションがあり、そちらを使っている場合も多い中、出力方式そのものを変えることができない場合もあるでしょう。

では、最終的に出力が渡ってきた後で、その文字列を切り取る方法がないかな、と思って、irbのプロンプトそのもののカスタマイズについて調べました。

irbプロンプトのカスタマイズ

irbプロンプトの設定をする際も、.irbrcに記述します。

記述項目は、IRB.conf[:PROMPT]で、下記のような形式となっています。

IRB.conf[:PROMPT][:SAMPLE] = {
  :PROMPT_I => "%N(%m):%03n:%i> ",  # 通常時のプロンプト
  :PROMPT_N => "%N(%m):%03n:%i> ",  # 継続行のプロンプト
  :PROMPT_S => "%N(%m):%03n:%i%l ", # 文字列などの継続行のプロンプト
  :PROMPT_C => "%N(%m):%03n:%i* ",  # 式が継続している時のプロンプト
  :RETURN => "==> s\n"   # メソッドから戻る時のプロンプト
}

%sとあって、あ、これCのprintfっぽいな、と思って http://d.hatena.ne.jp/iww/20090701/printf などを参考に、英数字40文字あたりで切れてください、というつもりで書いた設定は下記となります。

IRB.conf[:PROMPT][:MY_PROMPT] = {
  :PROMPT_I => "%N(%m):%03n:%i> ",  # 通常時のプロンプト
  :PROMPT_N => "%N(%m):%03n:%i> ",  # 継続行のプロンプト
  :PROMPT_S => "%N(%m):%03n:%i%l ", # 文字列などの継続行のプロンプト
  :PROMPT_C => "%N(%m):%03n:%i* ",  # 式が継続している時のプロンプト
  :RETURN => "==> %.80s\n"   # メソッドから戻る時のプロンプト
}

上記の設定を適用するには起動時に--promptオプションで指定します。

irb --prompt my-prompt
# (クラス定義中...)
irb(main):015:0> Memo.new
==> #<Memo:0x007f99b2010920 @id=123456, @user_id=12, @category_id=3456, @sub_categor

ためしに、出力形式をyml形式に変えても途中で出力をカットするように動いているか見てみます。

irb(main):017:0> conf.inspect_mode = :yaml
==> --- :yaml
...

irb(main):018:0> Memo.new
==> --- !ruby/object:Memo
id: 123456
user_id: 12
category_id: 3456
sub_category_id:

よさそうですね。

仕事で導入するのはやや怖いところもありますが、興味が出たので調べてみた結果はこんな感じです。

現場からは以上です。

参考

*1:ログは長いけれど、作業時間は短いのでやり直したほうが正確っぽい