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が出てくるのは下記です。
// 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がどこで使われているかというと、主にImageView
のonDraw
メソッド内部で実行される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
// 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だったらやめます)
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について試したのでまとめました。
検証結果について
検証用画像
検証結果スクショ
次の記事であるコード読んだときのメモのところで軽く触れているのですが、各種ScaleTypeはサイズがぴったり合っていたら何もしません。そのため、挙動を見る為には、
- 横長のImageViewに縦長の画像(画像の縦幅がImageViewの縦幅より大きい/小さい)
- 縦長のImageViewに横長の画像(画像の横幅がImageViewの横幅より大きい/小さい)
- ImageViewと画像の縦横比は同じだけどサイズが大きい/小さい
といった条件の画像とImageViewを用意してみるとよさそうです、ということで用意して突っ込んだ結果が下表です。
ScaleType | 縦長の画像 in 横長のImageView | 横長の画像 in 縦長のImageView | 正方形 in 正方形 |
---|---|---|---|
指定なし(FIT_CENTER) | |||
CENTER | |||
CENTER_CROP | |||
CENTER_INSIDE | |||
FIT_START | |||
FIT_END | |||
FIT_XY |
検証用レイアウト
<?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
gradle
でminify
の設定をした時、うまくいっていたら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
先輩に相談したら、「Activity
はminify
できないとか?」という話の流れになって、「Lanucher
に指定されているActivity
では?」「とりあえずAndroidフレームワークと関係ないJavaのクラスを入れてみては」という話になったので、次は下記のようなコード(検証用コード2)を書いてみたら、ログでminifyが確認できたのはJavaのクラスだけだったのでActivity
がminify
できないようでした。
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.gradle
とproguard-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のシステムから呼ばれるコンポーネントのService
やBroadcastReceiver
も似たようなことがありそうな気がしますが、調べる機会があったら追記します。
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
修飾子をつけて宣言したクラス変数と似たような働きをし、そのクラスのクラスメソッドとインスタンスメソッドから参照できるようになります。
ただし、この方法で定義されたクラス変数は、サブクラスに継承されません。このため、サブクラスでクラス変数を扱いたい場合は、スーパークラスでアクセサメソッドを定義して、それを継承させるようにします。
参考
- 作者: 荻原剛志
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2006/04/07
- メディア: 大型本
- クリック: 94回
- この商品を含むブログ (24件) を見る
- 作者: MMGames
- 出版社/メーカー: 秀和システム
- 発売日: 2011/06/24
- メディア: 単行本
- 購入: 3人 クリック: 600回
- この商品を含むブログ (1件) を見る
*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言語といったいままで使っていたJavaやRubyとは異なる毛色の言語を学んで色々思うところがあったので。 現状は、ObjectiveCかわいいよ、ObjectiveC(ぐったり)という気持ちなのですが、また来年書きます。
「魔法」に頼りすぎてはいけない
チームの雰囲気や文化が良いとかそういうのも魔法なんですよね、誰かがちょっと声をかけたりとか、そういうので成り立っていて、それはほんの少しの気遣いによるもので、ただではない、みたいな、そんな気持ち。
プロセス間通信とアプリケーションの応答時間の関係 & 並行処理に有効なメッセージパッシング
とりあえず並列処理をプロセスで書くというErlangを触ってみたいなぁ、という気持ち。
ポリモーフィズムの利用機会を見逃さない & リンカは魔法のプログラムではない
本体の文章もそうだけど、用語の説明がとてもわかりやすくてありがたかった。 こういう説明をできるようになりたいですね。
育ちの良いコード
このコードはいいのか悪いのか、がよくわからない時、他のコードをいじる必要があるかをパッチの単位から確認するやり方はいいな、と思いました。
JavaScriptでファイルのバイナリを送信するときと文字列のパラメータのみを送信するときに利用するリクエストボディの構成が全然違う
WebWorkerからサーバーへ通信を行いたくて、SafariだとWebWorkerではまだFormDataが使えない*1ので手動であれこれ頑張って調べていて*2面白かったことがあったので、メモ。
調査していた趣旨としては、FileAPIのFileReaderAPIのreadAsBinaryString()*3やreadAsArrayBuffer()でファイルの内容を読み込んだものをRailsのサーバへPOSTする、そのときよきにファイルとして扱ってもらう*4ためにうまいことリクエストパラメータを組み立てる、という話だったのですが、リクエストボディの文字列の構成からがらっと変える必要があったようです。
参考
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のリクエストボディを読んだ方が筋が良かったので反省...。
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:
よさそうですね。
仕事で導入するのはやや怖いところもありますが、興味が出たので調べてみた結果はこんな感じです。
現場からは以上です。
参考
- https://docs.ruby-lang.org/ja/latest/library/irb.html
- https://docs.ruby-lang.org/ja/2.0.0/method/IRB=3a=3aINSPECTORS/s/def_inspector.html
- http://d.hatena.ne.jp/iww/20090701/printf
*1:ログは長いけれど、作業時間は短いのでやり直したほうが正確っぽい